English 中文
preview
Переосмысливаем классические стратегии (Часть 13): Обновление стратегии по пересечению скользящих (Часть 2)

Переосмысливаем классические стратегии (Часть 13): Обновление стратегии по пересечению скользящих (Часть 2)

MetaTrader 5Примеры |
90 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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

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

Поиск торговых сигналов на фоне рыночного шума может быть сложной задачей. Существует ряд принципов работы с данными, которые могут помочь в этом. В частности, обратимся к презентации команды Лаборатории реактивного движения NASA (NASA Jet Propulsion Laboratory) при Калифорнийском технологическом институте (Caltech), которая содержит ряд полезных идей. Ссылка на презентацию доступна здесь. Эта презентация была посвящена анализу больших данных. В ней содержится ключевой принцип, который относится к нашей теме. Если вам стало интересно, рекомендую самостоятельно ознакомиться с этой презентацией. Вкратце, принцип заключается в том, что некоторые сложные задачи в области анализа данных могут становиться проще при проецировании данных в пространства большей размерности. На рисунке 1 ниже приведен фрагмент оригинальной презентации, о котором я говорю.

Рисунок 1: Слайд является частью общедоступной презентации "Big Data Analytics", представленной командой NASA JPL в Калифорнийском технологическом институте (Caltech), сентябрь 2014 года.

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

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

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


Начало работы в MQL5

Для начала мы создадим скрипт, чтобы получать необходимые данные из терминала MetaTrader 5. Прежде всего зафиксируем нужные константы. Например, период для всех скользящих средних будет установлен на фиксированное значение, и во всей статье мы будем использовать простые скользящие средние (Simple Moving Average).

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- Define our moving average indicator
#define MA_PERIOD 2                 //--- Moving Average Period
#define MA_TYPE   MODE_SMA          //--- Type of moving average we have
#define HORIZON   5                 //--- Forecast horizon

 Кроме того, определим глобальные переменные, обработчики и буферы для скользящих средних.

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[];

//--- File name
string file_name = Symbol() + " Market Data As Series Moving Average.csv";

//--- Amount of data requested
input int size = 3000;

Далее переходим к определению основной логики нашего скрипта. При выполнении скрипта инициализируем обработчики скользящих средних, после чего скопируем значения из этих обработчиков в соответствующие буферы. При подготовке записи данных в файл помним, что нам придется заполнить большое количество столбцов. Первые восемь столбцов стандартные: цены open, high, low, close, а также соответствующие им значения скользящих средних. Также добавим столбцы для обозначения роста внутри каждого ценового диапазона.

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

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
  int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
   ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW);

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading);
   ArraySetAsSeries(ma_o_reading,true);
   CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading);
   ArraySetAsSeries(ma_h_reading,true);
   CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading);
   ArraySetAsSeries(ma_l_reading,true);

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time",
                              //--- OHLC
                              "True Open",
                              "True High",
                              "True Low",
                              "True Close",
                              //--- MA OHLC
                              "True MA C",
                              "True MA O",
                              "True MA H",
                              "True MA L",
                              //--- Growth in OHLC
                              "Diff Open",
                              "Diff High",
                              "Diff Low",
                              "Diff Close",
                              //--- Growth in MA OHLC
                              "Diff MA Close 2",
                              "Diff MA Open 2",
                              "Diff MA High 2",
                              "Diff MA Low 2",
                              //--- Grwoth between channels
                              "O - C",
                              "Delta O - C",
                              "O - L",
                              "Delta O - L",
                              "O - H",
                              "Delta O - H",
                              "H - L",
                              "Delta H - L",
                              "C - H",
                              "Delta C - H",
                              "C - L",
                              "Delta C - L",
                              //--- Grwoth between MA channels
                              "MA O - C",
                              "MA Delta O - C",
                              "MA O - L",
                              "MA Delta O - L",
                              "MA O - H",
                              "MA Delta O - H",
                              "MA H - L",
                              "MA Delta H - L",
                              "MA C - H",
                              "MA Delta C - H",
                              "MA C - L",
                              "MA Delta C - L"
                  );
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i), 
                   //--- OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   //--- MA OHLC
                   ma_reading[i],
                   ma_o_reading[i],
                   ma_h_reading[i],
                   ma_l_reading[i],
                   //--- Growth in OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i)   - iOpen(_Symbol,PERIOD_CURRENT,(i + HORIZON)), 
                   iHigh(_Symbol,PERIOD_CURRENT,i)   - iHigh(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iLow(_Symbol,PERIOD_CURRENT,i)    - iLow(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iClose(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   //--- Growth in MA OHLC
                   ma_reading[i] - ma_reading[(i + HORIZON)],
                   ma_o_reading[i] - ma_o_reading[(i + HORIZON)],
                   ma_h_reading[i] - ma_h_reading[(i + HORIZON)],
                   ma_l_reading[i] - ma_l_reading[(i + HORIZON)],
                   //--- Growth between channels
                   iOpen(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON) - iClose(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iOpen(_Symbol,PERIOD_CURRENT,i) - iLow(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iOpen(_Symbol,PERIOD_CURRENT,i) - iHigh(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON) - iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iHigh(_Symbol,PERIOD_CURRENT,i) - iLow(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iClose(_Symbol,PERIOD_CURRENT,i)  -  iHigh(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i + HORIZON)  -  iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iClose(_Symbol,PERIOD_CURRENT,i)  -  iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i + HORIZON)  -  iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   //--- Growth between moving average channels
                   ma_o_reading[i] - ma_reading[i],
                   ma_o_reading[(i + HORIZON)] -  ma_reading[(i + HORIZON)],
                   ma_o_reading[i] -  ma_l_reading[i],
                   ma_o_reading[(i + HORIZON)] - ma_l_reading[(i + HORIZON)],
                   ma_o_reading[i] -  ma_h_reading[i],
                   ma_o_reading[(i + HORIZON)] - ma_h_reading[(i + HORIZON)],
                   ma_h_reading[i] - ma_l_reading[i],
                   ma_h_reading[(i + HORIZON)] - ma_l_reading[(i + HORIZON)],
                   ma_reading[i] -  ma_h_reading[i],
                   ma_reading[(i + HORIZON)] - ma_h_reading[(i + HORIZON)],
                   ma_reading[i] -  ma_l_reading[i],
                   ma_reading[(i + HORIZON)] - ma_l_reading[(i + HORIZON)]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+

Анализ данных в Python

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

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

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

data = pd.read_csv("C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075\\MQL5\\Files\\EURUSD Market Data As Series Moving Average.csv")
data

Рисунок 2: Визуализация данных, полученных из MetaTrader 5.

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

HORIZON             = 10

#Classical Target
data['Target']      =  0

#High Low Mid Point Target
data['Target 2']    = 0

#Open Close Mid Point Target
data['Target 3']    = 0

data.loc[data['True Close'].shift(-HORIZON) > data['True Close'],'Target'] = 1

#The Mid Point Between The High And The Low Moving Average
data.loc[((data['True MA H'].shift(-HORIZON) + data['True MA L'].shift(-HORIZON)) / 2) > ((data['True MA H'] + data['True MA L']) / 2),'Target 2'] = 1

#The Open And Close Mid Point
data.loc[((data['True MA O'].shift(-HORIZON) + data['True MA C'].shift(-HORIZON)) / 2) > ((data['True MA O'] + data['True MA C']) / 2),'Target 3'] = 1

data = data.iloc[:-HORIZON,:]

Прежде чем двигаться дальше, я хотел бы привести краткую демонстрацию, иллюстрирующую ценность проецирования наборов данных в пространства большей размерности. Поэтому если вы не знакомы с данным принципом, такая демонстрация может быть вполне убедительной. Сначала мы создадим копию исходного набора данных, содержащую только стандартные четыре столбца: цены open, high, low и close. И пересчитаем нашу целевую переменную на этом сокращенном наборе данных.

#Copy the dataset
X = data.iloc[:,:5].copy()
X['Target'] = data['True Close'].shift(-HORIZON) - data['True Close']
X.dropna(inplace=True)

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

def fill_zeros(f_data,f_n):
    #Copy the original data
    res = f_data.copy()

    #We want to keep the target at the end
    t = 'Target'
    v = res.pop('Target')

    #Add columns of zeros
    for i in np.arange(f_n):
        name = str(i) + ' Col'
        res[name] = 0

    #Place the target back
    res[t]= v

    #Return the new dataframe
    return(res)

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

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

#Load our libraries
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import cross_val_score,TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)
EPOCHS = 50

#Observe what happens to our error levels as we increase the number of columns in the dataset
res = []

for i in np.arange(EPOCHS):
    #Fetch new data with addtional columns of zeros
    new_data = fill_zeros(X,(1+i))

    #Record the new error
    res.append(np.mean(np.abs(cross_val_score(MLPRegressor(hidden_layer_sizes=(new_data.iloc[:,1:-1].shape[1],2,50,100),random_state=0,shuffle=False),X.iloc[:,1:-1],X.iloc[:,-1],cv=tscv,n_jobs=-1))))

plt.plot(res,color='black')
plt.grid()
plt.ylabel('RMSE Error')
plt.xlabel('Additional Columns of Zero')
plt.title('Our Error Levels Can Fall Without Any New Information')

Рисунок 3: Уровень ошибок модели снижается, даже несмотря на то, что мы не предоставляем никакой дополнительной информации.

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

Далее перейдем к определению входных и выходных столбцов. Сначала соберем все входные столбцы и сохраняем их в переменной с именем X.

X = data.iloc[:,1:-4].columns

Перечислим целевые переменные. 

y2 = data['Target 2']

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

return(RandomForestClassifier(random_state=0,n_estimators=500,max_depth=3,min_samples_leaf=20))

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

data = data.iloc[:-(365*2),:]

Z = pd.DataFrame(columns=['Z1','Z2'])

Z['Z1'] = data.loc[:,X].mean()
Z['Z2'] = data.loc[:,X].std()

data.loc[:,X] = (data.loc[:,X] - data.loc[:,X].mean()) / data.loc[:,X].std()

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

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from sklearn.ensemble import GradientBoostingRegressor

model = GradientBoostingRegressor(random_state=0,max_depth=3)
model.fit(data.loc[:,X],data.loc[:,'Target 2'])

initial_types = [("FLOAT INPUT",FloatTensorType([1,len(X)]))]

model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)

onnx.save(model_proto,"EURUSD GBR PRICE D1.onnx")


Соберем все вместе 

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

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
//--- Define our moving average indicator
#define MA_PERIOD 2                             //Moving Average Period
#define MA_TYPE   MODE_SMA                      //Type of moving average we have
#define HORIZON   10                            //Forecast horizon
#define TF PERIOD_D1

Далее объявим важные глобальные переменные. Например, значения Z1 и Z2, используемые для стандартизации и масштабирования нашего набора данных, нужно хранить в советнике. Также нужны глобальные переменные для обработчиков скользящих средних и соответствующих буферов.

//+------------------------------------------------------------------+
//| Global definitions                                               |
//+------------------------------------------------------------------+
float Z1[] =
  {
   1.23933432e+00,  1.24403263e+00,  1.23474846e+00,  1.23936216e+00,
   1.23935910e+00,  1.23933128e+00,  1.24402971e+00,  1.23474522e+00,
   3.83991053e-05,  3.60920275e-05,  3.66240614e-05,  3.55759706e-05,
   3.68749001e-05,  3.98194600e-05,  3.78958300e-05,  3.79070139e-05,
   -2.78415082e-05, -3.06646429e-05,  4.58586036e-03,  4.58408532e-03,
   -4.69831123e-03, -4.70061831e-03,  9.28417159e-03,  9.28470363e-03,
   -4.67046972e-03, -4.66995367e-03,  4.61370187e-03,  4.61474996e-03,
   -2.78151462e-05, -3.07597060e-05,  4.58606247e-03,  4.58415002e-03,
   -4.69842067e-03, -4.70034430e-03,  9.28448314e-03,  9.28449433e-03,
   -4.67060553e-03, -4.66958460e-03,  4.61387762e-03,  4.61490973e-03
  };

float Z2[]=
  {
   0.12576155, 0.12640182, 0.125071, 0.12572605, 0.12568469,
   0.125719, 0.12636385, 0.12503521, 0.0150256, 0.01494947,
   0.01478075, 0.01493629, 0.0141562, 0.01423137, 0.01419596,
   0.01404453, 0.00669432, 0.0066951, 0.00482275, 0.004823,
   0.00493041, 0.00493002, 0.0063063, 0.00630607, 0.0048614,
   0.0048616, 0.00471017, 0.0047104, 0.00471147, 0.00471252,
   0.00361188, 0.00361259, 0.00371563, 0.00371488, 0.00513505,
   0.00513498, 0.0037117, 0.0037125, 0.00353196, 0.00353191
  };

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[];

int fetch = HORIZON * 2;
int timer = 0;
int state = 0;

Загрузим нашу ONNX-модель в советника в виде ресурса.

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| DISCLAIMER                                                       |
//| This ONNX model was trained from 1 January 2003 until 29 January |
//| 2023. For reliable results, ensure that all back tests are done  |
//| beyond the model's training period.                              |
//+------------------------------------------------------------------+
#resource "\\Files\\MA Approximation\\EURUSD GBR MA D1.onnx" as const uchar onnx_proto[];

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

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\ONNX\ONNXFloat.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>

CTrade         Trade;
ONNXFloat      *onnx_handler;
Time           *time_handler;
TradeInfo      *trade_handler;

В процессе инициализации советника загрузим все эти библиотеки и технические индикаторы.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   onnx_handler  = new ONNXFloat(onnx_proto);
   time_handler  = new Time(Symbol(),TF);
   trade_handler = new TradeInfo(Symbol(),TF);

   Print("Onnx Handler Pointer: ",onnx_handler);

   onnx_handler.DefineOnnxInputShape(0,1,40);

   onnx_handler.DefineOnnxOutputShape(0,1,1);

//---Setup our technical indicators
   ma_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_LOW);

//---
   return(INIT_SUCCEEDED);
  }

Добавим удаление всех динамически созданных объектов и освободим технические индикаторы, которые больше не используются. 

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   IndicatorRelease(ma_handle);
   IndicatorRelease(ma_o_handle);
   IndicatorRelease(ma_h_handle);
   IndicatorRelease(ma_l_handle);
   delete time_handler;
   delete trade_handler;
   delete onnx_handler;
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(time_handler.NewCandle())
     {
      update();
      check_signal();
     }
  }

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

//+------------------------------------------------------------------+
//| Update our technical data                                        |
//+------------------------------------------------------------------+
void update(void)
  {

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading);
   ArraySetAsSeries(ma_o_reading,true);
   CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading);
   ArraySetAsSeries(ma_h_reading,true);
   CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading);
   ArraySetAsSeries(ma_l_reading,true);

   vectorf model_input_vector =
     {
      //--- OHLC
      iOpen(_Symbol,PERIOD_CURRENT,0),
      iHigh(_Symbol,PERIOD_CURRENT,0),
      iLow(_Symbol,PERIOD_CURRENT,0),
      iClose(_Symbol,PERIOD_CURRENT,0),
      //--- MA OHLC
      ma_reading[0],
      ma_o_reading[0],
      ma_h_reading[0],
      ma_l_reading[0],
      //--- Growth in OHLC
      iOpen(_Symbol,PERIOD_CURRENT,0)   - iOpen(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      iHigh(_Symbol,PERIOD_CURRENT,0)   - iHigh(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      iLow(_Symbol,PERIOD_CURRENT,0)    - iLow(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      iClose(_Symbol,PERIOD_CURRENT,0)  - iClose(_Symbol,PERIOD_CURRENT,(0 + HORIZON)),
      //--- Growth in MA OHLC
      ma_reading[0]   - ma_reading[(0 + HORIZON)],
      ma_o_reading[0] - ma_o_reading[(0 + HORIZON)],
      ma_h_reading[0] - ma_h_reading[(0 + HORIZON)],
      ma_l_reading[0] - ma_l_reading[(0 + HORIZON)],
      //--- Growth between channels
      iOpen(_Symbol,PERIOD_CURRENT,0) - iClose(_Symbol,PERIOD_CURRENT,0),
      iOpen(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iClose(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iOpen(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,0),
      iOpen(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iOpen(_Symbol,PERIOD_CURRENT,0) - iHigh(_Symbol,PERIOD_CURRENT,0),
      iOpen(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iHigh(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iHigh(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,0),
      iHigh(_Symbol,PERIOD_CURRENT,0 + HORIZON) - iLow(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iClose(_Symbol,PERIOD_CURRENT,0)  -  iHigh(_Symbol,PERIOD_CURRENT,0),
      iClose(_Symbol,PERIOD_CURRENT,0 + HORIZON)  -  iHigh(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      iClose(_Symbol,PERIOD_CURRENT,0)  -  iLow(_Symbol,PERIOD_CURRENT,0),
      iClose(_Symbol,PERIOD_CURRENT,0 + HORIZON)  -  iLow(_Symbol,PERIOD_CURRENT,0 + HORIZON),
      //--- Growth between moving average channels
      ma_o_reading[0] - ma_reading[0],
      ma_o_reading[(0 + HORIZON)] -  ma_reading[(0 + HORIZON)],
      ma_o_reading[0] -  ma_l_reading[0],
      ma_o_reading[(0 + HORIZON)] - ma_l_reading[(0 + HORIZON)],
      ma_o_reading[0] -  ma_h_reading[0],
      ma_o_reading[(0 + HORIZON)] - ma_h_reading[(0 + HORIZON)],
      ma_h_reading[0] - ma_l_reading[0],
      ma_h_reading[(0 + HORIZON)] - ma_l_reading[(0 + HORIZON)],
      ma_reading[0] -  ma_h_reading[0],
      ma_reading[(0 + HORIZON)] - ma_h_reading[(0 + HORIZON)],
      ma_reading[0] -  ma_l_reading[0],
      ma_reading[(0 + HORIZON)] - ma_l_reading[(0 + HORIZON)]
     };

   for(int i =0;i<40;i++)
     {
      model_input_vector[i] = ((model_input_vector[i] - Z1[i]) / Z2[i]);
     }

   onnx_handler.Predict(model_input_vector);
  }

Наша функция проверки сигналов работает как и ожидалось. Сначала сбросим таймер, если в данный момент нет открытых позиций, тем самым вернем систему в исходное состояние. Если модель ONNX прогнозирует бычье движение цены, открываем позицию buy. Если прогнозирует медвежье движение — позицию sell. Обратите внимание, что бычьи и медвежьи сигналы соответствуют вероятностям классов, которые выдает модель: вероятность больше 0.5 указывает на ожидаемое бычье движение, а вероятность меньше 0.5 указывает на ожидаемое медвежье движение. 

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

//+------------------------------------------------------------------+
//| Check if we have oppurtunities to trade                          |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      timer = 0;
      state = 0;

      if(onnx_handler.GetPrediction() > 0.5  
        {
         state =1;
         Trade.Buy(trade_handler.MinVolume(),trade_handler.GetSymbol(),trade_handler.GetAsk(),0,0,"");
        }

      else
         if(onnx_handler.GetPrediction() < 0.5 && ma_reading[0] < ma_o_reading[0])
           {
            state =-1;
            Trade.Sell(trade_handler.MinVolume(),trade_handler.GetSymbol(),trade_handler.GetBid(),0,0,"");
           }
     }

   else
     {
      timer++;
      if(timer >= HORIZON)
         Trade.PositionClose(Symbol());
     }

  }

Не забывайте делать undefine.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON
#undef MA_PERIOD
#undef MA_TYPE
#undef TF

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

Рисунок 4: Период тестирования не совпадает с периодом обучения, который модель видела.

Обязательно выбираем случайную задержку (Random delay), чтобы реалистично смоделировать непредсказуемую природу реальных торговых сессий.

Рисунок 5: Random delay для реалистичного тестирования.

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

Рисунок 6: Визуализация кривой средств от нашей модели.

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

Рисунок 7: Отчеты результатов работы нашей торговой стратегии.


Заключение

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

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

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

Название файла Описание файла
EURUSD GBR MA D1.onnx Модель ONNX, которую мы создали, используя многомерный набор данных с новыми столбцами.
Proof of Case Article.ipynb Созданный блокнот Jupyter, который демонстрирует преимущества проецирования данных в многомерное пространство.
MA Strategy EURUSD.ex5 Скомпилированная версия советника, разработанного для работы с моделью с увеличенным набором данных.
Fetch Data MA.mq5 MQL5-скрипт для получения многомерного набора данных и записи в CSV-файл.

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

Прикрепленные файлы |
Fetch_Data_MA.mq5 (7.87 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (Окончание) Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (Окончание)
В данной статье мы завершаем перенос ключевых компонентов фреймворка OneTrans в среду MQL5 и показываем их интеграцию в единый вычислительный граф. Основное внимание уделено организации обучения моделей на исторических финансовых данных с использованием Актера и Критика, а также оценке действий через псевдо идеальные сценарии. Результаты тестирования демонстрируют практическую ценность реализованных решений для построения адаптивных стратегий и анализа рыночной динамики.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
От начального до среднего уровня: Объекты (I) От начального до среднего уровня: Объекты (I)
В данной статье мы начнём рассматривать, как можно работать с объектами непосредственно на графике. Это делается с помощью кода, специально разработанного для демонстрации. Работа с объектами очень интересна и доставляет немало удовольствия. Поскольку это будет наш первый контакт, начнём с чего-нибудь очень простого.