Currency (FX) prediction using RNN

[In the previous article, I tried to predict the stock price, so I will try to predict the forex market. As with stock prices, there are a variety of factors that can cause fluctuations, so it will be difficult to make a prediction using only a neural network model, but I’ll try it as an exercise in Keras.

github

  • The file in jupyter notebook format is here

google colaboratory

  • If you want to run it in google colaboratory here

Author’s environment

The author’s OS is macOS, and the options are different from Linux and Unix commands.

! sw_vers
ProductName: Mac OS X
ProductVersion: 10.14.6
BuildVersion: 18G6020
Python -V
Python 3.7.3

Import the basic libraries and keras and check their versions.

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

import matplotlib
import matplotlib.pyplot as plt
import scipy
import numpy as np
import pandas as pd

import tensorflow as tf
from tensorflow import keras

print('matplotlib version :', matplotlib.__version__)
print('scipy version :', scipy.__version__)
print('numpy version :', np.__version__)
print('tensorflow version : ', tf.__version__)
print('keras version : ', keras.__version__)
matplotlib version : 3.0.3
scipy version : 1.4.1
numpy version : 1.19.4
tensorflow version : 2.4.0
keras version : 2.4.0

Getting the data

This time, we will make predictions for three currency pairs (daily): USD/JPY, EUR/USD, and GBP/USD. We have downloaded the data from the following website.

Checking the data

First of all, let’s look at the USDJPY data.

!ls . /data/
01_USDJPY_D.csv 03_EURUSD_D.csv 05_GBPUSD_D.csv eurusd_utf8.csv gbpusd_utf8.csv usdjpy_utf8.csv
%%bash
head . /data/01_USDJPY_D.csv
"ʉʉ݃y�A","����t","��n�l","����l","����l","��I�l"
"1", "2021/01/07",103.001,103.958,102.950,103.814
"1", "2021/01/06",102.697,103.445,102.592,103.069
"1", "2021/01/05",103.054,103.192,102.605,102.719
"1", "2021/01/04",103.083,103.316,102.713,103.152
"1", "2020/12/31",103.128,103.317,102.995,103.292
"1", "2020/12/30",103.530,103.584,102.962,103.211
"1", "2020/12/29",103.722,103.806,103.463,103.531
"1", "2020/12/28",103.496,103.899,103.404,103.788
"1", "2020/12/25",103.615,103.656,103.486,103.585
%%bash
nkf --guess . /data/01_USDJPY_D.csv
Shift_JIS (LF)

Change to utf-8.

%%bash
nkf -w . /data/01_USDJPY_D.csv > . /data/usdjpy_utf8.csv
nkf -w . /data/03_EURUSD_D.csv > . /data/eurusd_utf8.csv
nkf -w . . /data/05_GBPUSD_D.csv > . /data/gbpusd_utf8.csv

The character encoding is shift-jis, so change it to utf-8.

. %%bash
head . /data/usdjpy_utf8.csv
"Currency Pair", "Date", "Open", "High", "Low", "Close"
"1", "2021/01/07",103.001,103.958,102.950,103.814
"1", "2021/01/06",102.697,103.445,102.592,103.069
"1", "2021/01/05",103.054,103.192,102.605,102.719
"1", "2021/01/04",103.083,103.316,102.713,103.152
"1", "2020/12/31",103.128,103.317,102.995,103.292
"1", "2020/12/30",103.530,103.584,102.962,103.211
"1", "2020/12/29",103.722,103.806,103.463,103.531
"1", "2020/12/28",103.496,103.899,103.404,103.788
"1", "2020/12/25",103.615,103.656,103.486,103.585

This looks fine, so we’ll load it in pandas.

df_u = pd.read_csv('. /data/usdjpy_utf8.csv')
df_e = pd.read_csv('. /data/eurusd_utf8.csv')
df_g = pd.read_csv('. /data/gbpusd_utf8.csv')
df_u.head()
Currency pairDateOpen priceHigh priceLow priceClose price
012021/01/07103.001103.958102.950103.814
112021/01/06102.697103.445102.592103.069
212021/01/05103.054103.192102.605102.719
312021/01/04103.083103.316102.713103.152
412020/12/31103.128103.317102.995103.292
df_u.tail()
Currency pairDateOpen priceHigh priceLow priceClose price
19512020/04/08108.775109.099108.500108.845
19612020/04/07109.188109.263108.670108.796
19712020/04/06108.388109.382108.388109.200
19812020/04/03107.800108.678107.781108.460
19912020/04/02107.119108.091107.011107.857

Sort by date.

df_u.sort_values(by=["date"], ascending=True, inplace=True)
df_e.sort_values(by=["date"], ascending=True, inplace=True)
df_g.sort_values(by=["date"], ascending=True, inplace=True)

Graphing Data

ticks = 10
xticks = ticks * 5

plt.plot(df_u['date'][::ticks], df_u['close'][::ticks], label='usd/jpy')
plt.grid()
plt.legend()
plt.xticks(df_u['date'][::xticks], rotation=60)
plt.show()
plt.plot(df_e['date'][::ticks], df_e['close'][::ticks], label='eur/usd')
plt.grid()
plt.legend()
plt.xticks(df_u['date'][::xticks], rotation=60)
plt.show()
plt.plot(df_g['date'][::ticks], df_g['close'][::ticks], label='gbp/usd')
plt.grid()
plt.legend()
plt.xticks(df_u['date'][::xticks], rotation=60)
plt.show()
df_u.shape
(200, 6)
df_e.shape
(200, 6)
df_g.shape
(200, 6)

Data shaping

Compute the percentage change from the initial data and train on the list of values.

def shape_data(data_list):
  return [d / data_list[0] - 1 for d in data_list].

df_u['data_list'] = shape_data(list(df_u['closing']))
df_e['data_list'] = shape_data(list(df_e['closing price']))
df_g['data_list'] = shape_data(list(df_g['closing price']))
plt.plot(df_u['date'][::ticks], df_u['data_list'][::ticks], label='usd/jpy')
plt.grid()
plt.legend()
plt.xticks(df_u['date'][::xticks], rotation=60)
plt.show()

Preparing the constants

Since we have 200 bars of daily data, we will split the forecast into two parts.

### We have about four years of data, so we will divide it into 8 parts and make forecasts in each area.
TERM_PART_LIST = [0, 100, 200].

# Number of data to use for prediction.
# Predict the next 25 data from 75 data
NUM_LSTM = 75

# Number of intermediate layers
NUM_MIDDLE = 200

# Constants for the neural network model
batch_size = 100
epochs = 50
validation_split = 0.25

Prepare the data

Prepare the data for submission to keras.

def get_x_y_lx_ly(currency='usdjpy', term_part=0):
  if currency == 'usdjpy':
    date = np.array(df_u['date'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
    x = np.array(df_u.index.values[TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
    y = np.array(df_u['data_list'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])

  if currency == 'eurusd':
    date = np.array(df_e['date'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
    x = np.array(df_e.index.values[TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
    y = np.array(df_e['data_list'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])

  if currency == 'gbpusd':
    date = np.array(df_g['date'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
    x = np.array(df_g.index.values[TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
    y = np.array(df_g['data_list'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])

  n = len(y) - NUM_LSTM
  l_x = np.zeros((n, NUM_LSTM))
  l_y = np.zeros((n, NUM_LSTM))

  for i in range(0, n):
    l_x[i] = y[i: i + NUM_LSTM].
    l_y[i] = y[i + 1: i + NUM_LSTM + 1].

  l_x = l_x.reshape(n, NUM_LSTM, 1)
  l_y = l_y.reshape(n, NUM_LSTM, 1)

  return n, date, x, y, l_x, l_y

n, date, x, y, l_x, l_y = get_x_y_lx_ly('usdjpy', 0)
print('shape : ', x.shape)
print('ndim : ', x.ndim)
print('data : ', x[:10])
shape : (100,)
ndim : 1
data : [199 198 197 196 195 194 193 192 191 190].
print('shape : ', y.shape)
print('ndim : ', y.ndim)
print('data : ', y[:10])
shape : (100,)
ndim : 1
data : [ 0. 0.00559074 0.01245167 0.00870597 0.00916028 0.0058967
  0.0050159 -0.00100133 -0.00599868 -0.0039404 ]
print(l_y.shape)
print(l_x.shape)
(25, 75, 1)
(25, 75, 1)

Model building

This function defines the construction of the model. The default is RNN.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import LSTM
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import SimpleRNN
from tensorflow.keras.layers import GRU


def build_model(model_name='RNN'):
  # Build an LSTM neural net
  model = Sequential()

  # Make it possible to choose between RNN, LSTM and GRU
  if model_name == 'RNN':
    model.add(SimpleRNN(NUM_MIDDLE, input_shape=(NUM_LSTM, 1), return_sequences=True))

  if model_name == 'LSTM':
    model.add(LSTM(NUM_MIDDLE, input_shape=(NUM_LSTM, 1), return_sequences=True))

  if model_name == 'GRU':
    model.add(GRU(NUM_MIDDLE, input_shape=(NUM_LSTM, 1), return_sequences=True))

  model.add(Dense(1, activation="linear"))
  model.compile(loss="mean_squared_error", optimizer="sgd")

  return model


# Deepen the neural net (not used in this case)
def build_model_02():

  NUM_MIDDLE_01 = 50
  NUM_MIDDLE_02 = 50

  # Build the LSTM neural net
  model = Sequential()
  model.add(SimpleRNN(NUM_MIDDLE_01, input_shape = (NUM_LSTM, 1), return_sequences=True))
  model.add(Dropout(0.2))
  # model.add(SimpleRNN(NUM_MIDDLE_02, return_sequences=True))
  # model.add(Dropout(0.2))
  model.add(Dense(1))
  model.add(Activation("linear"))
  model.compile(loss="mean_squared_error", optimizer="sgd")
  # model.compile(loss="mse", optimizer='rmsprop')

  return model

model = build_model('RNN')

Model Details

model.summary()
Model: "sequential".
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn (SimpleRNN) (None, 75, 200) 40400
_________________________________________________________________
dense (Dense) (None, 75, 1) 201
=================================================================
Total params: 40,601
Trainable params: 40,601
Non-trainable params: 0
_________________________________________________________________
# use validation_split to use the last 10% for validation
history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)

Visualization of the loss function

Let’s visualize the error decreasing with learning. val_loss is increasing and we are overlearning. We can see that val_loss is increasing, indicating overtraining, and we need to normalize and DropOut.

loss = history.history['loss'].
val_loss = history.history['val_loss']

plt.plot(np.arange(len(loss)), loss, label='loss')
plt.plot(np.arange(len(val_loss)), val_loss, label='val_loss')
plt.grid()
plt.legend()
plt.show()

Checking the results with RNN

The period filled in light orange is the period we used for prediction. In that period, the prediction is consistent with the actual trend. The solid orange line is the actual stock price trend, and the blue line is the forecast.

def plot_result():

  # Initial input values
  res = [].
  res = np.append(res, l_x[0][0][0])
  res = np.append(res, l_y[0].reshape(-1))

  for i in range(0, n):
    _y = model.predict(res[- NUM_LSTM:].reshape(1, NUM_LSTM, 1))

    # Use the predicted data as input data for the next prediction
    res = np.append(res, _y[0][NUM_LSTM - 1][0])

  res = np.delete(res, -1)

  plt.plot(date, y, label="price", color='coral')
  plt.plot(date, res, label="prediction result", color='blue')
  plt.xticks(date[::12], rotation=60)

  plt.legend()
  plt.grid()

  plt.axvspan(0, NUM_LSTM, color="coral", alpha=0.2)

  plt.show()

print('{} - {} results'.format(date[0], date[NUM_LSTM - 1]))
plot_result()
Results for 2020/04/02 - 2020/07/15

USDJPY

Predicts the second half of the usdjpy period.

currency = 'usdjpy'
for term in [1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(currency, term)
  model = build_model('RNN')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('{} :'.format(currency))
  print('Prediction period : {} - {} results'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
usdjpy :
Results for the forecast period : 2020/08/20 - 2020/12/02

EURUSD

currency = 'eurusd'
for term in [0, 1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(currency, term)
  model = build_model('RNN')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('{} :'.format(currency))
  print('Prediction period : {} - {} results'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
eurusd :
Results for the forecast period : 2020/04/02 - 2020/07/15
eurusd :
Results for the forecast period : 2020/08/20 - 2020/12/02

GBPUSD

currency = 'gbpusd'
for term in [0, 1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(currency, term)
  model = build_model('RNN')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('{} :'.format(currency))
  print('Prediction period : {} - {} results'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
gbpusd :
Results for the forecast period : 2020/04/02 - 2020/07/15
gbpusd :
Result for the forecast period : 2020/08/20 - 2020/12/02

GBPUSD ver2

Let’s try the model with Dropout to counter overtraining.

currency = 'gbpusd'

n, date, x, y, l_x, l_y = get_x_y_lx_ly(currency, 1)
model = build_model_02()
history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
print('{} :'.format(currency))
print('Prediction period : {} - {} results'.format(date[0], date[NUM_LSTM - 1]))
plot_result()

# Loss function
loss = history.history['loss'].
val_loss = history.history['val_loss']

plt.plot(np.arange(len(loss)), loss, label='loss')
plt.plot(np.arange(len(val_loss)), val_loss, label='val_loss')
plt.grid()
plt.legend()
plt.show()
gbpusd :
Results for the forecast period : 2020/08/20 - 2020/12/02

Unlike the previous example, the overlearning measure is indeed working, but the prediction is way off the trend…

Summary

Currency forecasting is said to be more difficult than stock forecasting because there are many more component factors. I think it is reckless to make such a simple model prediction, but the purpose of this article is to get used to keras, so I’ll stop here. There are some parts that are 180 degrees out of phase, and some parts that are way off…