Русский 中文 Español Deutsch 日本語 Português
preview
Reimagining Classic Strategies (Part VII) : Forex Markets And Sovereign Debt Analysis on the USDJPY

Reimagining Classic Strategies (Part VII) : Forex Markets And Sovereign Debt Analysis on the USDJPY

MetaTrader 5Examples | 29 August 2024, 16:42
1 031 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Artificial Intelligence holds the potential to create new trading strategies for the modern investor. It is unlikely that any single investor will have enough time to carefully assess each possible strategy before deciding which one to trust with their capital. In this series of articles, we aim to present you with the information you need to arrive at an informed decision as to which strategy best fits your particular investor profile.


Synopsis of The Trading Strategy

Fixed income securities are investments that allow investors to diversify their portfolios safely. They are a class of investments that pay a fixed or floating rate of return until maturity. Upon maturity, the investor’s principal is paid back, and no further payments will be made to the investor. There are many different types of fixed income securities, such as bonds and Certificates of Deposit.

Bonds are among the most popular forms of fixed income securities and will be the focus of our discussion. Bonds may be issued by a Corporation or a Government. Government Bonds in particular are among the safest investments in the World. If an investor wishes to purchase a particular Government’s Bond, they must do so in the Currency of the issuing state. If a particular Government’s Bond is in high demand internationally, every investor desiring to acquire the bond will first convert their domestic Currency into the desired Currency. This may in turn shift the market’s beliefs about a fair valuation of the two currencies' exchange rate. 

How well a bond is performing is measured by the yield of the bond. There is an inverse relationship between the yield of a bond and the level of demand for that bond. In other words, as demand for a particular bond falls, the yield of the bond rises to rally demand for the bond. Some successful traders in the Currency markets incorporate this fundamental analysis into their trading strategy. By comparing the yields of mid-long term Government bonds from the 2 countries in any exchange rate, Currency traders may gain an intuition about the economic conditions of the two countries in question.

Typically, the bond offering investors higher interest rates will be more popular and according to the strategy, the Currency of the issuing country will also appreciate over time, and the Currency of the country issuing bonds with lower interest rates will depreciate over time. 


Synopsis of The Methodology

To assess the strategy, we train various models to predict the closing price of the USDJPY exchange rate. We had 3 sets of predictors for the models:

  1. Ordinary Open, High, Low, Close, Tick Volume (OHLCV) data fetched from the USDJPY Market.
  2. OHLCV data on the Japanese Government 10-Year Bond and the American Government 10-Year Treasury Note.
  3. A super set of the first two.

Our goal was to identify which set of predictors would produce a model with the lowest RMSE on unseen data. Although the correlation levels between the bonds' historical prices and the USDJPY were significantly strong, -0.85 for both Government bonds, the lowest test error rate was produced by models trained from the first set of predictors.

The best model we identified was the Linear Regression (LR) model. However, it has no parameters we can tune. Therefore, we selected the Linear Support Vector Regressor (LSVR) as our candidate solution. We successfully performed hyperparameter tuning on the LSVR model without overfitting to the training set. Furthermore, our customized LSVR model was able to outperform the benchmark performance set by the simpler LR model on validation data. The models were trained and compared using time series cross validation without random shuffling.

After successfully tuning our model, we exported it to ONNX format and integrated it into our customized Expert Advisor. 


Fetching Data

Let us get started, first we shall import the libraries we need.

#Import the libraries we need
import pandas as pd
import numpy as np
import MetaTrader5 as mt5
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
import sklearn
from sklearn.preprocessing import RobustScaler
from sklearn.model_selection import train_test_split

Here are the versions of the libraries we are using.

#Show library versions
print(f"Pandas version: {pd.__version__}")
print(f"Numpy version: {np.__version__}")
print(f"MetaTrader 5 version: {mt5.__version__}")
print(f"Matplotlib version: {matplotlib.__version__}")
print(f"Seaborn version: {sns.__version__}")
print(f"Scikit-learn version: {sklearn.__version__}")

Pandas version: 1.5.3

Numpy version: 1.24.4

MetaTrader 5 version: 5.0.45

Matplotlib version: 3.7.1

Seaborn version: 0.13.0

Scikit-learn version: 1.2.2

Let us initialize our terminal.

#Initialize the terminal
mt5.initialize()

True

Defining how far into the future we wish to forecast.

#Define how far ahead into the future we should forecast
look_ahead = 20

Fetching the time-series data that we need from the MetaTrader 5 terminal.

#Fetch historical market data 
usa_10y_bond = pd.DataFrame(mt5.copy_rates_from_pos("UST10Y_U4",mt5.TIMEFRAME_M1,0,100000))
jpn_10y_bond = pd.DataFrame(mt5.copy_rates_from_pos("JGB10Y_U4",mt5.TIMEFRAME_M1,0,100000))
usd_jpy      = pd.DataFrame(mt5.copy_rates_from_pos("USDJPY",mt5.TIMEFRAME_M1,0,100000))

The data frame time column needs to be formatted.

#Convert the time from seconds
usa_10y_bond["time"] = pd.to_datetime(usa_10y_bond["time"],unit="s")
jpn_10y_bond["time"] = pd.to_datetime(jpn_10y_bond["time"],unit="s")
usd_jpy["time"] = pd.to_datetime(usd_jpy["time"],unit="s")

We should set the time column as our index, this will make it easier for us to merge our 3 data frames into 1.

#Prepare to merge the data
usa_10y_bond.set_index("time",inplace=True)
jpn_10y_bond.set_index("time",inplace=True)
usd_jpy.set_index("time",inplace=True)

Merging the data frames.

#Merge the data
merged_data = usa_10y_bond.merge(jpn_10y_bond,how="inner",left_index=True,right_index=True,suffixes=(" usa"," japan"))
merged_data = merged_data.merge(usd_jpy,left_index=True,right_index=True)


Exploratory Data Analysis


Let us create a copy of the data frame that we will use for plotting purposes.

data_visualization = merged_data

We need to reset the index of the visualization data.

#Reset the index
data_visualization.reset_index(inplace=True)

Scale all the column values so that they all begin with one.

#Let's scale the data so all the first values in the column are one
for i in np.arange(1,data_visualization.shape[1]):
    data_visualization.iloc[:,i] = data_visualization.iloc[:,i] / data_visualization.iloc[0,i]

Let us plot the 3 time-series to see if there are any observable relationships.

#Let's create a plot
plt.figure(figsize=(10, 5))
plt.plot(data_visualization.loc[:,"open usa"])
plt.plot(data_visualization.loc[:,"open japan"])
plt.plot(data_visualization.loc[:,"open"])
plt.legend(["USA 10Y T-Note","JGB 10Y Bond","USDJPY Fx Rate"])

Our market data.

Fig 1: Visualizing our market data.

There appears to be no discernible relationship when we overlay the 3 markets. Let us try and make the plot easier to read, by plotting the spread between the American and Japanese bonds. That way, we only need to consider the USDJPY exchange rate and the USA JPY 10-Y Bond spread. Or in other words, the 3 curves we plotted above, can be fully represented by only 2 curves.

First, we need to calculate the spread between the bonds.

#Let's create a new feature to show the spread between the securities
data_visualization["spread"] = data_visualization["open usa"] - data_visualization["open japan"]

On the left side of the chart, we see a sample of the USDJPY exchange rate, whenever the exchange rate surpasses 1, the Dollar is performing better than the Yen, the opposite holds true when the exchange rate falls below 1. Furthermore, whenever the spread rises above 0, the American bonds are performing better than the Japanese bonds and the converse is true when the spread falls below 0. Therefore, when the spread is below 0 meaning the Japanese bonds are performing better in the market, we would also expect to see the equilibrium exchange shift in favor of the Yen. However, by visually inspecting the plots with our eyes, we can quickly observe that this expectation does not always hold true.

#Visualizing the results of using the bonds predictors
fig,axs = plt.subplots(1,2,sharex=True,sharey=False,figsize=(8,4))
columns = ["open","spread"]

for i,ax in enumerate(axs.flat):
    ax.plot(data_visualization.loc[:,columns[i]])
    ax.set_title(columns[i])

Visualizing the spread on the exchange rate.

Fig 2: Visualizing the bond spread on the exchange rate.

Let us now label our data. 

#Label the data
merged_data["target"] = merged_data["close"].shift(-look_ahead)
merged_data["binary target"] = np.nan
merged_data.loc[merged_data["close"] > merged_data["target"],"binary target"] = 0
merged_data.loc[merged_data["close"] < merged_data["target"],"binary target"] = 1
merged_data.dropna(inplace=True)
merged_data.reset_index(inplace=True)
merged_data


Our labelled data.

Fig 3: The current state of our data frame.

Now we need to define our target and inputs.

#Define the predictors and target
target = "target"
ohlc_predictors = ['open', 'high', 'low', 'close','tick_volume']
bonds_predictors = ['open usa','high usa','low usa','close usa','tick_volume usa','open japan','high japan', 'low japan', 'close japan','tick_volume japan']
predictors = ['open usa','high usa','low usa','close usa','tick_volume usa','open japan','high japan', 'low japan', 'close japan','tick_volume japan','open', 'high', 'low', 'close','tick_volume']

Let us analyze the correlation levels in our dataset.

#Analyze correlation levels
plt.subplots(figsize=(8,6))
sns.heatmap(merged_data.loc[:,predictors].corr(),annot=True)


Our correlation matrix


Fig 4: Our correlation matrix.

As we can observe, there are strong correlation levels between the American and Japanese bonds, 0.76. Furthermore, both the American and Japanese bond securities have strong negative correlation levels with the USDJPY exchange rate. 

Scatter plots allow us to visualize relationships between variables in 2 dimensions, let us create scatter plots using the data we have collected from the bond market. We will start by creating a scatter plot of the opening price of the American Treasury note against the opening price of the USDJPY exchange rate.

Scatter plot 1

Fig 5: A scatter plot of the USA Bond Open price against the USDJPY Open price.

As one can observe, there is no clear pattern or dependency being exposed by the scatter plot. It appears that the exchange rate may appreciate or depreciate, regardless of the changes occurring in the bond market.

We also performed another scatter plot using the opening price of the Japanese Government bond on the x-axis and the opening price of the USDJPY exchange rate on the y-axis. Unfortunately, there was still no visible relationship in the data.

Scatterplot 2

Fig 6: A scatter plot of the Japanese Government Bond Open price against the USDJPY Open price.

We also tried to create another scatter plot, this time using both Government bonds on each axis. We used the opening price of the Japanese Government bond on the x-axis, and the American Treasury notes were on the y-axis. Our scatter plot didn’t reveal any interesting patterns in the data, this may indicate to us that there may be other variables we aren’t considering that are also affecting the data.

Scatter plot 3

Fig 7: A scatter plot of the Japanese Government Bond Open price against the American Government Bond Open price.

Let us also check to see if there is any relationship between the tick volume of the American bond market and the closing price of the USDJPY exchange rate. Unfortunately, there is no clear separation in the scatter plot, we observe many instances where price rose and fell on the same tick volume reading.

Scatter plot 4

Fig 8: A scatter plot of the American Government Bond tick volume against the USDJPY Close price.


Modelling The Data

We are now ready to start modelling our data, we will start off by scaling and standardizing our dataset. This helps our machine learning models learn effectively.

#Scale the data
scaled_data = pd.DataFrame(RobustScaler().fit_transform(merged_data.loc[:,predictors]),columns=predictors)

Then we will partition our dataset into two halves, one half will be used to train and optimize our models, while the latter will be used to validate our models and test for overfitting.

#Partition the data
train_X , test_X, train_y, test_y = train_test_split(scaled_data,merged_data.loc[:,target],shuffle=False,test_size=0.5)

To effectively test various models, we will keep our models in a list so that we can loop over them and cross validate each of their performances in turn. We will also need to create 3 data frames:

  1. The first data frame will store our error levels when only using ordinary OHLCV data from the USDJPY market.
  2. The second data frame will store our error levels when only relying on OHCLV data from both bond markets.
  3. And the last data frame will store our error levels when incorporating all the data we have available.

#Model selection
from sklearn.linear_model import LinearRegression , Lasso , SGDRegressor
from sklearn.svm import LinearSVR
from sklearn.ensemble import GradientBoostingRegressor , RandomForestRegressor , BaggingRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.metrics import mean_squared_error 
from sklearn.model_selection import TimeSeriesSplit

#Define the columns
columns = [
    "Linear Model",
    "Lasso",
    "SGD",
    "Linear SV",
    "Gradient Boost",
    "Random Forest",
    "Bagging",
    "K Neighbors",
    "Neural Network"
]

#Define the models
models = [
    LinearRegression(),
    Lasso(),
    SGDRegressor(),
    LinearSVR(),
    GradientBoostingRegressor(),
    RandomForestRegressor(),
    BaggingRegressor(),
    KNeighborsRegressor(),
    MLPRegressor(hidden_layer_sizes=(100,40,20,10),shuffle=False)
]

#Create 2 dataframes to store our error on the training and test sets respectively
ohlc_training_loss = pd.DataFrame(index=np.arange(0,5),columns=columns)
ohlc_validation_loss = pd.DataFrame(index=np.arange(0,5),columns=columns)
bonds_training_loss = pd.DataFrame(index=np.arange(0,5),columns=columns)
bonds_validation_loss = pd.DataFrame(index=np.arange(0,5),columns=columns)
all_training_loss = pd.DataFrame(index=np.arange(0,5),columns=columns)
all_validation_loss = pd.DataFrame(index=np.arange(0,5),columns=columns)
#Create the time-series split object
tscv = TimeSeriesSplit(n_splits=5,gap=look_ahead)

We will now cross validate each of our models. The outer loop will iterate over each model we have available, while the inner loop will cross validate each model and store our respective training and test error levels. Note that we are cross validating the models on the training set only.

#Now perform cross validation
for j in np.arange(0,len(models)):
    model = models[j]
    for i,(train,test) in enumerate(tscv.split(train_X)):
        model.fit(train_X.loc[train[0]:train[-1],predictors],train_y.loc[train[0]:train[-1]])
        all_training_loss.iloc[i,j] = mean_squared_error(train_y.loc[train[0]:train[-1]],model.predict(train_X.loc[train[0]:train[-1],predictors]))
        all_validation_loss.iloc[i,j] = mean_squared_error(train_y.loc[test[0]:test[-1]],model.predict(train_X.loc[test[0]:test[-1],predictors]))

Let us now observe our error levels when using ordinary OHLCV data from the USDJPY market. As we can see, the linear model performed and the Linear Support Vector regressor performed notably well in this particular setup.

#Our results using the OHLC data
ohlc_validation_loss

Our OHLCV error levels

Fig 9: Our OHLCV error levels.

Let us visualize the results. We will start off with a line plot of the performance of each model in our 5-fold cross validation procedure.

#Visualizing the results of using the OHLC predictors
plt.plot(ohlc_validation_loss)
plt.legend(columns)

Our OHLCV error values

Fig 10: Line plots of our OHLCV error values. 

We can clearly see that the Lasso was the worst performing model, its validation error rate was the greatest by a significant margin. However, it is not clear which model is achieving the lowest error rate, we can use box plots to answer that question.

Box plots help us quickly identify which models are performing well in this particular task. As we can see from the plot below, the linear regression has the lowest average error levels, furthermore it appears stable and it has the lowest outlier value.

#Visualizing the results of using the OHLC predictors
fig,axs = plt.subplots(2,4,sharex=True,sharey=True,figsize=(16,10))

for i,ax in enumerate(axs.flat):
    ax.boxplot(ohlc_validation_loss.iloc[:,i])
    ax.set_title(columns[i])

Our error levels when using USDJPY OHLCV data

Fig 11: Some of our error levels when using ordinary USDJP OHLCV

When we used the data related to the government bonds, our performance levels dropped across the board. However, the Linear Support Vector Regressor (Linear SVR) appears to be able to handle this data quite well.

#Our results using the bonds data
bonds_validation_loss

Our error results when using the Bonds data

Fig 12: Our error levels when using the bonds data.

Let us visualize the results.

#Visualizing the results of using the bonds predictors
plt.plot(bonds_validation_loss)
plt.legend(columns)

Our error levels when using the Bonds data

Fig 13: A line plot of our validation error when using Bond Data to predict the USDJPY exchange rate.

We can also employ box plots to assess our error levels.

#Visualizing the results of using the bonds predictors
fig,axs = plt.subplots(2,4,sharex=True,sharey=True,figsize=(16,10))

for i,ax in enumerate(axs.flat):
    ax.boxplot(bonds_validation_loss.iloc[:,i])
    ax.set_title(columns[i])

Our error levels when using the Bonds Data

Fig 14: Some of our error levels when using OHLCV data from the Bond market to predict the future close price of the USDJPY

Finally, when we incorporated all the available data, our error levels improved in comparison to our previous step, however they were not as satisfactory when compared to our error levels using just the market quotes from the USDJPY market.

#Our results using all the data we have
all_validation_loss

Our error levels when using all the data we have

Fig 15: Our error levels when using all the data we have.

Let us visualize our performance.

#Visualizing the results of using the bonds predictors
plt.plot(all_validation_loss)
plt.legend(columns)

Our error levels when predicting the USDJPY close using all the data we have.

Fig 16:Our error levels when predicting the USDJPY close using all the data we have.

The Linear Regression model is clearly our best option here. However, it no hyperparameters of interest to us. Therefore, we will select the second-best model, the Linear SVR, and attempt to tune it to outperform the Linear model without overfitting to the training set. Before optimizing the model, let us assess which features are important to the model. If our strategy is viable, we would expect our feature elimination algorithms to retain the column. Otherwise, if the bond data is discarded we may have reason to revise the strategy.

#Visualizing the results of using the bonds predictors
fig,axs = plt.subplots(2,4,sharex=True,sharey=True,figsize=(16,10))

for i,ax in enumerate(axs.flat):
    ax.boxplot(all_validation_loss.iloc[:,i])
    ax.set_title(columns[i])

Our error levels when using all the data we have

Fig 17: Our Linear Model performed the best when using all the available data.



Feature Selection

Let us first start by calculating Shapley (SHAP) values. SHAP values are a metric designed to inform us the impact that each input has on our model’s predictions, when compared to a baseline value for each column. For example, consider a model predicting the likelihood of a driver getting a speeding ticket. If we wanted to assess whether our model is capable of making reasonable predictions, we may ask, “How does our model interpret the fact that the driver’s blood alcohol level is high?”.

Obviously, we would expect our model to predict higher probabilities of getting a speeding ticket if you’re driving under the influence of alcohol. SHAP Values help us answer questions of this nature by rephrasing the question to include a baseline value, “How does our model interpret the fact that the driver’s blood alcohol level is above the legal limit?”.

By including the legal limit, we have defined a baseline. Therefore, we calculate our SHAP Values by performing calculations on the difference between the model’s predictions when the driver’s blood alcohol levels are beneath and above the legal limits. 


Let us import the SHAP library.
#Feature selection
import shap

Now, we need to train our model.

#The SVR performed quite well, let's inspect it further
model = LinearSVR()
model.fit(train_X,train_y)

Let us fit the SHAP explainer.

#Calculate SHAP Values
explainer = shap.Explainer(model.predict,test_X)
shap_values = explainer(test_X)

Let us view the SHAP plot.

shap.plots.beeswarm(shap_values)

Our SHAP Values

Fig 18: Our SHAP Values from our Linear SVR model.

The features are arranged in order, starting with the most important at the top. Therefore, it appears that the close value of the USDJPY is the most important feature according to our SHAP explanations. Furthermore, we can also see that our data related to the government bonds was just after all the price data of the Currency pair. This is good evidence backing our strategy, our SHAP values consider that the bond data is more important than the tick volume of the USDJPY market itself.

However, all model explanations must be taken with a grain of salt. They are not immune to error.

Let us also consider backward selection. The backward selection algorithm begins by fitting a full model, and eliminates features sequentially until the test error can no longer by improved. 

Let us import the mlxtend library.

#Let's also perform backward selection
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs

Initialize the model.

#Reinitialize the model
model = LinearSVR()

Create the feature selector object.

#Prepare the feature selector
sfs = SFS(model,
         k_features=(1,train_X.shape[1]),
         forward=False,
          n_jobs = -1,
          scoring="neg_mean_squared_error",
          cv=5)

Fit the feature selector.

#Fit the feature selector
sfs_results = sfs.fit(train_X,train_y)

Let us see the features selected.

#The best features we identified
sfs_results.k_feature_names_

('open usa',

 'high usa',

 'tick_volume usa',

 'open japan',

 'low japan',

 'close',

 'tick_volume')

Our backward elimination algorithm gave more importance to the bond market data than our SHAP Values. Therefore, we may reasonably conclude that there may be a reliable relationship between our bond data and the future exchange rate of the USDJPY pair.


Let us plot the results.

#Prepare the plot
fig1 = plot_sfs(sfs_results.get_metric_dict(),kind="std_dev")
plt.title("Backward Selection on our Linear SVR")
plt.grid()

Our backward elimination results

Fig 19: Our backward elimination results.

It appears that our model's error rates do not violently fluctuates, meaning that our model may be stable even under circumstances of limited data. Remember, the algorithm eliminates features one by one until the error rate cannot be improved anymore by removing any of the features selected by the algorithm.


Hyperparameter Tuning

Let us now optimize our model to outperform the Linear Regression.

First, import the libraries we need.

#Parameter tuning 
from sklearn.model_selection import RandomizedSearchCV

Initialize the model.

#Reinitialize the model
model = LinearSVR()

Define the tuner object.

tuner = RandomizedSearchCV(model,
                          {
                              "epsilon":[0,0.001,0.01,0.1,25,50,100],
                              "tol": [0.1,0.01,0.001,0.0001,0.00001],
                              "C" : [1,5,10,50,100,1000,10000,100000],
                              "loss":["epsilon_insensitive", "squared_epsilon_insensitive"],
                              "fit_intercept": [False,True]
                          },
                           n_jobs=-1,
                           n_iter=100,
                           scoring="neg_mean_squared_error"
                          )

Tune the model.

tuner_results = tuner.fit(train_X,train_y)

It’s quite interesting to note that our best parameters are almost identical to the default settings. However, let us observe the difference in performance.

tuner_results.best_params_

{'tol': 0.0001,

 'loss': 'epsilon_insensitive',

 'fit_intercept': True,

 'epsilon': 0,

 'C': 1}


Testing For Overfitting

Let us now test if we were overfitting the training set. We will instantiate our models.
#Testing for overfitting
baseline_model = LinearRegression()
default_model =  LinearSVR()
customized_model = LinearSVR(tol=0.0001,loss='epsilon_insensitive',fit_intercept=True,epsilon=0,C=1)

Now, let us fit all 3 models.

#Fit the models
baseline_model.fit(train_X,train_y)
default_model.fit(train_X,train_y)
customized_model.fit(train_X,train_y)

Preparing to cross validate each model’s performance.

#Create a list of models
models = [
    baseline_model,
    default_model,
    customized_model
]

columns = [
    "Linear Regression",
    "Default Linear SVR",
    "Customized Linear SVR"
]

We need to reset the index of our datasets.
#Let's assess our new accuracy levels
test_y = test_y.reset_index()
test_X.reset_index(inplace=True)

Redefine the time series split object and create a data frame to store our validation error.

#Create our time-series test object
tscv = TimeSeriesSplit(n_splits=5,gap=look_ahead)
overfitting_error = pd.DataFrame(columns=columns,index=np.arange(0,5))

Cross-validate each model.
for j in np.arange(0,len(columns)):
    model = models[j]
    for i , (train,test) in enumerate(tscv.split(test_X)):
        model.fit(test_X.loc[train[0]:train[-1],predictors],test_y.loc[train[0]:train[-1],"target"])
        overfitting_error.iloc[i,j] = mean_squared_error(test_y.loc[test[0]:test[-1],"target"],model.predict(test_X.loc[test[0]:test[-1],predictors]))

Let us see the results.

#Visualizing the results of using the bonds predictors
fig,axs = plt.subplots(1,3,sharex=True,sharey=True,figsize=(8,4))

for i,ax in enumerate(axs.flat):
    ax.boxplot(overfitting_error.iloc[:,i])
    ax.set_title(columns[i])

Our error levels on unseen data.

Fig 20: Our error levels on unseen data

We can clearly see that our LinearSVR model produced the lowest average error in validation. Therefore, we have managed to outperform the benchmark set by the Linear Model. Furthermore, we also outperformed the default error rate without overfitting to the training set.


Exporting To ONNX

Let us now prepare to export our model to ONNX format so that we can easily integrate it into our MQL5 program.

Before we can progress, we must first standardize our data in a fashion we can reproduce in MQL5. We can accomplish this by subtracting the column mean from each respective column value, and subsequently dividing each column by its standard deviation.

Let us write out the respective values to a CSV file, in our Terminal’s file path.

#Create scaling factors
scaling_factors = pd.DataFrame(index=("mean","standard deviation"),columns=predictors)

#Write our the values
for i in np.arange(0,scaling_factors.shape[1]):
    scaling_factors.iloc[0,i] = merged_data.loc[:,predictors[i]].mean()
    scaling_factors.iloc[1,i] = merged_data.loc[:,predictors[i]].std()
    merged_data.loc[:,predictors[i]] = ((merged_data.loc[:,predictors[i]] - scaling_factors.iloc[0,i]) / scaling_factors.iloc[1,i])

scaling_factors

Our scaling factors.

Fig 21: Our scaling factors.

Now we shall save the CSV file.

#Save the scaling factors
scaling_factors.to_csv("C:\\Enter \\Your\\Path\\Here\\MetaQuotes\\Terminal\\D0E82094358C8CF3394F550E51FF075\\MQL5\\Files\\usdjpy scaling factors.csv")

Let us train the model on all the data we have available.

#Fit the model on all the data we have
customized_model.fit(merged_data.loc[:,predictors],merged_data.loc[:,"target"])

Import the libraries we need.

#Let's import the libraries we need
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import convert_sklearn
import netron
import onnx

Define the input type and shape of our ONNX model.

#Define the initial input types
initial_types = [('float_input',FloatTensorType([1,len(predictors)]))]

Create an ONNX model.

#Create an ONNX representation of the model
onnx_model = convert_sklearn(customized_model,initial_types=initial_types,target_opset=12)

Save the ONNX model to a file with the .onnx extension.

#Save the ONNX model
onnx_name = "USDJPY M1 FLOAT.onnx"
onnx.save(onnx_model,onnx_name)

Let us visualize the model in netron.

#Visualize the model
netron.start(onnx_name)

Our LinearSVR model

Fig 22: Visualizing our Linear SVR model.


Our ONNX model metadetails

Fig 23: Our ONNX model input and output shape.

Our model’s input and output shape are inline with our specifications. Let us proceed to build the Expert Advisor.


Implementation in MQL5

We shall first require our ONNX model as a resource that will be compiled into our program.
//+------------------------------------------------------------------+
//|                                                 USDJPY Bonds.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
#resource "\\Files\\USDJPY M1 FLOAT.onnx" as const uchar onnx_model_buffer[];

Now let us define a few global variables we need throughout our program.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long onnx_model;
float mean_values[15],std_values[15];
vector model_output = vector::Zeros(1);
int state = 0;
int prediction = 0;

Import the trade library so we can easily open and manage positions.

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
CTrade Trade;

Now we shall define helper functions for our Expert Advisor. We need a function that will load our ONNX model and define its input and output shapes. If we fail at any point in the procedure, our function will return a flag that will break the initialization procedure.

//+------------------------------------------------------------------+
//| Load our onnx file                                               |
//+------------------------------------------------------------------+
bool load_onnx_file(void)
  {
//--- Create the model from the buffer
   onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT);

//--- Set the input shape
   ulong input_shape [] = {1,15};

//--- Check if the input shape is valid
   if(!OnnxSetInputShape(onnx_model,0,input_shape))
     {
      Alert("Incorrect input shape, model has input shape ", OnnxGetInputCount(onnx_model));
      return(false);
     }

//--- Set the output shape
   ulong output_shape [] = {1,1};

//--- Check if the output shape is valid
   if(!OnnxSetOutputShape(onnx_model,0,output_shape))
     {
      Alert("Incorrect output shape, model has output shape ", OnnxGetOutputCount(onnx_model));
      return(false);
     }
//--- Everything went fine
   return(true);
  }

We also need a function to read the CSV file that has the scaling values and store them in an array for us to use later, in our predict function. Note that, the first row only contains the column titles. The first entry in the second row is the index label, and the second entry in the second row is the mean of the first column. Therefore, our function will check the current loop iteration to keep track of where it is and which values are important.

//+------------------------------------------------------------------+
//| Load our scaling factors                                         |
//+------------------------------------------------------------------+
void load_scaling_factors(void)
    {
//--- Read in the file
   string file_name = "usdjpy scaling factors.csv";

//--- Try open the file
   int result = FileOpen(file_name,FILE_READ|FILE_CSV|FILE_ANSI,","); //Strings of ANSI type (one byte symbols). 

//--- Check the result
   if(result != INVALID_HANDLE)
     {
      Print("Opened the file");
      //--- Store the values of the file
      
      int counter = 0;
      string value = "";
      
      while(!FileIsEnding(result) && !IsStopped()) //read the entire csv file to the end 
       {
       
         if (counter > 100) //if you aim to read 10 values set a break point after 10 elements have been read
           break; //stop the reading progress
         
         value = FileReadString(result);
         Print("Trying to read string: ",value," count value: ",counter);
         
         //--- Check where we are
         if((counter >= 17) && (counter < 32))
            {
               mean_values[counter - 17] = (float) value;
            }   
         //--- Check where we are
         if((counter >= 33) && (counter < 48))
            {
               std_values[counter - 33] = (float) value;
            }   
         //--- Reading a new row
         if(FileIsLineEnding(result))
           { 
             Print("row++");
           }
         
         counter++;
       }
      //---Close the file
      ArrayPrint(mean_values);
      ArrayPrint(std_values);
      FileClose(result);
     }
//--- We failed to find the file
else 
   {
      Print("Failed to find the file");
   }

  }

This function will fetch our model input values and standardize them before obtaining a prediction from our model. Subsequently, the model’s prediction will be stored as a binary state, 1 is a bullish prediction and 2 is a bearish position. This will help us identify when our model is predicting a reversal.

//+------------------------------------------------------------------+
//| Obtain a prediction from our model                               |
//+------------------------------------------------------------------+
void model_predict(void)
   {
     //--- Fetch input values
      string symbols[3] = {"UST10Y_U4","JGB10Y_U4","USDJPY"};
      vectorf model_inputs = {iOpen(symbols[0],PERIOD_CURRENT,0),iHigh(symbols[0],PERIOD_CURRENT,0),iLow(symbols[0],PERIOD_CURRENT,0),iClose(symbols[0],PERIOD_CURRENT,0),iTickVolume(symbols[0],PERIOD_CURRENT,0),
                      iOpen(symbols[1],PERIOD_CURRENT,0),iHigh(symbols[1],PERIOD_CURRENT,0),iLow(symbols[1],PERIOD_CURRENT,0),iClose(symbols[1],PERIOD_CURRENT,0),iTickVolume(symbols[1],PERIOD_CURRENT,0),
                      iOpen(symbols[2],PERIOD_CURRENT,0),iHigh(symbols[2],PERIOD_CURRENT,0),iLow(symbols[2],PERIOD_CURRENT,0),iClose(symbols[2],PERIOD_CURRENT,0),iTickVolume(symbols[2],PERIOD_CURRENT,0)
                     };
     //--- Normalize and scale our inputs
     for(int i=0;i < 15;i++)
         {
            model_inputs[i] = ((model_inputs[i] - mean_values[i])/std_values[i]);
         }
     //--- Show the inputs
     Print("Model inputs: ",model_inputs);
     //--- Fetch a forecast from our model
     OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_output);
     //--- Give the user feedback
     Comment("Model forecast: ",model_output[0]);
     
     //--- Store the prediction
     if(model_output[0] > iClose("USDJPY",PERIOD_CURRENT,0))
         {
            prediction = 1;
         }
     else if(model_output[0] < iClose("USDJPY",PERIOD_CURRENT,0))
         {
            prediction = 2;
         }       
   }

Our initialization procedure will first require we successfully load the ONNX file, before we read in the scaling values and finally test if our model works.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
  
//--- Load the ONNX file
   if(!load_onnx_file())
     {
      //--- We failed to load our onnx model
      return(INIT_FAILED);
     }
     
//--- Load scaling factors
load_scaling_factors();

//--- Test if our ONNX model works
model_predict();

//--- Everything worked out
   return(INIT_SUCCEEDED);
   
  }

Whenever our program is no longer in use, we must free up the resources we no longer need.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the resources we used for our onnx model
   OnnxRelease(onnx_model);
//--- Release the expert advisor
   ExpertRemove();
  }

Finally, whenever we have changes in price levels, we will first obtain a prediction from our model. If we have no open positions, we will follow our model’s prediction and store a flag to represent our current open position. Otherwise, if we already have open positions, we will check if our model’s forecast is inline with our open positions, in the case that it is not, we will close our open positions.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
      //--- Obtain a forecast from our model
      model_predict();
      
      //--- Check if we have any positions
      if(PositionsTotal() == 0)
         {
            //--- Reset the state of our system
            state = 0;
            
            //--- Check for an entry
            if(model_output[0] > iClose("USDJPY",PERIOD_CURRENT,0))
               {
                  Trade.Buy(0.3,"USDJPY",SymbolInfoDouble("USDJPY",SYMBOL_ASK),SymbolInfoDouble("USDJPY",SYMBOL_ASK)-2,SymbolInfoDouble("USDJPY",SYMBOL_ASK)+2,"USDJPY Bonds AI");
                  state = 1;
               }
            
             if(model_output[0] < iClose("USDJPY",PERIOD_CURRENT,0))
               {
                  Trade.Sell(0.3,"USDJPY",SymbolInfoDouble("USDJPY",SYMBOL_BID),SymbolInfoDouble("USDJPY",SYMBOL_ASK)+2,SymbolInfoDouble("USDJPY",SYMBOL_ASK)-2,"USDJPY Bonds AI");
                  state = 2;
               }
         }
         
      //--- Check for reversals
      if(state != prediction)
         {
            Alert("Reversal detected by the AI system!");
            Trade.PositionClose("USDJPY");
         }
  }
//+------------------------------------------------------------------+

Our program in action.

Fig 24: Forward testing our program.

Our AI model detected a reversal

Fig 25: Our Expert Advisor can close positions automatically whenever it detects a reversal.


Conclusion

In this article, we have demonstrated how you can employ AI to give new life to a classic trading strategy. Whether our strategy is worth its complexity is debatable, we could’ve gotten lower accuracy levels using a simpler model. Therefore, we may reasonably conclude that unless more time is invested in transforming the features to better expose the relationship then we may be better off using a simpler strategy of just the ordinary market quotes. 
Attached files |
USDJPY_Bonds.mq5 (8.22 KB)
Example of Causality Network Analysis (CNA) and Vector Auto-Regression Model for Market Event Prediction Example of Causality Network Analysis (CNA) and Vector Auto-Regression Model for Market Event Prediction
This article presents a comprehensive guide to implementing a sophisticated trading system using Causality Network Analysis (CNA) and Vector Autoregression (VAR) in MQL5. It covers the theoretical background of these methods, provides detailed explanations of key functions in the trading algorithm, and includes example code for implementation.
Developing a Replay System (Part 45): Chart Trade Project (IV) Developing a Replay System (Part 45): Chart Trade Project (IV)
The main purpose of this article is to introduce and explain the C_ChartFloatingRAD class. We have a Chart Trade indicator that works in a rather interesting way. As you may have noticed, we still have a fairly small number of objects on the chart, and yet we get the expected functionality. The values present in the indicator can be edited. The question is, how is this possible? This article will start to make things clearer.
Developing a multi-currency Expert Advisor (Part 8): Load testing and handling a new bar Developing a multi-currency Expert Advisor (Part 8): Load testing and handling a new bar
As we progressed, we used more and more simultaneously running instances of trading strategies in one EA. Let's try to figure out how many instances we can get to before we hit resource limitations.
Implementing a Rapid-Fire Trading Strategy Algorithm with Parabolic SAR and Simple Moving Average (SMA) in MQL5 Implementing a Rapid-Fire Trading Strategy Algorithm with Parabolic SAR and Simple Moving Average (SMA) in MQL5
In this article, we develop a Rapid-Fire Trading Expert Advisor in MQL5, leveraging the Parabolic SAR and Simple Moving Average (SMA) indicators to create a responsive trading strategy. We detail the strategy’s implementation, including indicator usage, signal generation, and the testing and optimization process.