English 中文 Deutsch 日本語
preview
Создание самооптимизирующихся советников на MQL5 (Часть 7): Одновременная торговля на нескольких периодах

Создание самооптимизирующихся советников на MQL5 (Часть 7): Одновременная торговля на нескольких периодах

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

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

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

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

Предложенное нами в этой статье решение позволяет нам устранить сложность определения оптимального периода и вместо этого использовать все доступные нам периоды одновременно. Для достижения этой цели мы рассмотрим семейство алгоритмов машинного обучения, известное как алгоритмы уменьшения размерности (Dimension Reduction Algorithms), с особым акцентом на относительно новом алгоритме, известном как аппроксимация и проекция равномерного многообразия (Uniform Manifold Approximation And Projection, UMAP). Далее мы покажем, что это семейство алгоритмов позволяет нам использовать все доступные данные, описывающие проблему в осмысленном виде, что дает больше информации, чем набор данных в его первоначальном виде.

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


Создание необходимых классов в MQL5

В нашем последнем обсуждении самооптимизирующихся советников мы разработали класс RSI, который предоставил нам осмысленный и организованный способ получения данных индикатора за множество различных периодов RSI. На случай если вы пропустили, статью можно найти здесь. Однако в рамках данного обсуждения мы отойдем от RSI и заменим его индикатором процентного диапазона Уильямса (WPR).

WPR обычно считается осциллятором импульса, и его полный возможный диапазон составляет от 0 до -100. Значения от 0 до -20 считаются медвежьими, а значения от -80 до -100 — бычьими. По сути, индикатор работает путем сравнения текущей цены определенного символа с максимальным значением, достигнутым за выбранный пользователем период. Наша первая задача — создать новый класс SingleBufferIndicator, который будет использоваться как классом RSI, так и классом WPR. Благодаря тому, что классы RSI и WPR имеют общего родителя, мы получим согласованную функциональность обоих классов индикаторов. Начнем с определения класса SingleBufferIndicator и перечисления его членов. 

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


Рис. 1. Визуализация дерева наследования нашего семейства индикаторов с одним буфером

Для начала мы обобщим функциональность, использованную при разработке класса RSI, чтобы она подходила для любого индикатора, имеющего только один буфер. MetaTrader 5 предлагает полный набор необходимых индикаторов. Тот факт, что мы создаем класс для индикаторов с одним буфером, должен подсказать читателю, что существуют индикаторы, имеющие более одного буфера. При разработке классов, как правило, мы хотим, чтобы у класса была четкая и определенная цель.

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

//+------------------------------------------------------------------+
//|                                        SingleBufferIndicator.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

class SingleBufferIndicator
  {

public:
   
   //--- Class methods
   bool              SetIndicatorValues(int buffer_size,bool set_as_series);
   double            GetReadingAt(int index);
   bool              SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series);
   double            GetDifferencedReadingAt(int index);
   double            GetCurrentReading(void);

   //--- Have the indicator values been copied to the buffer?
   bool              indicator_values_initialized;
   bool              indicator_differenced_values_initialized;

   //--- How far into the future we wish to forecast
   int               forecast_horizon;

   //--- The buffer for our indicator
   double            indicator_reading[];
   vector            indicator_differenced_values;

   //--- The current size of the buffer the user last requested
   int               indicator_buffer_size;
   int               indicator_differenced_buffer_size;

   //--- The handler for our indicator
   int               indicator_handler;

   //--- The time frame our indicator should be applied on
   ENUM_TIMEFRAMES   indicator_time_frame;

   //--- The price should the indicator be applied on
   ENUM_APPLIED_PRICE indicator_price;

   //--- Give the user feedback
   string            user_feedback(int flag);

   //--- The Symbol our indicator should be applied on
   string            indicator_symbol;

   //--- Our period
   int               indicator_period;

   //--- Is our indicator valid?
   bool              IsValid(void);

   //---- Testing the Single Buffer Indicator Class
   //--- This method should be deleted in production
   virtual void      Test(void);
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Set our indicator values and our buffer size                     |
//+------------------------------------------------------------------+
bool              SingleBufferIndicator::SetIndicatorValues(int buffer_size,bool set_as_series)
  {

//--- Buffer size
   indicator_buffer_size = buffer_size;
   CopyBuffer(this.indicator_handler,0,0,buffer_size,indicator_reading);

//--- Should the array be set as series?
   if(set_as_series)
      ArraySetAsSeries(this.indicator_reading,true);
   indicator_values_initialized = true;

//--- Did something go wrong?
   vector indicator_test;
   indicator_test.CopyIndicatorBuffer(indicator_handler,0,0,buffer_size);
   if(indicator_test.Sum() == 0)
      return(false);

//--- Everything went fine.
   return(true);
  }

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

//+--------------------------------------------------------------+
//| Let's set the conditions for our differenced data            |
//+--------------------------------------------------------------+
bool              SingleBufferIndicator::SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series)
  {
//--- Internal variables
   indicator_differenced_buffer_size = buffer_size;
   indicator_differenced_values = vector::Zeros(indicator_differenced_buffer_size);

//--- Prepare to record the differences in our RSI readings
   double temp_buffer[];
   int fetch = (indicator_differenced_buffer_size + (2 * differencing_period));
   CopyBuffer(indicator_handler,0,0,fetch,temp_buffer);
   if(set_as_series)
      ArraySetAsSeries(temp_buffer,true);

//--- Fill in our values iteratively
   for(int i = indicator_differenced_buffer_size;i > 1; i--)
     {
      indicator_differenced_values[i-1] = temp_buffer[i-1] - temp_buffer[i-1+differencing_period];
     }

//--- If the norm of a vector is 0, the vector is empty!
   if(indicator_differenced_values.Norm(VECTOR_NORM_P) != 0)
     {
      Print(user_feedback(2));
      indicator_differenced_values_initialized = true;
      return(true);
     }

   indicator_differenced_values_initialized = false;
   Print(user_feedback(3));
   return(false);
  }

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

//--- Get a differenced value at a specific index
double            SingleBufferIndicator::GetDifferencedReadingAt(int index)
  {
//--- Make sure we're not trying to call values beyond our index
   if(index > indicator_differenced_buffer_size)
     {
      Print(user_feedback(4));
      return(-1e10);
     }

//--- Make sure our values have been set
   if(!indicator_differenced_values_initialized)
     {
      //--- The user is trying to use values before they were set in memory
      Print(user_feedback(1));
      return(-1e10);
     }

//--- Return the differenced value of our indicator at a specific index
   if((indicator_differenced_values_initialized) && (index < indicator_differenced_buffer_size))
      return(indicator_differenced_values[index]);

//--- Something went wrong.
   return(-1e10);
  }

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

//+------------------------------------------------------------------+
//| Get a reading at a specific index from our RSI buffer            |
//+------------------------------------------------------------------+
double            SingleBufferIndicator::GetReadingAt(int index)
  {
//--- Is the user trying to call indexes beyond the buffer?
   if(index > indicator_buffer_size)
     {
      Print(user_feedback(4));
      return(-1e10);
     }

//--- Get the reading at the specified index
   if((indicator_values_initialized) && (index < indicator_buffer_size))
      return(indicator_reading[index]);

//--- User is trying to get values that were not set prior
   else
     {
      Print(user_feedback(1));
      return(-1e10);
     }
  }

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

//+------------------------------------------------------------------+
//| Get our current reading from the RSI indicator                   |
//+------------------------------------------------------------------+
double SingleBufferIndicator::GetCurrentReading(void)
  {
   double temp[];
   CopyBuffer(this.indicator_handler,0,0,1,temp);
   return(temp[0]);
  }

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

//+------------------------------------------------------------------+
//| Check if our indicator handler is valid                          |
//+------------------------------------------------------------------+
bool SingleBufferIndicator::IsValid(void)
  {
   return((this.indicator_handler != INVALID_HANDLE));
  }

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

//+------------------------------------------------------------------+
//| Give the user feedback on the actions he is performing           |
//+------------------------------------------------------------------+
string SingleBufferIndicator::user_feedback(int flag)
  {
   string message;

//--- Check if the indicator loaded correctly
   if(flag == 0)
     {
      //--- Check the indicator was loaded correctly
      if(IsValid())
         message = "Indicator Class Loaded Correcrtly \nSymbol: " + (string) indicator_symbol + "\nPeriod: " + (string) indicator_period;
      return(message);
      //--- Something went wrong
      message = "Error loading Indicator: [ERROR] " + (string) GetLastError();
      return(message);
     }

//--- User tried getting indicator values before setting them
   if(flag == 1)
     {
      message = "Please set the indicator values before trying to fetch them from memory, call SetIndicatorValues()";
      return(message);
     }

//--- We sueccessfully set our differenced indicator values
   if(flag == 2)
     {
      message = "Succesfully set differenced indicator values.";
      return(message);
     }

//--- Failed  to set our differenced indicator values
   if(flag == 3)
     {
      message = "Failed to set our differenced indicator values: [ERROR] " + (string) GetLastError();
      return(message);
     }

//--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first
   if(flag == 4)
     {
      message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first";
      return(message);
     }

//--- The class has been deactivated by the user
   if(flag == 5)
     {
      message = "Goodbye.";
      return(message);
     }

//--- No feedback
   else
      return("");
  }

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

Рис. 2. Дерево зависимостей для наших классов индикаторов

Теперь перейдем к первому шагу, который мы предпримем в нашем классе WPR, а именно к включению класса SingleBufferIndicator в класс WPR.

//+------------------------------------------------------------------+
//|                                                          WPR.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Load the parent class                                            |
//+------------------------------------------------------------------+
#include <VolatilityDoctor\Indicators\SingleBuffer\SingleBufferIndicator.mqh>

На этот раз, прежде чем определять члены класса WPR, мы укажем, что этот класс расширяет класс SingleBufferIndicator, используя двоеточие ":". Вот как мы расширяем классы в MQL5. Для читателей, незнакомых с концепциями ООП, поясним: расширение класса позволяет нам вызывать методы, написанные в классе SingleBufferIndicator, из класса WPR. Благодаря тому, что классы WPR и RSI наследуют класс SingleBufferIndicator, мы получим согласованную функциональность в обоих классах. Иными словами, все открытые члены класса SingleBufferIndicator, которые мы добавили в наш класс, будут легко доступны в любом классе, который его наследует.

//+------------------------------------------------------------------+
//| This class will provide us with usefull functionality for the WPR|
//+------------------------------------------------------------------+
class WPR : public SingleBufferIndicator
  {
public:
                     WPR();
                     WPR(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period);
                    ~WPR();
  };

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

//+------------------------------------------------------------------+
//| Our default constructor for our Indicator class                  |
//+------------------------------------------------------------------+
void WPR::WPR()
  {
   indicator_values_initialized       = false;
   indicator_symbol                   = "EURUSD";
   indicator_time_frame               = PERIOD_D1;
   indicator_period                   = 5;
   indicator_handler                  = iWPR(indicator_symbol,indicator_time_frame,indicator_period);
//--- Give the user feedback on initilization
   Print(user_feedback(0));
//--- Remind the user they called the default constructor
   Print("Default Constructor Called: ",__FUNCSIG__," ",&this);
  }

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

//+------------------------------------------------------------------+
//| Our parametric constructor for our Indicator class               |
//+------------------------------------------------------------------+
void WPR::WPR(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period)
  {
   indicator_values_initialized       = false;
   indicator_symbol                   = user_symbol;
   indicator_time_frame               = user_time_frame;
   indicator_period                   = user_period;
   indicator_handler                  = iWPR(indicator_symbol,indicator_time_frame,indicator_period);
//--- Give the user feedback on initilization
   Print(user_feedback(0));
  }

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

//+------------------------------------------------------------------+
//| Our destructor for our Indicator class                           |
//+------------------------------------------------------------------+
void WPR::~WPR()
  {
//--- Free up resources we don't need and reset our flags
   if(IndicatorRelease(indicator_handler))
     {
      indicator_differenced_values_initialized = false;
      indicator_values_initialized = false;
      Print(user_feedback(5));
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//|                                                         Time.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

class Time
  {
private:
   datetime          time_stamp;
   datetime          current_time;
   string            selected_symbol;
   ENUM_TIMEFRAMES   selected_time_frame;
public:
                     Time(string user_symbol,ENUM_TIMEFRAMES user_time_frame);
   bool              NewCandle(void);
                    ~Time();
  };

Обратите внимание, что у класса нет конструктора по умолчанию, это сделано намеренно. В данном конкретном случае использование конструкторов по умолчанию не имело бы особого смысла.

//+------------------------------------------------------------------+
//| Create our time object                                           |
//+------------------------------------------------------------------+
Time::Time(string user_symbol,ENUM_TIMEFRAMES user_time_frame)
  {
   selected_time_frame = user_time_frame;
   selected_symbol = user_symbol;
   current_time = iTime(user_symbol,selected_time_frame,0);
   time_stamp   = iTime(user_symbol,selected_time_frame,0);
  }

Сейчас деструктор класса пуст.

//+------------------------------------------------------------------+
//| Our destructor is currently empty                                |
//+------------------------------------------------------------------+
Time::~Time()
  {
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Check if a new candle has fully formed                           |
//+------------------------------------------------------------------+
bool Time::NewCandle(void)
  {
   current_time = iTime(selected_symbol,selected_time_frame,0);

//--- Check if a new candle has formed
   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      return(true);
     }

//--- No new candle has completely formed
   return(false);
  }

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

//+------------------------------------------------------------------+
//|                                                    ONNXFloat.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| This class will help us work with ONNX Float models.             |
//+------------------------------------------------------------------+
class ONNXFloat
  {
private:
   //--- Our ONNX model handler
   long              onnx_model;
   int               onnx_outputs;
public:
   //--- Is our Model Valid?
   bool              OnnxModelIsValid(void);

   //--- Define the input shape of our model
   bool              DefineOnnxInputShape(int n_index,int n_stacks,int n_input_params);

   //--- Define the output shape of our model
   bool              DefineOnnxOutputShape(int n_index,int n_stacks, int n_output_params);

   vectorf           Predict(const vectorf &model_inputs);

   //--- ONNXFloat class constructor
                     ONNXFloat(const uchar &user_proto[]);

   //---- ONNXFloat class destructor
                    ~ONNXFloat();
  };

Конструктор нашего класса принимает прототип модели ONNX и создает модель ONNX из буфера, переданного пользователем. Обратите внимание, что буферы модели ONNX могут передаваться только по ссылке, а не по значению. Символ амперсанда "&", расположенный перед именем буфера модели ONNX "&user_proto", явно указывает на то, что этот параметр является ссылкой на объект в памяти. Всякий раз, когда функция имеет параметр, передаваемый по ссылке, пользователь должен понимать, что любые изменения параметра внутри функции приведут к изменению исходного параметра за пределами функции.

В нашем случае мы не намерены редактировать прототип ONNX; поэтому мы изменяем параметр на const, указывая программисту и компилятору, что никаких изменений вносить не следует. Следовательно, если программист проигнорирует наши директивы, компилятор этого не примет.

//+------------------------------------------------------------------+
//| Parametric Constructor For Our ONNXFloat class                   |
//+------------------------------------------------------------------+
ONNXFloat::ONNXFloat(const uchar &user_proto[])
  {
   onnx_model = OnnxCreateFromBuffer(user_proto,ONNX_DATA_TYPE_FLOAT);

   if(OnnxModelIsValid())
      Print("Volatility Doctor ONNXFloat Class Loaded Correctly: ",__FUNCSIG__," ",&this);

   else
      Print("Failed To Create The specified ONNX model: ",GetLastError());
  }

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

//+------------------------------------------------------------------+
//| Our ONNXFloat class destructor                                   |
//+------------------------------------------------------------------+
ONNXFloat::~ONNXFloat()
  {
   OnnxRelease(onnx_model);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| A method that returns true if our ONNXFloat model is valid       |
//+------------------------------------------------------------------+
bool ONNXFloat::OnnxModelIsValid(void)
  {
//--- Check if the model is valid
   if(onnx_model != INVALID_HANDLE)
      return(true);

//--- Something went wrong
   return(false);
  }

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

//+------------------------------------------------------------------+
//| Set the input shape of our ONNXFloat model                       |
//+------------------------------------------------------------------+
bool ONNXFloat::DefineOnnxInputShape(int n_index,int n_stacks,int n_input_params)
  {
   const ulong model_input_shape[] = {n_stacks,n_input_params};

   if(OnnxSetInputShape(onnx_model,n_index,model_input_shape))
     {
      Print("Succefully specified ONNX model output shape: ",__FUNCTION__," ",&this);
      return(true);
     }

//--- Something went wrong
   Print("Failed to set the passed ONNX model output shape: ",GetLastError());
   return(false);
  }

То же самое относится и к форме выходных данных модели ONNX.

//+------------------------------------------------------------------+
//| Set the output shape of our model                                |
//+------------------------------------------------------------------+
bool ONNXFloat::DefineOnnxOutputShape(int n_index,int n_stacks,int n_output_params)
  {
   const ulong model_output_shape[] = {n_output_params,n_stacks};
   onnx_outputs = n_output_params;

   if(OnnxSetOutputShape(onnx_model,n_index,model_output_shape))
     {
      Print("Succefully specified ONNX model input shape: ",__FUNCSIG__," ",&this);
      return(true);
     }

//--- Something went wrong
   Print("Failed to set the passed ONNX model input shape: ",GetLastError());
   return(false);
  }

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

//+------------------------------------------------------------------+
//| Get a prediction from our model                                  |
//+------------------------------------------------------------------+
vectorf ONNXFloat::Predict(const vectorf &model_inputs)
  {
   vectorf model_output(onnx_outputs);
   if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output))
     {
      vectorf res = model_output;
      return(res);
     }

   Comment("Failed to get a prediction from our ONNX model");
   Print("ONNX Run Failed: ",GetLastError());
   vectorf res = {10e8};
   return(res);
  }

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

//+------------------------------------------------------------------+
//|                                                    TradeInfo.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"
class TradeInfo
  {
private:
                     string user_symbol;
                     ENUM_TIMEFRAMES user_time_frame;
                     double min_volume,max_volume,volume_step;
public:
                     TradeInfo(string selected_symbol,ENUM_TIMEFRAMES selected_time_frame);
                     double MinVolume(void);
                     double MaxVolume(void);
                     double VolumeStep(void);
                     double GetAsk(void);
                     double GetBid(void);
                     double GetClose(void);
                     string GetSymbol(void);
                    ~TradeInfo();
  };
  

Конструктор параметрического класса принимает 2 параметра, указывающих на желаемый символ и таймфрейм.

//+------------------------------------------------------------------+
//| The constructor will load our symbol information                 |
//+------------------------------------------------------------------+
TradeInfo::TradeInfo(string selected_symbol,ENUM_TIMEFRAMES selected_time_frame)
  {
      //--- Which symbol are you interested in?
      user_symbol = selected_symbol;
      user_time_frame = selected_time_frame;
      
      if(SymbolSelect(user_symbol,true))
         {
            //--- Load symbol details
            min_volume = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_MIN);
            max_volume = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_MAX);
            volume_step = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_STEP);
            Print("Trade Info Loaded Successfully: ",__FUNCSIG__);
         }
   
      else
         {
            Print("Error Symbol Information Could Not Be Found For: ",selected_symbol," ",GetLastError());
         }
         
  }

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

//+------------------------------------------------------------------+
//| Return the close of the selected symbol                          |
//+------------------------------------------------------------------+
double TradeInfo::GetClose(void)
   {
      double res = iClose(user_symbol,user_time_frame,0);
      return(res);
   }

//+------------------------------------------------------------------+
//| Return the open of the selected symbol                           |
//+------------------------------------------------------------------+
double TradeInfo::GetOpen(void)
   {
      double res = iOpen(user_symbol,user_time_frame,0);
      return(res);
   }

//+------------------------------------------------------------------+
//| Return the high of the selected symbol                           |
//+------------------------------------------------------------------+
double TradeInfo::GetHigh(void)
   {
      double res = iHigh(user_symbol,user_time_frame,0);
      return(res);
   }

//+------------------------------------------------------------------+
//| Return the low of the selected symbol                            |
//+------------------------------------------------------------------+
double TradeInfo::GetLow(void)
   {
      double res = iLow(user_symbol,user_time_frame,0);
      return(res);
   }

При работе с несколькими символами полезно иметь напоминание о том, какому символу присвоен текущий экземпляр класса.

//+------------------------------------------------------------------+
//| Return the selected symbol                                       |
//+------------------------------------------------------------------+
string TradeInfo::GetSymbol(void)
   {
      string res = user_symbol;
      return(res);
   }

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

//+------------------------------------------------------------------+
//| Return the volume step allowed                                   |
//+------------------------------------------------------------------+
double TradeInfo::VolumeStep(void)
   {
      double res = volume_step;
      return(res);
   }  

//+------------------------------------------------------------------+
//| Return the minimum volume allowed                                |
//+------------------------------------------------------------------+
double TradeInfo::MinVolume(void)
   {
      double res = min_volume;
      return(res);
   }  

//+------------------------------------------------------------------+
//| Return the maximum volume allowed                                |
//+------------------------------------------------------------------+
double TradeInfo::MaxVolume(void)
   {
      double res = max_volume;
      return(res);
   } 

Нам также потребуется, чтобы класс оперативно предоставлял нам текущие цены bid и ask.

//+------------------------------------------------------------------+
//| Return the current ask                                           |
//+------------------------------------------------------------------+
double TradeInfo::GetAsk(void)
   {
      return(SymbolInfoDouble(GetSymbol(),SYMBOL_ASK));
   }

//+------------------------------------------------------------------+
//| Return the current bid                                           |
//+------------------------------------------------------------------+
double TradeInfo::GetBid(void)
   {
      return(SymbolInfoDouble(GetSymbol(),SYMBOL_BID));
   }

В данный момент деструктор класса Time пуст.

//+------------------------------------------------------------------+
//| Destructor is currently empty                                    |
//+------------------------------------------------------------------+
TradeInfo::~TradeInfo()
  {
  }
//+------------------------------------------------------------------+

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

Рис. 3. Эти классы следует хранить в дереве зависимостей, похожем на наше

Теперь давайте определим скрипт, который будет получать необходимые нам рыночные данные. Сначала мы хотим получить данные по четырем основным ценовым потокам (OHLC), затем данные о росте этих четырех ценовых потоков, и, наконец, мы выведем данные по 14 индикаторам WPR. 

//+------------------------------------------------------------------+
//|                                                      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

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define HORIZON 10

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <VolatilityDoctor\Indicators\WPR.mqh>

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
WPR *my_wpr_array[14];
string file_name = Symbol() + " WPR Algorithmic Input Selection.csv";

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int size = 3000;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- How much data should we store in our indicator buffer?
   int fetch = size + (2 * HORIZON);

//--- Store pointers to our WPR objects
   for(int i = 0; i <= 13; i++)
     {
      //--- Create an WPR object
      my_wpr_array[i] = new WPR(Symbol(),PERIOD_CURRENT,((i+1) * 5));
      //--- Set the WPR buffers
      my_wpr_array[i].SetIndicatorValues(fetch,true);
      my_wpr_array[i].SetDifferencedIndicatorValues(fetch,HORIZON,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","True Open","True High","True Low","True Close","Open","High","Low","Close","WPR 5","WPR 10","WPR 15","WPR 20","WPR 25","WPR 30","WPR 35","WPR 40","WPR 45","WPR 50","WPR 55","WPR 60","WPR 65","WPR 70","Diff WPR 5","Diff WPR 10","Diff WPR 15","Diff WPR 20","Diff WPR 25","Diff WPR 30","Diff WPR 35","Diff WPR 40","Diff WPR 45","Diff WPR 50","Diff WPR 55","Diff WPR 60","Diff WPR 65","Diff WPR 70");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   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),
                   my_wpr_array[0].GetReadingAt(i),
                   my_wpr_array[1].GetReadingAt(i),
                   my_wpr_array[2].GetReadingAt(i),
                   my_wpr_array[3].GetReadingAt(i),
                   my_wpr_array[4].GetReadingAt(i),
                   my_wpr_array[5].GetReadingAt(i),
                   my_wpr_array[6].GetReadingAt(i),
                   my_wpr_array[7].GetReadingAt(i),
                   my_wpr_array[8].GetReadingAt(i),
                   my_wpr_array[9].GetReadingAt(i),
                   my_wpr_array[10].GetReadingAt(i),
                   my_wpr_array[11].GetReadingAt(i),
                   my_wpr_array[12].GetReadingAt(i),
                   my_wpr_array[13].GetReadingAt(i),
                   my_wpr_array[0].GetDifferencedReadingAt(i),
                   my_wpr_array[1].GetDifferencedReadingAt(i),
                   my_wpr_array[2].GetDifferencedReadingAt(i),
                   my_wpr_array[3].GetDifferencedReadingAt(i),
                   my_wpr_array[4].GetDifferencedReadingAt(i),
                   my_wpr_array[5].GetDifferencedReadingAt(i),
                   my_wpr_array[6].GetDifferencedReadingAt(i),
                   my_wpr_array[7].GetDifferencedReadingAt(i),
                   my_wpr_array[8].GetDifferencedReadingAt(i),
                   my_wpr_array[9].GetDifferencedReadingAt(i),
                   my_wpr_array[10].GetDifferencedReadingAt(i),
                   my_wpr_array[11].GetDifferencedReadingAt(i),
                   my_wpr_array[12].GetDifferencedReadingAt(i),
                   my_wpr_array[13].GetDifferencedReadingAt(i)
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);

//--- Delete our WPR object pointers
   for(int i = 0; i <= 13; i++)
     {
      delete my_wpr_array[i];
     }
  }
//+------------------------------------------------------------------+
#undef HORIZON


Анализ наших данных с помощью Python

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

#Load the libraries
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

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

#Read in the data
data = pd.read_csv("..\EURGBP WPR Algorithmic Input Selection.csv")

#Label the data
HORIZON = 10
data['Target'] = data['Close'].shift(-HORIZON) - data['Close']

#Drop the last 10 rows 
data = data.iloc[:-HORIZON,:]

Создадим копии входных параметров и целевых данных.

#Define inputs and target
X = data.iloc[:,1:-1].copy() 
y = data.iloc[:,-1].copy()

Масштабируем и центрируем каждый числовой столбец в наборе данных.

#Store Z-scores
Z1 = X.mean()
Z2 = X.std()

#Scale the data
X = ((X - Z1)/ Z2)

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

from sklearn.model_selection import cross_val_score,TimeSeriesSplit
from sklearn.linear_model import Ridge

Создадим объект для перекрестной проверки временных рядов.

tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)sdvdsvds

Определим метод, который всегда будет возвращать уровни точности, полученные в результате перекрестной проверки.

#Return our cross validated accuracy
def score(f_model,f_X,f_y):
    return(np.mean(np.abs(cross_val_score(f_model,f_X,f_y,scoring='neg_mean_squared_error',cv=tscv,n_jobs=-1))))

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

def get_model():
    return(Ridge())

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

X['Null'] = 0

Зафиксируем ошибку, возникающую при постоянном прогнозировании средней доходности рынка (сумма квадратов / сумма квадратов убытков). Теперь, когда мы определили пороговое значение ошибки, заданное постоянным прогнозированием средней доходности рынка, мы можем с уверенностью утверждать, что любая модель, демонстрирующая уровень ошибки выше 0,000324, не обладает достаточной эффективностью в рамках данного обсуждения.

#This will be the last entry in our list of results
#Record our error if we always predict the average market return (total sum of squares/TSS)
tss = score(get_model(),X[['Null']],y)
tss

0.00032439931180771236

Теперь мы создадим массив, который поможет нам отслеживать результаты.

res = []

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

#This will be our first entry in our list of results 
#Record our error using OHLC price data
res.append(score(get_model(),X.iloc[:,:8],y))

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

#Second
#Record our error using just indicators
res.append(score(get_model(),X.iloc[:,8:-1],y))

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

#Third
#Record our error using all the data we have
res.append(score(get_model(),X.iloc[:,:-1],y))

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

import umap

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

EPOCHS = X.iloc[:,:-1].shape[1] - 2

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

for i in range(EPOCHS):
    reducer = umap.UMAP(n_components=(i+1),metric='euclidean',random_state=0,transform_seed=0,n_neighbors=30)
    X_embedded = pd.DataFrame(reducer.fit_transform(X.iloc[:,:-1]))
    res.append(score(get_model(),X_embedded,y))

Объединим результаты.

res.append(tss)

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

Рис. 4. Используя два встроенных компонента UMAP, мы превзошли аналогичную модель, использующую все рыночные данные в их исходном виде

Преобразуем данные, используя оптимальные настройки UMAP, которые мы определили.

reducer = umap.UMAP(n_components=2,metric='euclidean',random_state=0,transform_seed=0,n_neighbors=30)
X_embedded = pd.DataFrame(reducer.fit_transform(X.iloc[:,:-1]))

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

data['Class'] = 0

data.loc[data['Target'] > 0,'Class'] = 1

Подготовим набор данных для хранения преобразованных данных.

umap_data =pd.DataFrame(columns=['UMAP 1','UMAP 2'])

Сохраним встроенные уровни цен.

umap_data['UMAP 1'] = X_embedded.iloc[:,0]
umap_data['UMAP 2'] = X_embedded.iloc[:,1]

Без UMAP наши данные сложно осмысленно визуализировать из-за большого количества измерений. По сути, лучшее, что мы можем сделать, это создать пары диаграмм рассеяния; в противном случае, нет эффективного способа визуализировать 36 измерений одновременно. На рисунках 5 и 6 ниже красные точки обозначают бычье движение цены, а черные — медвежье. 

fig , axs = plt.subplots(2,2)

fig.suptitle('Visualizing EURGBP 2002-2025 Daily Price Data')

axs[0,0].scatter(data.loc[data['Target']>0 ,'Open'],data.loc[data['Target']>0 ,'Close'],color='red')
axs[0,0].scatter(data.loc[data['Target']<0 ,'Open'],data.loc[data['Target']<0 ,'Close'],color='black')

axs[0,1].scatter(data.loc[data['Target']>0 ,'True Open'],data.loc[data['Target']>0 ,'True Close'],color='red')
axs[0,1].scatter(data.loc[data['Target']<0 ,'True Open'],data.loc[data['Target']<0 ,'True Close'],color='black')

axs[1,1].scatter(data.loc[data['Target']>0 ,'WPR 5'],data.loc[data['Target']>0 ,'WPR 50'],color='red')
axs[1,1].scatter(data.loc[data['Target']<0 ,'WPR 5'],data.loc[data['Target']<0 ,'WPR 50'],color='black')

axs[1,0].scatter(data.loc[data['Target']>0 ,'WPR 15'],data.loc[data['Target']>0 ,'WPR 25'],color='red')
axs[1,0].scatter(data.loc[data['Target']<0 ,'WPR 15'],data.loc[data['Target']<0 ,'WPR 25'],color='black')

Рис. 5. Слева показана зависимость изменения цены открытия от изменения цены закрытия. Справа же отображены реальные цены открытия и закрытия

Рис. 6. На левом графике рассеяния показана зависимость между WPR с периодами 5 и 50, а на правом — зависимость между WPR с периодами 15 и 25

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

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

sns.scatterplot(x=X_embedded.iloc[:,0],y=X_embedded.iloc[:,1],hue=data['Class'])
plt.grid()
plt.ylabel('Second UMAP Embedding')
plt.xlabel('First UMAP Embedding')
plt.title('Visualizing The Most Effective Embedding We Found')

Рис. 7. Визуализация наших UMAP-вложений исходных рыночных данных

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

from sklearn.model_selection import train_test_split

Разделим рыночные данные. Наши обучающие выборки охватывают период с ноября 2002 года по август 2018 года, поэтому период тестирования на исторических данных начнется в сентябре 2018 года.

train , test = train_test_split(data,test_size=0.3,shuffle=False)
train

Рис. 8. Просмотр рыночных данных в их первоначальном виде

Теперь давайте загрузим нашу статистическую модель.

from sklearn.neural_network import MLPRegressor

Масштабируем обучающие данные.

#Sample mean
Z1 = train.iloc[:,1:-2].mean()

#Sample standard deviation
Z2 = train.iloc[:,1:-2].std()

train_scaled = train.copy()

train_scaled.iloc[:,1:-2] = ((train.iloc[:,1:-2] - Z1) / Z2)

Встроим обучающие данные.

reducer = umap.UMAP(n_components=2,metric='euclidean',random_state=0,transform_seed=0,n_neighbors=30)
X_embedded = pd.DataFrame(reducer.fit_transform(train_scaled.iloc[:,1:-2],columns=['UMAP 1','UMAP 2']))

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

#Learn To Estimate UMAP Embeddings From The Data
umap_model = MLPRegressor(shuffle=False,hidden_layer_sizes=(train.iloc[:,1:-2].shape[1],10,20,100,20,10,2),random_state=0,solver='lbfgs',activation='relu',learning_rate='constant',learning_rate_init=1e-4,power_t=1e-1)
np.mean(np.abs(cross_val_score(umap_model,train.iloc[:,1:-2],X_embedded,scoring='neg_mean_squared_error',n_jobs=-1)))

11.2489992665160363

Обучим функцию UMAP.

umap_model.fit(train.iloc[:,1:-2],X_embedded)
predictions = umap_model.predict(train.iloc[:,1:-2])

Теперь нам нужна модель, которая прогнозирует доходность рынка EURGBP с учетом UMAP-вложений рынка. В scikit-learn наши модели нейронных сетей имеют важный параметр random_state. Параметр влияет на начальные веса и смещения, с которыми начинает работу нейронная сеть. В зависимости от решаемой задачи, многократное обучение модели с различными начальными состояниями может привести к значительным колебаниям уровня производительности, как показано на рисунке 9 ниже.

EPOCHS = 100
res = []

for i in range(EPOCHS):
    #Try different random states
    model = MLPRegressor(shuffle=False,early_stopping=False,hidden_layer_sizes=(2,1,10,20,1),activation='identity',solver='lbfgs',random_state=i,max_iter=int(2e5))
    res.append(score(model,predictions,train['Target']))

Визуализация наших результатов.

plt.plot(res,color='black')
plt.axhline(np.min(res),color='red',linestyle=':')
plt.scatter(res.index(np.min(res)),np.min(res),color='red')
plt.grid()
plt.ylabel('Cross Validated RMSE')
plt.xlabel('Neural Network Random State')
plt.title('Our Neural Network Performance With Different Initial Conditions')

Рис. 9. Визуализация оптимального начального состояния для нашей нейронной сети в данной задаче.

Выбранная нами нейронная сеть прогнозирует 10-дневную доходность рынка EURGBP с погрешностью на 38% меньше, чем при прогнозировании средней доходности рынка. 

tss = score(Ridge(),train[['Close']]*0,train['Target'])
1-(np.min(res)/tss)

0.3822093585025088

Подгоним модель, используя оптимальное случайное состояние, которое мы определили на рис. 9.

embedded_model = MLPRegressor(shuffle=False,early_stopping=False,hidden_layer_sizes=(2,1,10,20,1),activation='identity',solver='lbfgs',random_state=res.index(np.min(res)),max_iter=int(2e5))
embedded_model.fit(predictions,train['Target'])

Загрузим необходимые библиотеки для преобразования модели в формат ONNX.

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

Определим формы параметров наших моделей.

umap_model_input_shape = [("float_input",FloatTensorType([1,train.iloc[:,1:-2].shape[1]]))]
umap_model_output_shape = [("float_output",FloatTensorType([X_embedded.iloc[:,:].shape[1],1]))]

embedded_model_input_shape = [("float_input",FloatTensorType([1,X_embedded.iloc[:,:].shape[1]]))]
embedded_model_output_shape = [("float_output",FloatTensorType([1,1]))]

Преобразуем модели ONNX в их прототипы.

umap_proto = convert_sklearn(umap_model,initial_types=umap_model_input_shape,final_types=umap_model_output_shape,target_opset=12)
embeded_proto = convert_sklearn(embedded_model,initial_types=embedded_model_input_shape,final_types=embedded_model_output_shape,target_opset=12)

Сохраним прототипы на диск.

onnx.save(umap_proto,"EURGBP WPR Ridge UMAP.onnx")
onnx.save(embeded_proto,"EURGBP WPR Ridge EMBEDDED.onnx")


Разработка приложения на MQL5

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

//+------------------------------------------------------------------+
//|                             EURGBP Multiple Periods Analysis.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| REMINDER:                                                        |
//| These ONNX models were trained with Daily EURGBP data ranging    |
//| from 24 November 2002 until 12 August 2018. Test the strategy    |
//| outside of these time periods, on the Daily Time-Frame for       |
//| reliable results.                                                |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+

//--- ONNX Model I/O Parameters
#define UMAP_INPUTS 36
#define UMAP_OUTPUTS 2
#define EMBEDDED_INPUTS  2
#define EMBEDDED_OUTPUTS 1

//--- Our forecasting periods
#define HORIZON 10

//--- Our desired time frame
#define SYSTEM_TIMEFRAME_1 PERIOD_D1

Теперь давайте загрузим наши ONNX-модели.

//+------------------------------------------------------------------+
//| Load our ONNX models as resources                                |
//+------------------------------------------------------------------+

//--- ONNX Model Prototypes
#resource  "\\Files\\EURGBP WPR UMAP.onnx" as const uchar umap_proto[];
#resource  "\\Files\\EURGBP WPR EMBEDDED.onnx" as const uchar embedded_proto[];

Затем мы загрузим библиотеки, необходимые для нашего приложения.

//+------------------------------------------------------------------+
//| Libraries We Need                                                |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Indicators\WPR.mqh>
#include <VolatilityDoctor\ONNX\OnnxFloat.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>

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

//+------------------------------------------------------------------+
//| Global varaibles                                                 |
//+------------------------------------------------------------------+
CTrade Trade;
TradeInfo *TradeInformation;

//--- Our time object let's us know when a new candle has fully formed on the specified time-frame
Time *eurgbp_daily;

//--- All our different William's Percent Range Periods will be kept in a single array
WPR *wpr_array[14];

//--- Our ONNX class objects have usefull functions designed for rapid ONNX development
ONNXFloat *umap_onnx,*embedded_onnx;

//--- Model forecast
double expected_return;

int position_timer;

Мы также скопировали значения Z1 и Z2, которые использовали для масштабирования обучающих данных в Python.

//--- The average column values from the training set
double Z1[] = {7.84311120e-01,  7.87104135e-01,  7.81713516e-01,  7.84343731e-01,
               5.23887980e-04,  5.26022077e-04,  5.25382257e-04,  5.25688880e-04,
               -5.08398234e+01, -5.07130228e+01, -5.05834313e+01, -5.04425081e+01,
               -5.02709031e+01, -5.01349627e+01, -5.00653250e+01, -5.01661938e+01,
               -5.03082375e+01, -5.04550339e+01, -5.05861939e+01, -5.06434696e+01,
               -5.07286211e+01, -5.07819768e+01,  1.96979782e-02,  5.29204133e-02,
               4.12732506e-02,  3.20037455e-02,  2.61762719e-02,  2.34184127e-02,
               2.62342592e-02,  3.32894491e-02,  3.81853070e-02,  3.85464026e-02,
               3.85499926e-02,  3.94004124e-02,  4.02388908e-02,  4.02388908e-02
               };

//--- The column standard deviation from the training set
double Z2[] = {8.29473604e-02, 8.35406090e-02, 8.23981331e-02, 8.28950223e-02,
               1.21995172e-02, 1.22880295e-02, 1.20471133e-02, 1.21798952e-02,
               3.00742110e+01, 3.05948913e+01, 3.05244154e+01, 3.03776475e+01,
               3.02862706e+01, 3.00844693e+01, 2.98788650e+01, 2.97182936e+01,
               2.95133008e+01, 2.93983475e+01, 2.92679071e+01, 2.91072869e+01,
               2.90154368e+01, 2.89821474e+01, 4.32293242e+01, 4.43537714e+01,
               4.02730688e+01, 3.66106699e+01, 3.41930128e+01, 3.21743917e+01,
               3.03647897e+01, 2.87462989e+01, 2.73771066e+01, 2.63857585e+01,
               2.54625376e+01, 2.43656339e+01, 2.33983568e+01, 2.26334633e+01
              };

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Do no display the indicators, they will clutter our view
   TesterHideIndicators(true);

//--- Setup our pointers to our WPR objects
   update_indicators();

//--- Get trade information on the symbol
   TradeInformation = new TradeInfo(Symbol(),SYSTEM_TIMEFRAME_1);

//--- Create our ONNXFloat objects
   umap_onnx     = new ONNXFloat(umap_proto);
   embedded_onnx = new ONNXFloat(embedded_proto);

//--- Create our Time management object
   eurgbp_daily = new Time(Symbol(),SYSTEM_TIMEFRAME_1);

//--- Check if the models are valid
   if(!umap_onnx.OnnxModelIsValid())
      return(INIT_FAILED);
   if(!embedded_onnx.OnnxModelIsValid())
      return(INIT_FAILED);

//--- Reset our position timer
   position_timer = 0;

//--- Specify the models I/O shapes
   if(!umap_onnx.DefineOnnxInputShape(0,1,UMAP_INPUTS))
      return(INIT_FAILED);
   if(!embedded_onnx.DefineOnnxInputShape(0,1,EMBEDDED_INPUTS))
      return(INIT_FAILED);

   if(!umap_onnx.DefineOnnxOutputShape(0,1,UMAP_OUTPUTS))
      return(INIT_FAILED);
   if(!embedded_onnx.DefineOnnxOutputShape(0,1,EMBEDDED_OUTPUTS))
      return(INIT_FAILED);
      
//---
   return(INIT_SUCCEEDED);
  }

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

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the pointers for our custom objects
   delete umap_onnx;
   delete embedded_onnx;
   delete eurgbp_daily;
   //--- Delete all pointers to our WPR objects
   for(int i = 0; i <= 13; i++)
     {
         delete wpr_array[i];
     }
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Do we have a new daily candle?
   if(eurgbp_daily.NewCandle())
     {
      static int i = 0;
      Print(i+=1);
      update_indicators();

      if(PositionsTotal() == 0)
        {
         position_timer =0;
         find_setup();
        }

      else
         if((PositionsTotal() > 0) && (position_timer < HORIZON))
            position_timer += 1;

         else
            if((PositionsTotal() > 0) && (position_timer >= (HORIZON -1)))
               Trade.PositionClose(Symbol());

      Comment("Position Timer: ",position_timer);
     }
  }

Для поиска оптимальной торговой стратегии нам достаточно получить соответствующие рыночные данные и подготовить их в качестве входных данных для нашей модели ONNX. Обратите внимание, что перед сохранением входных данных в константный вектор типа vectorf мы вычитаем среднее значение каждого столбца и делим на стандартное отклонение столбца. Затем мы передаем этот вектор констант в наш метод ONNXFloat.Predict() и получаем прогноз от нашей модели. Создание этих классов помогло нам значительно сократить общее количество строк кода, которые нам необходимо написать.

//+------------------------------------------------------------------+
//| Find A Trading Setup For Us                                      |
//+------------------------------------------------------------------+
void find_setup(void)
  {
//--- Update our indicators
   update_indicators();

//--- Prepare our input vector
   vectorf market_state(UMAP_INPUTS);

//--- Fill in the Market Data that has to embedded into UMAP form
   market_state[0] = (float) iOpen(_Symbol,SYSTEM_TIMEFRAME_1,0);
   market_state[1] = (float) iHigh(_Symbol,SYSTEM_TIMEFRAME_1,0);
   market_state[2] = (float) iLow(_Symbol,SYSTEM_TIMEFRAME_1,0);
   market_state[3] = (float) iClose(_Symbol,SYSTEM_TIMEFRAME_1,0);
   market_state[4] = (float)(iOpen(_Symbol,SYSTEM_TIMEFRAME_1,0) - iOpen(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
   market_state[5] = (float)(iHigh(_Symbol,SYSTEM_TIMEFRAME_1,0) - iHigh(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
   market_state[6] = (float)(iLow(_Symbol,SYSTEM_TIMEFRAME_1,0) - iLow(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
   market_state[7] = (float)(iClose(_Symbol,SYSTEM_TIMEFRAME_1,0) - iClose(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
   market_state[8] = (float) wpr_array[0].GetReadingAt(0);
   market_state[9] = (float) wpr_array[1].GetReadingAt(0);
   market_state[10] = (float) wpr_array[2].GetReadingAt(0);
   market_state[11] = (float) wpr_array[3].GetReadingAt(0);
   market_state[12] = (float) wpr_array[4].GetReadingAt(0);
   market_state[13] = (float) wpr_array[5].GetReadingAt(0);
   market_state[14] = (float) wpr_array[6].GetReadingAt(0);
   market_state[15] = (float) wpr_array[7].GetReadingAt(0);
   market_state[16] = (float) wpr_array[8].GetReadingAt(0);
   market_state[17] = (float) wpr_array[9].GetReadingAt(0);
   market_state[18] = (float) wpr_array[10].GetReadingAt(0);
   market_state[19] = (float) wpr_array[11].GetReadingAt(0);
   market_state[20] = (float) wpr_array[12].GetReadingAt(0);
   market_state[21] = (float) wpr_array[13].GetReadingAt(0);
   market_state[22] = (float) wpr_array[0].GetDifferencedReadingAt(0);
   market_state[23] = (float) wpr_array[1].GetDifferencedReadingAt(0);
   market_state[24] = (float) wpr_array[2].GetDifferencedReadingAt(0);
   market_state[25] = (float) wpr_array[3].GetDifferencedReadingAt(0);
   market_state[26] = (float) wpr_array[4].GetDifferencedReadingAt(0);
   market_state[27] = (float) wpr_array[5].GetDifferencedReadingAt(0);
   market_state[27] = (float) wpr_array[6].GetDifferencedReadingAt(0);
   market_state[29] = (float) wpr_array[7].GetDifferencedReadingAt(0);
   market_state[30] = (float) wpr_array[8].GetDifferencedReadingAt(0);
   market_state[31] = (float) wpr_array[9].GetDifferencedReadingAt(0);
   market_state[32] = (float) wpr_array[10].GetDifferencedReadingAt(0);
   market_state[33] = (float) wpr_array[11].GetDifferencedReadingAt(0);
   market_state[34] = (float) wpr_array[12].GetDifferencedReadingAt(0);
   market_state[35] = (float) wpr_array[13].GetDifferencedReadingAt(0);

//--- Standardize and scale each input
   for(int i =0; i < UMAP_INPUTS;i++)
     {
      market_state[i] = (float)((market_state[i] - Z1[i]) / Z2[i]);
     };

   const vectorf onnx_inputs = market_state;

   const vectorf umap_predictions = umap_onnx.Predict(onnx_inputs);
   Print("UMAP Model Returned Embeddings: ",umap_predictions);

   const vectorf expected_eurgbp_return = embedded_onnx.Predict(umap_predictions);
   Print("Embeddings Model Expects EURGBP Returns: ",expected_eurgbp_return);
   expected_return = expected_eurgbp_return[0];

   vector o,c;

   o.CopyRates(Symbol(),SYSTEM_TIMEFRAME_1,COPY_RATES_OPEN,0,HORIZON);
   c.CopyRates(Symbol(),SYSTEM_TIMEFRAME_1,COPY_RATES_CLOSE,0,HORIZON);

   bool bullish_reversal   = o.Mean() < c.Mean();
   bool bearish_reversal   = o.Mean() > c.Mean();

   if(bearish_reversal)
     {
      if(expected_return > 0)
        {
         Trade.Buy((TradeInformation.MinVolume()*2),Symbol(),TradeInformation.GetAsk(),0,0,"");
         return;
        }

      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

   else
      if(bullish_reversal)
        {
         if(expected_return < 0)
           {
            Trade.Sell((TradeInformation.MinVolume()*2),Symbol(),TradeInformation.GetBid(),0,0,"");
           }

         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }

  }

Это реализация метода, который мы называем методом обновления наших технических индикаторов.

//+------------------------------------------------------------------+
//| Update our indicator readings                                    |
//+------------------------------------------------------------------+
void update_indicators(void)
  {
//--- Store pointers to our WPR objects
   for(int i = 0; i <= 13; i++)
     {
      //--- Create an WPR object
      wpr_array[i] = new WPR(Symbol(),SYSTEM_TIMEFRAME_1,((i+1) * 5));
      //--- Set the WPR buffers
      wpr_array[i].SetIndicatorValues(60,true);
      wpr_array[i].SetDifferencedIndicatorValues(60,HORIZON,true);
     }
  }

И наконец, всегда помните о необходимости отменить определение созданных вами системных констант в конце программы. 

//+------------------------------------------------------------------+
//| Undefine system constants we no longer need                      |
//+------------------------------------------------------------------+
#undef EMBEDDED_INPUTS
#undef EMBEDDED_OUTPUTS
#undef UMAP_INPUTS
#undef UMAP_OUTPUTS
#undef HORIZON
#undef SYSTEM_TIMEFRAME_1
//+------------------------------------------------------------------+

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

//--- Do no display the indicators, they will clutter our view
   TesterHideIndicators(true);

Рис. 10. Вначале наше представление будет перегружено из-за большого количества используемых нами индикаторов

После этого мы можем приступить к тестированию на исторических данных. Напомним, что наши обучающие выборки охватывали период с ноября 2002 года по август 2018 года; следовательно, период тестирования на исторических данных должен был начаться в сентябре 2018 года и продолжаться до настоящего времени. К сожалению, мое интернет-соединение было нестабильным, и я не смог безопасно загрузить исторические данные от своего брокера. Поэтому мне пришлось проводить тестирование с начала 2023 года по настоящее время. 

Рис. 11. Даты тестирования на истории

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

Рис. 12. Настройки, которые мы использовали для тестирования стратегии, также имеют важное значение

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

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

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

Рис. 14. Визуализация кривой эквити, полученной в результате применения нашей торговой стратегии.

Наконец, мы также можем визуализировать подробный анализ эффективности нашей торговой стратегии. Как мы видим, точность нашей стратегии составила 58% по всем совершенным сделкам, а коэффициент Шарпа — 0,90.

Рис. 15. Подробный анализ эффективности нашей торговой стратегии на данных, которые она ранее не видела



Заключение

Мы рассмотрели преимущества статистического моделирования, выходящие за рамки обычной задачи прогнозирования цен. Мы показали, что:

  1. Машинное обучение можно использовать для управления капиталом: увеличивая размер лота, когда наша модель соответствует торговому сигналу, мы фактически передаем статистической модели контроль над объемом торгов, позволяя машине совершать более крупные сделки, когда она "чувствует себя уверенно". 
  2. Машинное обучение также может быть использовано для выявления более осмысленных способов анализа данных: мы можем использовать семейство алгоритмов машинного обучения, известных как методы уменьшения размерности, для сжатия наших данных, что позволяет нам выявлять важные закономерности в больших наборах данных. 

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

Кроме того, использование предложенного в этой статье алгоритма UMAP дает читателю множество преимуществ по сравнению с такими популярными методами, как PCA (Principal Components Analysis, анализ главных компонентов). Выделим несколько материальных преимуществ:

  1. UMAP — это нелинейный метод: популярные методы уменьшения размерности, такие как PCA, по своей сути предполагают наличие линейной зависимости в данных. Алгоритмы дают сбой, когда это предположение неверно. UMAP, с другой стороны, специально предназначен для поиска нелинейных зависимостей. Читателю не следует считать, что UMAP более "мощный", чем PCA, скорее уместнее сказать, что UMAP более "гибкий", чем PCA.
  2. UMAP — геометрическая, а не евклидова матрица: это означает, что UMAP воспринимает формы, а не только прямые расстояния. В отличие от таких методов, как PCA, которые разрезают данные прямыми линиями, UMAP изгибается вместе с вашими данными. Эта модель не предполагает, что Земля плоская, а скорее, исходит из предположения, что ваши данные находятся на искривленной поверхности, называемой римановым многообразием — концепции из математического исследования топологии, которая помогает описывать сложные нелинейные пространства. Это позволяет UMAP сохранять истинную геометрию ваших данных не путем их сглаживания, а путем движения по ним.

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


Имя файла Описание файла
Use_All_Data.ipynb Jupyter Notebook для анализа рыночных данных.
Fetch_Data_Algorithmic_Input_Selection.mq5 Скрипт MQL5 для получения необходимых рыночных данных.
EURGBP_Multiple_Periods_Analysis.mq5 Созданный нами советник, который одновременно использует 14 различных периодов WPR.
EURGBP_WPR_Algorithmic_Input_Selection.csv Исторические рыночные данные от брокера.
EURGBP_WPR_EMBEDDED.onnx ONNX-модель, отвечающая за аппроксимацию наших 36 столбцов данных до 2 UMAP-встраиваний.
EURGBP_WPR_UMAP.onnx ONNX-модель, отвечающая за прогнозирование доходности рынка EURGBP на основе двух UMAP-вложений.
EURGBP_Multiple_Periods_Analysis.ex5 Скомпилированная версия нашего советника.

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

Нейросети в трейдинге: Адаптивная факторная токенизация (Окончание) Нейросети в трейдинге: Адаптивная факторная токенизация (Окончание)
Статья завершает перенос и интеграцию ключевых компонентов фреймворка MTmixAtt в архитектуру торговой модели для анализа рыночных данных. Продемонстрировано, как адаптивная токенизация и блоки MTmixAttBlock позволяют эффективно выявлять локальные и глобальные паттерны, учитывать сценарии поведения цены.
Разрабатываем мультивалютный советник (Часть 32): Секреты шага создания проекта оптимизации (II) Разрабатываем мультивалютный советник (Часть 32): Секреты шага создания проекта оптимизации (II)
В статье рассматриваются параметры второго этапа конвейера автоматической оптимизации мультивалютного советника. Мы анализируем критерии фильтрации проходов первого этапа и правила формирования групп торговых стратегий. Демонстрируется влияние настроек на результаты оптимизации, обсуждаются аспекты надёжности процесса и баланс между строгостью отбора и достаточностью кандидатов для алгоритма.
Машинное обучение и Data Science (Часть 41): YOLOv8v для поиска паттернов на рынках Forex и акций Машинное обучение и Data Science (Часть 41): YOLOv8v для поиска паттернов на рынках Forex и акций
Выявление графических закономерностей на финансовых рынках представляет собой сложную задачу, поскольку требует анализа данных на графике, что трудно осуществить в MQL5 из-за ограничений, связанных с обработкой изображений. В этой статье мы рассмотрим достойную модель на Python, которая позволит с минимальными усилиями обнаруживать паттерны на графике.
Искусство ведения логов (Часть 7): Как отображать логи на графике Искусство ведения логов (Часть 7): Как отображать логи на графике
Узнайте, как организованно отображать логи прямо на графике MetaTrader, используя рамки, заголовки и автоматическую прокрутку. В этой статье мы показываем, как создать визуальную систему логирования с помощью MQL5, идеально подходящую для отслеживания действий вашего робота в реальном времени.