
Переосмысливаем классические стратегии (Часть IV): SP500 и казначейские облигации США
Введение
В предыдущей статье мы обсудили торговую стратегию 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()
Отсюда мы можем видеть наши уровни ошибок S&P 500, и похоже, что линейная регрессия оказалась одной из наиболее эффективных моделей в этом случае, за ней следует SGD-регрессор. Нейронная сеть показала себя довольно плохо. Возможно, следовало провести настройку параметров.
SP500_error
Рис. 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_
Мы по-прежнему будем пытаться оптимизировать нашу модель и посмотреть, какой производительности мы можем добиться.
#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")
Из результатов нашего линейного поиска следует, что мы пересекли оптимальные точки уже в первой итерации.
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. Визуализация наших уровней ошибок
Итак, теперь, когда у нас есть представление о том, что кажется оптимальным, мы можем выполнить локальный поиск в регионе, который представляется оптимальным. Мы будем использовать алгоритм 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
Теперь попробуем оптимизировать нашу модель под обучающие данные.
#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")
Каковы результаты?
#What are the results?
result
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]))
Теперь давайте сравним эти данные с уровнями ошибок настроенной модели.
#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]))
Похоже, что мы действительно переобучились на обучающих данных и не смогли превзойти настройки по умолчанию. В этом случае мы продолжим работу с моделью по умолчанию и экспортируем ее в формат 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}")
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}")
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)
Рис. 11: Визуализация нашей модели ONNX с использованием Netron
Рис. 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
последний (самый нижний) скриншот, он вообще имеет отношение к статье и означенным стратегиям ?
там-же таймфрейм M1 и цели в несколько пунктов :-)