English 中文 Español Deutsch 日本語 Português
preview
Переосмысливаем классические стратегии (Часть IV): SP500 и казначейские облигации США

Переосмысливаем классические стратегии (Часть IV): SP500 и казначейские облигации США

MetaTrader 5Примеры |
441 1
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Введение

В предыдущей статье мы обсудили торговую стратегию S&P 500, основанную на использовании ряда акций, имеющих высокий вес в индексе. В этой статье мы рассмотрим альтернативный подход к торговле индексом S&P 500 с использованием доходности казначейских облигаций. На протяжении многих лет, когда инвесторы не хотели рисковать, они обычно забирали свои деньги из рискованных инвестиций, таких как акции, и предпочитали вкладывать их в более безопасные инвестиции, такие как облигации и казначейские обязательства. И наоборот, когда инвесторы обретают уверенность в рынках, они, как правило, забирали свои деньги из надежных инвестиций, таких как облигации, и вместо этого вкладывают их в фондовый рынок.

Со временем фундаментальные аналитики поняли, что эта корреляция между движениями индекса S&P 500 и движениями доходности казначейских облигаций, по-видимому, является отрицательной. То есть инвесторы, вложившие больше средств в акции, склонны вкладывать меньше средств в облигации и казначейские обязательства.


Обзор торговой стратегии

Индекс S&P 500 является важным индикатором эффективности промышленной экономики США в очень широком смысле. С другой стороны, казначейские облигации считаются самыми безопасными инвестициями на Земле. Когда инвестор покупает облигацию или казначейский билет, он по сути одалживает деньги правительству, выпустившему этот казначейский билет. По каждой казначейской облигации выплачиваются процентные купоны, которые указаны на лицевой стороне облигации.

Когда спрос на облигации низкий, доходность облигаций растет. Это делается для того, чтобы возродить спрос. Таким образом, по мере того как всё меньше инвесторов покупают облигации, мы видим рост доходности. Фундаментальные аналитики уже давно используют эту взаимосвязь в своих интересах. При торговле на S&P 500, они ищут признаки ослабления тренда.

Если доходность облигаций начинает расти, фундаментальные аналитики понимают, что инвесторы не покупают облигации, а вкладывают свои деньги в ценные бумаги, которые принесут им больше прибыли, например, в акции.

Если же доходность облигаций падает, это признак того, что спрос на них очень высок. Это подсказывает фундаментальному аналитику, что ему, вероятно, пока не следует инвестировать в фондовый рынок, поскольку его общее настроение неблагоприятно для риска. Это знание можно использовать для входа и выхода из позиций.

В сегодняшней статье мы хотим выяснить, является ли эта взаимосвязь статистически значимой и можно ли на ее основе строить торговую стратегию.


Обзор методологии

Чтобы эмпирически изучить достоинства этой стратегии, мы подгоним различные модели для прогнозирования цены закрытия SP500, используя обычные данные OHLC самого индекса, а затем будем наблюдать изменение точности, пытаясь обучить модели прогнозировать ту же цель. Однако на этот раз модели будут иметь доступ только к данным OHLC пятилетних казначейских облигаций США. Наши наблюдения привели нас к выводу, что инвесторам может быть выгоднее использовать данные индекса SP500. Уровень эффективности нашей модели снизился по всем направлениям, и, более того, дисперсия наших уровней ошибок увеличилась, когда мы попытались использовать данные по казначейским облигациям. Для сравнения моделей различной сложности мы использовали перекрестную проверку временных рядов без случайного перемешивания.

После наблюдения за изменениями в уровнях ошибок мы выделили SGD-регрессор в качестве наиболее эффективной модели, а затем выполнили отбор признаков для нее. Ни один из данных, связанных с казначейскими облигациями, не был выбран нашим селектором признаков, что указывает на то, что взаимосвязь может быть статистически незначимой. Хотя на тот момент у нас было достаточно доказательств того, что мы можем отказаться от данных по казначейским облигациям, мы сохранили эти данные и продолжили строить нашу модель.

На последнем этапе перед экспортом модели в формат ONNX мы попытались настроить гиперпараметры модели. Мы использовали алгоритм L-BFGS-B (Limited-Memory Broyden-Fletcher-Goldfarb-Shanno, ограниченная память Бройдена-Флетчера-Гольдфарба-Шенно), чтобы найти оптимальные настройки параметров нашей модели. Нашей целью было превзойти производительность настроек модели по умолчанию. К сожалению, в итоге мы переобучили нашу модель и не смогли превзойти модель по умолчанию.


Исследовательский анализ данных в Python

Чтобы извлечь данные из терминала MetaTrader 5, я создал скрипт для записи исторических рыночных данных в формат CSV. Скрипт приложен к статье. Просто перетащите его на график, и он выведет для нас данные.

После подготовки данных мы начинаем импортировать необходимые нам библиотеки.

#Import the libraries we need 
import pandas as pd
import numpy as np
import seaborn as sns

Считаем наши данные.

#Read in the data
SP500 = pd.read_csv("/home/volatily/market_data/Market Data US SP 500.csv")
T5Y = pd.read_csv("/home/volatily/market_data/Market Data UST05Y_U4.csv")

Нам необходимо определить, насколько далеко в будущее мы хотели бы заглянуть. В этом примере мы будем делать прогноз на 20 шагов вперед.

#How far into the future should we forecast?
look_ahead = 20

Теперь нам также необходимо убедиться, что данные начинаются с самого старого дня, а самый последний день во всех данных должен быть потерян.

#Make sure the data starts with the oldest day first
SP500 = SP500[::-1].reset_index().set_index("Time").drop(columns=["index"])
T5Y = T5Y[::-1].reset_index().set_index("Time").drop(columns=["index"])

Как только это будет сделано, мы приступим к маркировке данных. У нас будет одна метка, которая будет будущей ценой закрытия S&P 500 на 20 шагов вперед. А вторая бинарная цель создается только для целей построения графика.

#Insert the label
SP500["Target SP500"] = SP500["Close"].shift(-look_ahead)
SP500["Binary Target SP500"] = 0
SP500.loc[SP500["Close"] < SP500["Target SP500"],"Binary Target SP500"] = 1
SP500.dropna(inplace=True)

Объединим данные по индексу S&P 500 и доходности пятилетних казначейских облигаций в один объединенный фрейм данных.

#Merge the data
merged_df = pd.merge(SP500,T5Y,how="inner",left_index=True,right_index=True,suffixes=(" SP500"," T5Y"))

Мы можем наблюдать объединение фрейма данных.

#Let's observe the merged dataframe
merged_df

Объединенный фрейм данных

Рис. 1. Объединенный фрейм данных

Мы также можем проанализировать корреляцию в объединенном фрейме данных. Мы можем наблюдать уровни корреляции около 0,1, что не является сильным показателем.

#Merged data frame correlation
merged_df.corr()


Уровни корреляции.

Рис. 2. Уровни корреляции в нашем объединенном фрейме данных

Однако высокие уровни корреляции не обязательно означают, что между двумя рассматриваемыми переменными существует определенная связь. Это также не означает, что одна переменная является причиной другой переменной. Сильные уровни корреляции могут указывать на то, что существует общая причина, влияющая на эти два рынка.

Я построил диаграмму рассеяния, отложив по оси X время, а по оси Y — цену открытия S&P 500. Затем я использовал бинарные цели для раскрашивания точек на диаграмме рассеяния. Обратите внимание, что синие и оранжевые точки естественным образом группируются вместе. Это может указывать на то, что время хорошо разделяет данные. Как мы помним, наша бинарная цель говорит нам о том, что произойдет через 20 шагов в будущем: синие точки означают, что цена упала за следующие 20 шагов, а оранжевые точки говорят нам, что произошло обратное.

#It appears that one variable that separates the data well is time
sns.scatterplot(data=merged_df,x="Candle",y="Open SP500",hue="Binary Target SP500")

Время хорошо разделяет наши данные

Рис. 3: Наши данные, по-видимому, хорошо разделены во времени.

Похоже, что время очень хорошо разделяет данные. Однако когда мы пытаемся использовать другие переменные для разделения данных, как, например, здесь, мы создаем диаграмму рассеяния цены открытия SP500 по сравнению с доходностью пятилетних казначейских облигаций. Мы получаем плохо разделенную диаграмму рассеяния, на которой слишком много точек накладываются друг на друга, и нет никакого четкого разделения.

#It appears that one variable that separates the data well is time
sns.scatterplot(data=merged_df,x="Open T5Y",y="Open SP500",hue="Binary Target SP500")

Плохое разделение

Рис. 4. Плохие уровни разделения


Выбор модели

Перейдем к моделированию взаимосвязи между индексом SP500 и доходностью казначейских облигаций. Импортируем необходимые нам модули из scikit-learn.

#Import the libraries we need
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import SGDRegressor
from sklearn.svm import LinearSVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import BaggingRegressor
from sklearn.ensemble import AdaBoostRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import root_mean_squared_error
from sklearn.preprocessing import RobustScaler
import time
from numpy.random import rand,randn
from scipy.optimize import minimize

Подготовимся к созданию объекта разделения временного ряда. Сначала определим необходимое нам количество разделений, а затем создадим сам объект разделения временного ряда.

#Define the number of splits we want
splits = 10
#Create the time series split object
tscv = TimeSeriesSplit(n_splits = splits, gap=look_ahead)

А поскольку у нас много моделей, мы сохраним их в списке.

#Store the models in a list
models = [LinearRegression(),
         Lasso(),
         SGDRegressor(),
         LinearSVR(),
         RandomForestRegressor(),
         GradientBoostingRegressor(),
         BaggingRegressor(),
         AdaBoostRegressor(),
         MLPRegressor(hidden_layer_sizes=(10,4),early_stopping=True),
         ]

Я определю функцию для инициализации наших моделей, и эта функция будет называться initialize_models.

#Define a function to initialize our models
def initialize_models():
    models = [LinearRegression(),
         Lasso(),
         SGDRegressor(),
         LinearSVR(),
         RandomForestRegressor(),
         GradientBoostingRegressor(),
         BaggingRegressor(),
         AdaBoostRegressor(),
         MLPRegressor(hidden_layer_sizes=(10,4),early_stopping=True),
         ]

Затем нам также понадобятся фреймы данных для хранения наших уровней ошибок. Нам нужно три фрейма данных. Первый фрейм данных будет хранить наши уровни ошибок, когда мы просто используем обычные данные об открытии, максимуме, минимуме, закрытии индекса S&P 500, второй фрейм данных будет хранить наши уровни ошибок, когда мы пытаемся прогнозировать индекс S&P 500, полагаясь только на доходность казначейских облигаций. Последний фрейм данных хранит наши уровни ошибок при использовании всех имеющихся у нас данных.

#Create 3 dataframes to measure our performance
#Before we do that, we will define the columns and idexes
columns = ["Linear Regression",
          "Lasso",
          "SGD Regressor",
          "Linear SVR",
          "Random Forest Regressor",
          "Gradient Boosting Regressor",
          "Bagging Regressor",
          "Ada Boost Regressor",
          "MLP Regressor"]
indexes = np.arange(0,10)


#First dataframe stores our error levels using just the ordinary SP500 OHCL
SP500_error = pd.DataFrame(columns=columns,index=indexes)
#Second dataframe stores our error levels using just the ordinary Treasury Yield OHCL
TY5_error = pd.DataFrame(columns=columns,index=indexes)
#Last dataframe stores our error levels using all the data we have
total_error = pd.DataFrame(columns=columns,index=indexes)

Теперь определим наши входные параметры и цель.

#Now we will define the inputs and target
target = "Target SP500"
predictors = ["Open T5Y",
              "Close T5Y",
              "High T5Y",
              "Low T5Y",
              "Open SP500",
              "Close SP500",
              "High SP500",
              "Low SP500"
             ]

Затем мы сбросим индекс нашего объединенного фрейма данных.

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

Мы собираемся масштабировать данные, используя надежный масштабировщик (robust scaler). Поэтому мы просто создаем экземпляр надежного масштабировщика, вызываем функцию преобразования и передаем фрейм данных слияния в функцию преобразования подгонки (fit transform). Все это помещено в новый объект фрейма данных, который мы создадим с помощью pandas.

#Scale the data
scaled_data = pd.DataFrame(RobustScaler().fit_transform(merged_df.loc[:,predictors]),columns=predictors,index=np.arange(0,merged_df.shape[0]))

Мы готовы провести перекрестную валидацию. Самый простой способ это сделать - использовать вложенный цикл. Таким образом, первый цикл for выполняет итерацию по всем имеющимся у нас моделям, а затем второй цикл выполняет перекрестную проверку каждой модели по отдельности. Поэтому мы подгоним линейную регрессионную модель, затем подгоним лассо и так далее.

#Now we will perform cross validation
#First we iterate over all the models we have
for j in np.arange(0,len(models)):
    for i,(train,test) in enumerate(tscv.split(merged_df)):
        #Prepare the models
        initialize_models()
        #Prepare the data
        X_train = scaled_data.loc[train[0]:train[-1],predictors]
        X_test = scaled_data.loc[test[0]:test[-1],predictors]
        y_train = merged_df.loc[train[0]:train[-1],target]
        y_test = merged_df.loc[test[0]:test[-1],target]
        #Now fit each model and measure its accuracy
        models[j].fit(X_train,y_train)
        SP500_error.iloc[i,j] = root_mean_squared_error(y_test,models[j].predict(X_test))
        print(f"Completed fitting model {models[j]}")
Completed fitting model LinearRegression()
Completed fitting model LinearRegression()
Completed fitting model LinearRegression()
Completed fitting model LinearRegression()
Completed fitting model LinearRegression()

Отсюда мы можем видеть наши уровни ошибок S&P 500, и похоже, что линейная регрессия оказалась одной из наиболее эффективных моделей в этом случае, за ней следует SGD-регрессор. Нейронная сеть показала себя довольно плохо. Возможно, следовало провести настройку параметров.

SP500_error

Уровни ошибок SP500

Рис. 5. Наши уровни ошибок при использовании обычных данных OHLC SP500

Переходим к доходности наших пятилетних казначейских облигаций. В этом случае все наши модели показали плохие результаты. Однако регрессор случайного леса (Random Forest Regressor), похоже, работает довольно хорошо.

TY5_error

Уровни ошибок доходности казначейских облигаций

Рис. 6. Наши уровни ошибок при расчете доходности казначейских облигаций

И наконец, у нас есть общая ошибка при использовании всех доступных данных. Похоже, что регрессор стохастического градиентного спуска работает достаточно хорошо, и поэтому я выбрал SGD-регрессор как наиболее эффективную модель.

total_error

общий уровень ошибок

Рис. 7. Наши уровни ошибок при использовании всех доступных данных


Выбор признаков

Теперь мы выполним отбор признаков, чтобы проверить, считает ли наш компьютер данные о доходности казначейских облигаций важными. Если селектор признаков отбрасывает данные, связанные с доходностью казначейских облигаций, то это повод для беспокойства - возможно, взаимосвязь ненадежна и стратегию придется пересмотреть. Если же наш селектор признаков сохранит эти данные, то это хороший знак.

#Feature selection
from mlxtend.feature_selection import SequentialFeatureSelector as SFS

#Get the best model
model = SGDRegressor()

Мы создаем объект последовательного селектора признаков и передаем ему модель, которую хотим использовать. После этого я даю команду алгоритму выбрать столько признаков, сколько необходимо. Мы могли бы указать, что он должен выбрать пять признаков, но я хотел выбрать столько признаков, сколько он посчитает важными. Мы устанавливаем forward на true. Это означает, что будет выполнен прямой отбор, и оттуда мы передаем CV, равный пяти, то есть мы будем использовать пятикратную перекрестную валидацию. Оттуда мы передали n-jobs, равный минус 1, что позволяет селектору функций выполнять эту задачу параллельно.

#Let us perform feature selection for the best model we have
sfs_sgd_regressor = SFS(model,
                            (1,8),
                            forward=True,
                            cv=5,
                            n_jobs=-1,
                            scoring="neg_mean_squared_error"
                           )

Далее мы подбираем селектор функций.

#Fit the feature selector
sfs_1 = sfs_sgd_regressor.fit(scaled_data.loc[:,predictors],merged_df.loc[:,target])

Если теперь посмотреть, какие характеристики были наиболее важны для нашей модели, то, к сожалению, ни одна из них не связана с казначейскими облигациями. Доходность выбиралась только по выбранным максимумам и минимумам закрытия индекса S&P 500. Это может указывать на то, что связь не столь стабильна. Хорошо известно, что корреляция между доходностью казначейских облигаций и индексом S&P 500 время от времени нарушается.

#Which features were most important to our model?
sfs_1.k_feature_names_
('Close SP500', 'High SP500', 'Low SP500')

Мы по-прежнему будем пытаться оптимизировать нашу модель и посмотреть, какой производительности мы можем добиться.

#None the less, let us attempt to optimize the model
from scipy import optimize

Создадим два специальных набора данных. Один для обучения и оптимизации модели, а другой - для валидации. На проверочном наборе мы сравним производительность нашей оптимизированной модели с производительностью модели по умолчанию, которая использует только настройки по умолчанию. Нам нужно превзойти уровни ошибок по умолчанию.

#Create a training and validation set
scaled_data = merged_df.loc[:,predictors]
scaled_data = (scaled_data - scaled_data.mean()) / (scaled_data.std())
#Create the two datasets
train_data , test_data = scaled_data.loc[:(scaled_data.shape[0]//2),:],scaled_data.loc[(scaled_data.shape[0]//2):,:]

В первый раз я просто использовал robust scaler. На этот раз я использую другой метод масштабирования. На этот раз мы применили очень распространенный метод масштабирования, при котором мы вычитаем среднее значение из каждого столбца, а затем делим каждый столбец на его стандартное отклонение.

#Let's write out the column mean and standard deviations
#We'll store the mean first 
#Then the standard deviation
scale_factors = pd.DataFrame(columns=predictors,index=(0,1))
#Save the mean and std value of each respective column
for i in (np.arange(0,len(predictors))):
    #Calculate and store the values of each column mean and std
    scale_factors.iloc[0,i] = merged_df.loc[:,predictors[i]].mean()
    scale_factors.iloc[1,i] = merged_df.loc[:,predictors[i]].std()

#Inspect the data
scale_factors

Факторы масштабирования

Рис. 8: Наше среднее значение и стандартное отклонение для каждого столбца

Средние значения и стандартные отклонения, которые мы рассчитали для каждого столбца, значительны, и эти данные нам понадобятся, когда мы снова начнем работать в MQL5, поэтому я записываю данные в формат CSV.

#Write it out to csv format
scale_factors.to_csv("/home/volatily/.wine/drive_c/Program Files/MetaTrader 5/MQL5/Files/sp500_treasury_yields_scale.csv")


Настройка модели SGD-регрессора

Теперь попытаемся настроить модель. Сначала определим целевую функцию. Целевой функцией в этом случае будут уровни среднеквадратической ошибки (root-mean-square error, RMSE). Мы хотим минимизировать наши уровни RMSE на обучающих данных. Однако эта процедура — палка о двух концах. Какие бы гиперпараметры ни минимизировали нашу ошибку на обучающем наборе, это не гарантирует минимизации нашей ошибки на проверочном наборе!

#Define the objective function 
def objective(x):
    #Initialize the model with the new parameters
    model = SGDRegressor(alpha=x[0],shuffle=False,eta0=x[1])
    #We need a dataframe to store our current model accuracy levels
    current_accuracy = pd.DataFrame(index=np.arange(0,splits),columns=["Error"])
    #Now we perform cross validation
    for i,(train,test) in enumerate(tscv.split(train_data)):
        #Split the data into a training set and test set
        X_train = train_data.loc[train[0]:train[-1],predictors]
        X_test  = train_data.loc[test[0]:test[-1],predictors]
        y_train = merged_df.loc[train[0]:train[-1],target]
        y_test  = merged_df.loc[test[0]:test[-1],target]
        #Fit the model
        model.fit(X_train,y_train)
        #Record the accuracy
        current_accuracy.iloc[i,0] = root_mean_squared_error(y_test,model.predict(X_test))
    #Return the model accuracrcy
    return(current_accuracy.iloc[:,0].mean())

Как обычно, мы начнем с выполнения линейного поиска, чтобы понять, где могут находиться оптимальные значения. Мы начали с обычного линейного поиска, и на его завершение у нас ушла 41 секунда.

#Let's optimize our model
#Let us measure how much time this takes.
start = time.time()

#Create a dataframe to measure the error rates
starting_point_error = pd.DataFrame(index=np.arange(0,21),columns=["Average CV RMSE"])
starting_point_error["Iteration"] = np.arange(0,21)

#Let us first find a good starting point for our optimization algorithm
for i in np.arange(0,21):
    #Set a new starting point
    new_starting_point = (10.0 ** -i)
    #Store error rates
    starting_point_error.iloc[i,0] = objective([new_starting_point  ,new_starting_point]) 

#Record the time stamp at the end
stop = time.time()

#Report the amount of time taken
print(f"Completed in {stop - start} seconds")
Completed in 41.863527059555054 seconds

Из результатов нашего линейного поиска следует, что мы пересекли оптимальные точки уже в первой итерации.

starting_point_error["alpha"] = 0
starting_point_error["eta0"] = 0

for i in np.arange(0,21):
    starting_point_error.loc[i,"alpha"] = (10.0 ** -i)
    starting_point_error.loc[i,"eta0"] = (10.0 ** -i)

starting_point_error

Результаты линейного поиска

Рис. 9. Результаты нашего линейного поиска

Мы также можем изобразить эту информацию визуально. Как видим, форма напоминает перевернутую хоккейную клюшку с самой низкой ошибкой в самом начале, а затем наша ошибка просто продолжает увеличиваться,

#Let's visualize our error levels
sns.lineplot(data=starting_point_error,x="Iteration",y="Average CV RMSE").set(title="Optimizing our SGD Regressor on Training Data")

Рис. 10. Построение графика наших уровней ошибок

Рис. 10. Визуализация наших уровней ошибок

Итак, теперь, когда у нас есть представление о том, что кажется оптимальным, мы можем выполнить локальный поиск в регионе, который представляется оптимальным. Мы будем использовать алгоритм L-BGFS-B для поиска оптимальных точек. Сначала мы выберем случайные точки из области, которая представляется оптимальной.

#Now let us perform a local search in the space that appears optimal
pt = abs(((10 ** -2) + rand(2) * ((1) - (10 ** -2))))
pt

array([0.94169659, 0.33068772])

Теперь попробуем оптимизировать нашу модель под обучающие данные.

#Let's try optimize our model
start = time.time()
bounds = ((0.01,1),(0.01,1))
result = minimize(objective,pt,bounds=bounds,method="L-BFGS-B")
stop = time.time()
print(f"Task completed in {stop - start} seconds")
Task completed in 106.46932244300842 seconds

Каковы результаты?

#What are the results?
result
message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH
  success: True
   status: 0
      fun: 11.428966326221078
        x: [ 1.040e-01  3.193e-01]
      nit: 24
      jac: [ 9.160e+00 -1.475e+01]
     nfev: 351
     njev: 117
 hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>

Похоже, у нас получилось: наименьшая ошибка, которую нам удалось получить, составила 11,43, однако настоящей проверкой будет сравнение настроенной модели с моделью по умолчанию в тестовом наборе.


Проверка на переобучение

Чтобы определить, не переобучаем ли мы обучающие данные, давайте сравним уровни ошибок нашей настроенной модели с уровнями ошибок модели, использующей настройки по умолчанию. Как вы помните, перед настройкой параметров мы разделили набор данных на две половины.
#Now let us compare the default model and the customized model
default_model = SGDRegressor()
customized_model = SGDRegressor(alpha=result.x[0],shuffle=False,eta0=result.x[1])

Сначала давайте оценим уровни ошибок модели по умолчанию и тестового набора.

#Default model accuracy
default_model.fit(train_data.loc[:,predictors],merged_df.loc[:(merged_df.shape[0]//2),target])
root_mean_squared_error(merged_df.loc[(merged_df.shape[0]//2):,target],default_model.predict(test_data.loc[:,predictors]))
5.793428451043455

Теперь давайте сравним эти данные с уровнями ошибок настроенной модели.

#Customized model accuracy
customized_model.fit(train_data.loc[:,predictors],merged_df.loc[:(merged_df.shape[0]//2),target])
root_mean_squared_error(merged_df.loc[(merged_df.shape[0]//2):,target],customized_model.predict(test_data.loc[:,predictors]))
63.45882351828459

Похоже, что мы действительно переобучились на обучающих данных и не смогли превзойти настройки по умолчанию. В этом случае мы продолжим работу с моделью по умолчанию и экспортируем ее в формат ONNX.


Экспорт в формат ONNX

Начнем с импорта необходимых нам библиотек.

#Let's convert the regression model to ONNX format
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import convert_sklearn
import onnxruntime as ort
import onnx

Затем мы нормализуем и масштабируем наши входные данные.

for i in predictors:
    merged_df.loc[:,i] = (merged_df.loc[:,i] - merged_df.loc[:,i].mean()) / merged_df.loc[:,i].std()

Теперь обучим модель на всем наборе данных.

#Prepare the model
model = SGDRegressor()
model.fit(merged_df.loc[:,predictors],merged_df.loc[:,"Target SP500"])

Теперь определим форму и типы входных данных.

#Define the input types
initial_type_float = [("float_input",FloatTensorType([1,len(predictors)]))]
onnx_model_float = convert_sklearn(model,initial_types=initial_type_float,target_opset=12)

Давайте сохраним модель ONNX.

#ONNX file name
onnx_file_name = "SP500_ONNX_FLOAT_M1.onnx"
#ONNX file
onnx.save_model(onnx_model_float,onnx_file_name)

Теперь давайте быстро рассмотрим форму входных и выходных данных нашей модели ONNX.

# load the ONNX model and inspect input and ouput shapes
onnx_session = ort.InferenceSession(onnx_file_name)
input_name = onnx_session.get_inputs()[0].name
output_name = onnx_session.get_outputs()[0].name

Давайте убедимся, что форма входного сигнала нашей модели - 1 на 8.

#Display information about input tensors in ONNX
print("Information about input tensors in ONNX:")
for i, input_tensor in enumerate(onnx_session.get_inputs()):
    print(f"{i + 1}. Name: {input_tensor.name}, Data Type: {input_tensor.type}, Shape: {input_tensor.shape}")
Information about input tensors in ONNX:
1. Name: float_input, Data Type: tensor(float), Shape: [1, 8]

Наконец, форма нашего выходного сигнала должна быть 1 на 1.

#Display information about output tensors in ONNX
print("Information about output tensors in ONNX:")
for i, output_tensor in enumerate(onnx_session.get_outputs()):
    print(f"{i + 1}. Name: {output_tensor.name}, Data Type: {output_tensor.type}, Shape: {output_tensor.shape}")
Information about output tensors in ONNX:
1. Name: variable, Data Type: tensor(float), Shape: [1, 1]

Мы также можем визуализировать нашу модель ONNX с помощью Netron.

#Visualize the model
import netron

Функция start в Netron позволяет нам визуализировать нашу модель ONNX.

#Call netron 
netron.start(onnx_file_name)

Визуализация нашей модели ONNX с использованием Netron

Рис. 11: Визуализация нашей модели ONNX с использованием Netron


Метаданные нашей модели ONNX

Рис. 12. Свойства нашей модели ONNX


Реализация средствами MQL5

Теперь, когда мы закончили создание модели ONNX и экспортировали ее, мы можем приступить к созданию нашего советника. Первое, что мы сделаем в нашем советнике, — загрузим модель ONNX, которую мы только что экспортировали.

//+------------------------------------------------------------------+
//|                                      SP500 X Treasury Yields.mq5 |
//|                                        Gamuchirai Zororo Ndawana |
//|                          https://www.mql5.com/en/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/gamuchiraindawa"
#property version   "1.00"
#property tester_file "sp500_treasury_yields_scale.csv"

//+------------------------------------------------------------------+
//| Require the ONNX model                                           |
//+------------------------------------------------------------------+
#resource "\\Files\\SP500_ONNX_FLOAT_M1.onnx" as const uchar ModelBuffer[];

Мы также собираемся включить торговую библиотеку, которая поможет нам открывать, закрывать и изменять наши позиции.

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

Также необходимо принять во внимание некоторые данные от конечного пользователя, например, насколько большим должен быть кратный лот и насколько большим должен быть наш стоп-лосс.

//+------------------------------------------------------------------+
//| Inputs for our EA                                                |
//+------------------------------------------------------------------+
input int lot_multiple = 1; //How many times bigger than minimum lot?
input double sl_width = 1;  //How wide should our stop loss be?

Нам нужны глобальные переменные, которые будут использоваться во всем советнике. Нам нужна одна глобальная переменная для представления модели ONNX и еще один вектор для хранения прогнозов нашей модели.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long model;                              //Our ONNX SGDRegressor model
vectorf prediction(1);                   //Our model's prediction
float mean_values[8],variance_values[8]; //We need this data to normalise and scale model inputs
double trading_volume;                   //How big should our positions be?
int state = 0;

Также необходима функция, отвечающая за чтение CSV-файла, который мы определили ранее. Файл необходим, поскольку он содержит средние значения и значения стандартного отклонения каждого столбца. Эта функция гарантирует, что все входные данные, которые мы передаем в нашу модель ONNX, нормализованы. Функция начнет работу с попытки открыть файл с помощью команды открытия файла. Если нам удалось открыть файл, мы приступаем к его анализу и сохраняем средние значения и значения дисперсии в их собственных отдельных массивах. В противном случае, функция выведет сообщение о том, что ей не удалось прочитать файл, и вернет false, а процедура инициализации завершится неудачей.

//+------------------------------------------------------------------+
//| A function responsible for reading the CSV config file           |
//+------------------------------------------------------------------+
bool read_configuration_file(void)
  {
//--- Read the config file
   Print("Reading in the config file");

//--- Config file name
   string file_name = "sp500_treasury_yields_scale.csv";

//--- Try open the file
   int result = FileOpen(file_name,FILE_READ|FILE_CSV|FILE_ANSI,",");

//--- Check the result
   if(result != INVALID_HANDLE)
     {
      Print("Opened the file");
      //--- Prepare to read the file
      int counter = 0;
      string value = "";
      //--- Make sure we can proceed
      while(!FileIsEnding(result) && !IsStopped())
        {
         if(counter > 60)
            break;
         //--- Read in the file
         value = FileReadString(result);
         Print("Reading: ",value);
         //--- Have we reached the end of the line?
         if(FileIsLineEnding(result))
            Print("row++");
         counter++;
         //--- The first few lines will contain the title of each columns, we will ingore that
         if((counter >= 11) && (counter <= 18))
           {
            mean_values[counter - 11] = (float) value;
           }
         if((counter >= 20) && (counter <= 27))
           {
            variance_values[counter - 20] = (float) value;
           }
        }
      //--- Close the file
      FileClose(result);
      Print("Mean values");
      ArrayPrint(mean_values);
      Print("Variance values");
      ArrayPrint(variance_values);
      return(true);
     }

   else
      if(result == INVALID_HANDLE)
        {
         Print("Failed to read the file");
         return(false);
        }

   return(false);
  }

Нам также нужна функция, отвечающая за получение прогноза из нашей модели. В начале у нас есть вектор для хранения входных данных. После того, как мы извлекли все необходимые нам цены, мы вычитаем среднее значение для этого столбца и делим на дисперсию для этого конкретного столбца. Как только это будет сделано, мы сможем получить прогноз с помощью нашей модели.

//+------------------------------------------------------------------+
//| A function responsible for getting a forecast from our model     |
//+------------------------------------------------------------------+
void predict(void)
  {
//--- Let's prepare our inputs
   vectorf input_data = vectorf::Zeros(8);
//--- Select the symbol
   input_data[0] = ((iOpen("UST05Y_U4",PERIOD_M1,0) - mean_values[0]) / variance_values[0]);
   input_data[1] = ((iClose("UST05Y_U4",PERIOD_M1,0) - mean_values[1]) / variance_values[1]);
   input_data[2] = ((iHigh("UST05Y_U4",PERIOD_M1,0) - mean_values[2]) / variance_values[2]);
   input_data[3] = ((iLow("UST05Y_U4",PERIOD_M1,0) - mean_values[3]) / variance_values[3]);;
   input_data[4] = ((iOpen("US500",PERIOD_M1,0) - mean_values[4]) / variance_values[4]);;
   input_data[5] = ((iClose("US500",PERIOD_M1,0) - mean_values[5]) / variance_values[5]);;
   input_data[6] = ((iHigh("US500",PERIOD_M1,0) - mean_values[6]) / variance_values[6]);
   input_data[7] = ((iLow("US500",PERIOD_M1,0) - mean_values[7]) / variance_values[7]);;
//--- Show the inputs
   Print("Inputs: ",input_data);
//--- Obtain a prediction from our model
   OnnxRun(model,ONNX_DEFAULT,input_data,prediction);
  }

После того, как наша модель дала нам прогноз, нам нужно действовать. В данном конкретном случае мы можем либо принять решение об открытии позиции в направлении, которое предсказала наша модель, или, если наша модель прогнозирует, что цена развернется против нас, мы можем принять решение закрыть наши открытые позиции.

//+------------------------------------------------------------------+
//| This function will decide if we should open or close our trades  |
//+------------------------------------------------------------------+
void intepret_prediction(void)
  {
   if(PositionsTotal() == 0)
     {
      double ask = SymbolInfoDouble("US500",SYMBOL_ASK);
      double bid = SymbolInfoDouble("US500",SYMBOL_BID);
      double close = iClose("US500",PERIOD_M1,0);
      if(prediction[0] > close)
        {
         Trade.Buy(trading_volume,"US500",ask,(ask - sl_width),(ask + sl_width),"SP500 X Treasury Yields");
         state = 1;
        }

      if(prediction[0] < iClose("US500",PERIOD_M1,0))
        {
         Trade.Sell(trading_volume,"US500",bid,(bid + sl_width),(bid - sl_width),"SP500 X Treasury Yields");
         state = 2;
        }
     }
   else
      if(PositionsTotal() > 0)
        {
         if((state == 1) && (prediction[0] > iClose("US500",PERIOD_M1,0)))
           {
            Alert("Reversal predicted, consider closing your buy position");
           }

         if((state == 2) && (prediction[0] < iClose("US500",PERIOD_M1,0)))
           {
            Alert("Reversal predicted, consider closing your buy position");
           }
        }

  }

Мы закончили определение вспомогательных функций для нашей модели и переходим к определению функции инициализации нашего советника. Сначала необходимо создать модель ONNX, а затем убедиться, что она рабочая.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- Create the ONNX model from the model buffer we have
   model = OnnxCreateFromBuffer(ModelBuffer,ONNX_DEFAULT);

//--- Ensure the model is valid
   if(model == INVALID_HANDLE)
     {
      Comment("[ERROR] Failed to initialize the model: ",GetLastError());
      return(INIT_FAILED);
     }

Убедившись, что модель верна, мы определяем формы входного и выходного сигналов нашей модели.

//--- Define the model parameters, input and output shapes
   ulong input_shape[] = {1,8};

//--- Check if we were defined the right input shape
   if(!OnnxSetInputShape(model,0,input_shape))
     {
      Comment("[ERROR] Incorrect input shape specified: ",GetLastError(),"\nThe model's inputs are: ",OnnxGetInputCount(model));
      return(INIT_FAILED);
     }

   ulong output_shape[] = {1,1};

//--- Check if we were defined the right output shape
   if(!OnnxSetOutputShape(model,0,output_shape))
     {
      Comment("[ERROR] Incorrect output shape specified: ",GetLastError(),"\nThe model's outputs are: ",OnnxGetOutputCount(model));
      return(INIT_FAILED);
     }

После того, как все это будет сделано, мы можем прочитать файл конфигурации. Это необходимо сделать при инициализации, и если нам не удастся прочитать файл конфигурации, советник должен будет завершить работу, поскольку мы не можем делать прогнозы на основе ненормализованных данных.

//--- Read the configuration file
   if(!read_configuration_file())
     {
      Comment("Failed to find the configuration file, ensure it is stored here: ",TerminalInfoString(TERMINAL_DATA_PATH));
      return(INIT_FAILED);
     }

Теперь нам нужно выбрать символы и добавить их в "Обзор рынка".

//--- Select the symbols
   SymbolSelect("US500",true);
   SymbolSelect("UST05Y_U4",true);

Наконец, нам нужно получить некоторые рыночные данные.

//--- Calculate the lotsize
   trading_volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;

//--- Return init succeeded
   return(INIT_SUCCEEDED);
  }
Всякий раз, когда наш советник не используется, мы должны освободить выделенные нам ресурсы.
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Free up the resources we used for our ONNX model
   OnnxRelease(model);
//--- Remove the expert advisor
   ExpertRemove();
  }

Наконец, в нашем обработчике событий OnTick мы будем делать прогнозы, используя нашу модель ONNX, а затем преобразовывать эти прогнозы в действия.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Get a prediction
   predict();
//--- Interpret the forecast
   intepret_prediction();
   Comment("Model forecast",prediction[0]);
  }

Наш советник в действии

Рис. 13. Наш советник в действии


Заключение

В этой статье мы пересмотрели классическую стратегию торговли SP500, основанную на доходности казначейских облигаций. Наш анализ показал, что эта взаимосвязь не всегда стабильна. Более того, инвесторам, по-видимому, может быть выгоднее использовать обычные рыночные данные из самого индекса SP500.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15531

Прикрепленные файлы |
FetchData312.mq5 (2.05 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Maxim Kuznetsov
Maxim Kuznetsov | 17 февр. 2025 в 11:08

последний (самый нижний) скриншот, он вообще имеет отношение к статье и означенным стратегиям ?

там-же таймфрейм M1 и цели в несколько пунктов :-)

Фибоначчи на Форекс (Часть I): Проверяем отношения цены и времени Фибоначчи на Форекс (Часть I): Проверяем отношения цены и времени
Как рынок ходит по отношениям, основанным на числах Фибоначчи? Эта последовательность, где каждое следующее число равно сумме двух предыдущих (1, 1, 2, 3, 5, 8, 13, 21...), не только описывает рост популяции кроликов. Рассмотрим гипотезу Пифагора о том, что все в мире подчиняется определенным соотношениям чисел...
Нейросети в трейдинге: Двухмерные модели пространства связей (Chimera) Нейросети в трейдинге: Двухмерные модели пространства связей (Chimera)
Откройте для себя инновационный фреймворк Chimera — двухмерную модель пространства состояний, использующую нейросети для анализа многомерных временных рядов. Этот метод предлагает высокую точность с низкими вычислительными затратами, превосходя традиционные подходы и архитектуры Transformer.
От начального до среднего уровня: Массивы и строки (II) От начального до среднего уровня: Массивы и строки (II)
В этой статье я покажу, что хотя мы всё еще находимся на очень базовой стадии программирования, мы уже можем реализовать несколько интересных приложений. В данном случае мы создадим довольно простой генератор паролей. Таким образом мы сможем применить некоторые концепции, которые объяснялись до этого. Кроме того, мы рассмотрим, как можно разработать решения для некоторых конкретных проблем.
Разработка системы репликации (Часть 67): Совершенствуем индикатор управления Разработка системы репликации (Часть 67): Совершенствуем индикатор управления
В данной статье мы рассмотрим, чего можно добиться с помощью небольшой доработки кода. Данная доработка направлена на упрощение нашего кода, более активное использование вызовов библиотеки MQL5 и, прежде всего, на то, чтобы сделать его гораздо более стабильным, безопасным и простым для использования в другом коде, который мы будем разрабатывать в будущем.