English Deutsch 日本語
preview
Изучение передовых методов машинного обучения в стратегии пробоя «коридора Дарваса» (Darvas Box Breakout)

Изучение передовых методов машинного обучения в стратегии пробоя «коридора Дарваса» (Darvas Box Breakout)

MetaTrader 5Трейдинг |
100 2
Zhuo Kai Chen
Zhuo Kai Chen

Введение

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

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


Генерация сигналов

Машинное обучение может быть трех основных типов: контролируемое обучение, неконтролируемое обучение и обучение с подкреплением. В алгоритмизированной торговле трейдеры чаще всего предпочитают другим типам контролируемое обучение по двум основным причинам.
  1. Неконтролируемое обучение часто оказывается слишком примитивным, чтобы уловить сложные взаимосвязи между результатами торговли и характеристиками рынка. Без меток оно с трудом справляется с выполнением целей прогнозирования и больше подходит для предсказания косвенных данных, а не прямых результатов торговой стратегии.
  2. Обучение с подкреплением требует создания обучающей среды с функцией вознаграждения, направленной на доведения прибыли до максимума в долгосрочной перспективе, а не сосредоточенной на точных индивидуальных прогнозах. Такой подход подразумевает сложную настройку для простой задачи прогнозирования результатов, что делает его менее рентабельным для розничных трейдеров.

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

Другой подход, который мы рассмотрим в этой статье, заключается в использовании контролируемого обучения для генерации сигналов. Для типичных задач регрессии, таких как прогнозирование цены, это просто: покупайте, если модель прогнозирует рост цены, и продавайте, когда она предсказывает падение. Но как совместить это с такой базовой стратегией, как Darvas Box Breakout?

Сначала разработаем советник для сбора необходимых данных о свойствах и метках для последующего обучения модели на языке Python.

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

double high;
double low;
bool boxFormed = false;

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

Вот несколько примеров «коридора Дарваса» на графике:

Коридор Дарваса

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

input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

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

В качестве сведений о характеристиках, используемых для прогнозирования результатов, я выбрал три последних нормализованных доходности, нормализованное расстояние от максимального и минимального значений диапазона, а также некоторые распространенные стационарные индикаторы. Сохраняем эти данные в многомерном массиве, который затем сохраняется в файле CSV с использованием класса CFileCSV из включенного файла. Убедимся, что все таймфреймы и символы установлены так, как указано ниже, чтобы можно было легко переключаться между таймфреймами и активами.
string data[50000][12];
int indexx = 0;

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator


CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

Окончательный код советника для выборки данных будет выглядеть следующим образом:

#include <Trade/Trade.mqh>
CTrade trade;
#include <FileCSV.mqh>
CFileCSV csvFile;
string fileName = "box.csv";
string headers[] = {
    "Average Directional Movement Index", 
    "Average Directional Movement Index by Welles Wilder",  
    "DeMarker", 
    "Relative Strength Index", 
    "Relative Vigor Index", 
    "Stochastic Oscillator",
    "Stationary",
    "Box Size",
    "Stationary2",
    "Stationary3",
    "Distance High",
    "Distance Low"
};

input double lott = 0.01;
input int Magic = 0;
input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;
input double slp = 0.003;
input double tpp = 0.003;
input bool saveData = true;

string data[50000][12];
int indexx = 0;
int barsTotal = 0;
int count = 0;
double high;
double low;
bool boxFormed = false;
double lastClose;
double lastlastClose;

int handleAdx;     // Average Directional Movement Index - 3
int handleWilder;  // Average Directional Movement Index by Welles Wilder - 3
int handleDm;      // DeMarker - 1
int handleRsi;     // Relative Strength Index - 1
int handleRvi;     // Relative Vigor Index - 2
int handleSto;     // Stochastic Oscillator - 2

int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3
handleWilder=iADXWilder(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index by Welles Wilder - 3
handleDm=iDeMarker(_Symbol,PERIOD_CURRENT,14);//DeMarker - 1
handleRsi=iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);//Relative Strength Index - 1
handleRvi=iRVI(_Symbol,PERIOD_CURRENT,10);//Relative Vigor Index - 2
handleSto=iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_SMA,STO_LOWHIGH);//Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   if (!saveData) return;
   if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI))
     {
      //Write the header
      csvFile.WriteHeader(headers);
      //Write data rows
      csvFile.WriteLine(data);
      //Close the file
      csvFile.Close();
     }
   else
     {
      Print("File opening error!");
     }
  }

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       trade.Buy(lott,_Symbol,ask,sl,tp);
       if(PositionsTotal()>0)getData();
}

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

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

пример сделки

В качестве обучающих и проверочных данных используем данные за 2020-2024 годы, а позднее протестируем результат на данных за 2024-2025 годы в терминале MetaTrader 5. После запуска этого советника в тестере стратегий CSV-файл будет сохранен в каталоге /Tester/Agent-sth000 после деинициализации советника.

Кроме того, щелкнем правой клавишей мыши, чтобы получить отчет о тестировании на исторических данных в Excel, как показано ниже:

отчет в Excel

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

найти строку

После этого обучим модель на языке Python.

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

import pandas as pd

# Replace 'your_file.xlsx' with the path to your file
input_file = 'box.xlsx'

# Load the Excel file and skip the first {skiprows} rows
data1 = pd.read_excel(input_file, skiprows=4417)

# Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions
profit_data = data1["Profit"][1:-1] 
profit_data = profit_data[profit_data.index % 2 == 0]  # Filter for rows with odd indices
profit_data = profit_data.reset_index(drop=True)  # Reset index
# Convert to float, then apply the condition to set values to 1 if > 0, otherwise to 0
profit_data = pd.to_numeric(profit_data, errors='coerce').fillna(0)  # Convert to float, replacing NaN with 0
profit_data = profit_data.apply(lambda x: 1 if x > 0 else 0)  # Apply condition

# Load the CSV file with semicolon separator
file_path = 'box.csv'
data2 = pd.read_csv(file_path, sep=';')

# Drop rows with any missing or incomplete values
data2.dropna(inplace=True)

# Drop any duplicate rows if present
data2.drop_duplicates(inplace=True)

# Convert non-numeric columns to numerical format
for col in data2.columns:
    if data2[col].dtype == 'object':
        # Convert categorical to numerical using label encoding
        data2[col] = data2[col].astype('category').cat.codes

# Ensure all remaining columns are numeric and cleanly formatted for CatBoost
data2 = data2.apply(pd.to_numeric, errors='coerce')
data2.dropna(inplace=True)  # Drop any rows that might still contain NaNs after conversion

# Merge the two DataFrames on the index
merged_data = pd.merge(profit_data, data2, left_index=True, right_index=True, how='inner')

# Save the merged data to a new CSV file
output_csv_path = 'merged_data.csv'
merged_data.to_csv(output_csv_path)

print(f"Merged data saved to {output_csv_path}")
Используем этот код для маркировки отчета в Excel, присваивая 1 сделкам с положительной прибылью и 0 — сделкам без нее. Затем объединим его с данными о свойствах, собранными из CSV-файла советника, извлекающего данные. Имейте в виду, что значение skiprow соответствует номеру строки в «Сделках».
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
data = pd.read_csv("merged_data.csv",index_col=0)

XX = data.drop(columns=['Profit'])
yy = data['Profit']
y = yy.values
X = XX.values
pd.DataFrame(X,y)

Затем присваиваем массив меток переменной y, а фрейм данных свойств — переменной X.

import numpy as np
import pandas as pd
import warnings
import seaborn as sns
warnings.filterwarnings("ignore")
from sklearn.model_selection import train_test_split
import catboost as cb
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, shuffle=False)

# Identify categorical features
cat_feature_indices = [i for i, col in enumerate(XX.columns) if XX[col].dtype == 'object']

# Train CatBoost classifier
model = cb.CatBoostClassifier(   
    iterations=5000,             # Number of trees (similar to n_estimators)
    learning_rate=0.02,          # Learning rate
    depth=5,                    # Depth of each tree
    l2_leaf_reg=5,
    bagging_temperature=1,
    early_stopping_rounds=50,
    loss_function='Logloss',    # Use 'MultiClass' if it's a multi-class problem
    verbose=1000)
model.fit(X_train, y_train, cat_features=cat_feature_indices)

Затем разделим данные в соотношении 9:1 на обучающий и проверочный наборы и начнем обучение модели. Настройка по умолчанию для функции разделения обучающих и тестовых данных в sklearn включает в себя перемешивание, что не идеально для данных временных рядов, поэтому обязательно задайте параметр shuffle=False в параметрах. Хорошая идея — настроить гиперпараметры, чтобы избежать переобучения или недообучения в зависимости от размера выборки. Лично я обнаружил, что прекращение итераций при значении потери логарифма около 0.1 работает хорошо.

import numpy as np
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Assuming you already have y_test, X_test, and model defined
# Predict probabilities
y_prob = model.predict_proba(X_test)[:, 1]  # Probability for positive class

# Compute ROC curve and AUC (for reference)
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc_score = roc_auc_score(y_test, y_prob)
print(f"AUC Score: {auc_score:.2f}")

# Define confidence thresholds to test (e.g., 50%, 60%, 70%, etc.)
confidence_thresholds = np.arange(0.5, 1.0, 0.05)  # From 50% to 95% in steps of 5%
accuracies = []
coverage = []  # Fraction of samples classified at each threshold

for thresh in confidence_thresholds:
    # Classify only when probability is >= thresh (positive) or <= (1 - thresh) (negative)
    y_pred_confident = np.where(y_prob >= thresh, 1, np.where(y_prob <= (1 - thresh), 0, -1))
    
    # Filter out unclassified samples (where y_pred_confident == -1)
    mask = y_pred_confident != -1
    y_test_confident = y_test[mask]
    y_pred_confident = y_pred_confident[mask]
    
    # Calculate accuracy and coverage
    if len(y_test_confident) > 0:  # Avoid division by zero
        acc = np.mean(y_pred_confident == y_test_confident)
        cov = len(y_test_confident) / len(y_test)
    else:
        acc = 0
        cov = 0
    
    accuracies.append(acc)
    coverage.append(cov)

# Plot Accuracy vs Confidence Threshold
plt.figure(figsize=(10, 6))
plt.plot(confidence_thresholds, accuracies, marker='o', label='Accuracy', color='blue')
plt.plot(confidence_thresholds, coverage, marker='s', label='Coverage', color='green')
plt.xlabel('Confidence Threshold')
plt.ylabel('Metric Value')
plt.title('Accuracy and Coverage vs Confidence Threshold')
plt.legend(loc='best')
plt.grid(True)
plt.show()

# Also show the original ROC curve for reference
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {auc_score:.2f})', color='blue')
plt.plot([0, 1], [0, 1], 'k--', label='Random Guess')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc='lower right')
plt.grid(True)
plt.show()

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

доверительный порог точности

Показатель AUC

Здесь мы замечаем, что показатель AUC превышает 0.5, а точность повышается по мере повышения доверительного порога, что обычно является положительным признаком. Если два этих измерения не соответствуют друг другу, не паникуйте, попробуйте сначала скорректировать гиперпараметры, прежде чем полностью отказываться от модели.
# Feature importance
feature_importance = model.get_feature_importance()
importance_df = pd.DataFrame({
    'feature': XX.columns,
    'importance': feature_importance
}).sort_values('importance', ascending=False)
print("Feature Importances:")
print(importance_df)
plt.figure(figsize=(10, 6))
sns.barplot(x='importance', y='feature', data=importance_df)
plt.title(' Feature Importance')
plt.xlabel('Importance')
plt.ylabel('Feature')
x = 100/len(XX.columns)
plt.axvline(x,color = 'red', linestyle = '--')
plt.show()

значимость свойства

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

  1. Значимость на основе дерева: измеряет уменьшение засоренности (например, индекс Джини) в деревьях решений или ансамблях, таких как Random Forest и XGBoost.
  2. Значимость перестановки: оценивает падение производительности при перестановке значений свойства.
  3. Значения SHAP: рассчитывает вклад свойства в прогнозы на основе значений Шепли.
  4. Величина коэффициента: использует абсолютное значение коэффициентов в линейных моделях.

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

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

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

from onnx.helper import get_attribute_value
import onnxruntime as rt
from skl2onnx import convert_sklearn, update_registered_converter
from skl2onnx.common.shape_calculator import (
    calculate_linear_classifier_output_shapes,
)  # noqa
from skl2onnx.common.data_types import (
    FloatTensorType,
    Int64TensorType,
    guess_tensor_type,
)
from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name
from catboost import CatBoostClassifier
from catboost.utils import convert_to_onnx_object

def skl2onnx_parser_castboost_classifier(scope, model, inputs, custom_parsers=None):
    
    options = scope.get_options(model, dict(zipmap=True))
    no_zipmap = isinstance(options["zipmap"], bool) and not options["zipmap"]

    alias = _get_sklearn_operator_name(type(model))
    this_operator = scope.declare_local_operator(alias, model)
    this_operator.inputs = inputs

    label_variable = scope.declare_local_variable("label", Int64TensorType())
    prob_dtype = guess_tensor_type(inputs[0].type)
    probability_tensor_variable = scope.declare_local_variable(
        "probabilities", prob_dtype
    )
    this_operator.outputs.append(label_variable)
    this_operator.outputs.append(probability_tensor_variable)
    probability_tensor = this_operator.outputs

    if no_zipmap:
        return probability_tensor

    return _apply_zipmap(
        options["zipmap"], scope, model, inputs[0].type, probability_tensor
    )

def skl2onnx_convert_catboost(scope, operator, container):
    """
    CatBoost returns an ONNX graph with a single node.
    This function adds it to the main graph.
    """
    onx = convert_to_onnx_object(operator.raw_operator)
    opsets = {d.domain: d.version for d in onx.opset_import}
    if "" in opsets and opsets[""] >= container.target_opset:
        raise RuntimeError("CatBoost uses an opset more recent than the target one.")
    if len(onx.graph.initializer) > 0 or len(onx.graph.sparse_initializer) > 0:
        raise NotImplementedError(
            "CatBoost returns a model initializers. This option is not implemented yet."
        )
    if (
        len(onx.graph.node) not in (1, 2)
        or not onx.graph.node[0].op_type.startswith("TreeEnsemble")
        or (len(onx.graph.node) == 2 and onx.graph.node[1].op_type != "ZipMap")
    ):
        types = ", ".join(map(lambda n: n.op_type, onx.graph.node))
        raise NotImplementedError(
            f"CatBoost returns {len(onx.graph.node)} != 1 (types={types}). "
            f"This option is not implemented yet."
        )
    node = onx.graph.node[0]
    atts = {}
    for att in node.attribute:
        atts[att.name] = get_attribute_value(att)
    container.add_node(
        node.op_type,
        [operator.inputs[0].full_name],
        [operator.outputs[0].full_name, operator.outputs[1].full_name],
        op_domain=node.domain,
        op_version=opsets.get(node.domain, None),
        **atts,
    )

update_registered_converter(
    CatBoostClassifier,
    "CatBoostCatBoostClassifier",
    calculate_linear_classifier_output_shapes,
    skl2onnx_convert_catboost,
    parser=skl2onnx_parser_castboost_classifier,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)
model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

# And save.
with open("box2024.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

Наконец, экспортируем файл ONNX и сохраним его в каталоге MQL5/Files.

Теперь вернемся к редактору кода MetaTrader 5 для создания торгового советника.

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

#resource "\\Files\\box2024.onnx" as uchar catboost_onnx[]
#include <CatOnnx.mqh>
CCatBoost cat_boost;
string data[1][12];
vector xx;
vector prob;

Затем настроим функцию getData() так, чтобы она возвращала вектор.

vector getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[0][6] = DoubleToString(stationary,2);
data[0][7] = DoubleToString(boxSize,2);
data[0][8] = DoubleToString(stationary2,2);
data[0][9] = DoubleToString(stationary3,2);
data[0][10] = DoubleToString(highDistance,2);
data[0][11] = DoubleToString(lowDistance,2);

vector features(12);    
   for(int i = 0; i < 12; i++)
    {
      features[i] = StringToDouble(data[0][i]);
    }
    return features;
}

Окончательная торговая логика в функции OnTick() будет выглядеть следующим образом:

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        prob = cat_boost.predict_proba(xx);
        if(prob[1]>threshold)executeBuy(); 
        if(prob[0]>threshold)executeSell();
        }
    }
 }

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

Мы провели тестирование на исторических данных в тестере стратегий MetaTrader 5, используя внутривыборочные данные за период с 2020 по 2024 год, чтобы подтвердить, что наши обучающие данные не содержат ошибок и что свойства и результаты объединяются корректно. Если все верно, кривая капитала должна выглядеть практически идеально, вот так:

внутри выборки

Затем проводим тестирование на истории за пределами выборки за период с 2024 по 2025 год, чтобы проверить, является ли стратегия прибыльной в самое последнее время. Установим порог на 0.7, чтобы модель открывала сделку в каком-либо направлении только в том случае, если уровень достоверности достижения тейк-профита в этом направлении составляет 70% и выше, исходя из обучающих данных.

настройка тестирования на исторических данных (дискретная)

параметры

кривая капитала (дискретная)

результат (дискретный)

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


Непрерывный сигнал

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

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

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

double calclots(double slpoints, string symbol, double risk)
{  
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskAmount = balance* risk / 100;

   double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickvalue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   double lotstep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

   double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
   double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
   lots = MathMin(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX));
   lots = MathMax(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN));
   return lots;
}
Затем обновим функции покупки и продажи таким образом, чтобы они вызывали эту функцию calclots() и принимали в качестве входных данных фактор риска:  
void executeSell(double riskMultiplier) {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = lastClose*(1+slp);
       double tp = lastClose*(1-tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Sell(lots,_Symbol,bid,sl,tp);  
       }

void executeBuy(double riskMultiplier) {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Buy(lots,_Symbol,ask,sl,tp);
}
Поскольку наша модель машинного обучения уже выводит уровень достоверности, мы можем непосредственно использовать его в качестве входных данных фактора риска. Если мы хотим отрегулировать степень влияния уровня достоверности на риск для каждой сделки, мы можем просто увеличивать или уменьшать степень достоверности по мере необходимости.
if(prob[1]>threshold)executeBuy(prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]);

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

if(prob[1]>threshold)executeBuy(prob[1]*prob[1]*prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]*prob[0]*prob[0]);

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

настройка тестирования на истории (непрерывная)

кривая капитала(непрерывная)

результат(непрерывный)

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


Проверка на нескольких таймфреймах

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

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

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

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

int OnInit()
{
   trade.SetExpertMagicNumber(Magic);
   handleAdx = iADX(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index - 3
   handleWilder = iADXWilder(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index by Welles Wilder - 3
   handleDm = iDeMarker(_Symbol, PERIOD_H1, 14); // DeMarker - 1
   handleRsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE); // Relative Strength Index - 1
   handleRvi = iRVI(_Symbol, PERIOD_H1, 10); // Relative Vigor Index - 2
   handleSto = iStochastic(_Symbol, PERIOD_H1, 5, 3, 3, MODE_SMA, STO_LOWHIGH); // Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
}

void getData()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
}

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

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

vector getData2()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
   double highDistance = 1000 * (close - high) / close;
   double lowDistance = 1000 * (close - low) / close;
   double boxSize = 1000 * (high - low) / close;
   double adx[];       // Average Directional Movement Index
   double wilder[];    // Average Directional Movement Index by Welles Wilder
   double dm[];        // DeMarker
   double rsi[];       // Relative Strength Index
   double rvi[];       // Relative Vigor Index
   double sto[];       // Stochastic Oscillator

   CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
   CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
   CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
   CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
   CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
   CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

   data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
   data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
   data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
   data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
   data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
   data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
   data[0][6] = DoubleToString(stationary, 2);
   data[0][7] = DoubleToString(boxSize, 2);
   data[0][8] = DoubleToString(stationary2, 2);
   data[0][9] = DoubleToString(stationary3, 2);
   data[0][10] = DoubleToString(highDistance, 2);
   data[0][11] = DoubleToString(lowDistance, 2);

   vector features(12);    
   for(int i = 0; i < 12; i++)
   {
      features[i] = StringToDouble(data[0][i]);
   }
   return features;
}

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

if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        xx2 = getData2();
        prob = cat_boost.predict_proba(xx);
        prob2 = cat_boost.predict_proba(xx2);
        double probability_buy = (prob[1]+prob2[1])/2;
        double probability_sell = (prob[0]+prob2[0])/2;

        if(probability_buy>threshold)executeBuy(probability_buy); 
        if(probability_sell>threshold)executeSell(probability_sell);
        }
    }

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


Заключение

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

В целом, целью данной статьи является представление нетрадиционных идей применения моделей машинного обучения при контролируемом обучении для торговли с помощью CTA (Commodity Trading Advisor). Кроме того, цель ее не предполагает однозначно утверждать, какой именно подход работает лучше всего, поскольку все зависит от конкретной ситуации, а скорее вдохновить читателей мыслить творчески и расширять простые исходные концепции. В конце концов, ничто не является абсолютно новым — инновации часто происходят из объединения существующих идей для создания чего-то необычного.

Таблица файлов

Название файла Использование файла
Darvas_Box.ipynb Файл Jupyter Notebook для тренировки модели машинного обучения
Darvas Box Data.mq5 Советник для извлечения данных с целью обучения модели
Darvas Box EA.mq5 Торговый советник в статье
CatOnnx.mqh Включаемый файл для обработки модели CatBoost model
FileCSV.mqh Включаемый файл для сохранения данных в CSV

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

Прикрепленные файлы |
Darvas_ML.zip (136.61 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
linfo2
linfo2 | 24 мар. 2025 в 00:42
Спасибо Чжоу за интересную статью и примеры кода. Мне пришлось вручную установить некоторые компоненты Python, чтобы заставить его работать. что может помочь другим пользователям !pip install catboost!pip install onnxruntime !pip install skl2onnx. по завершении я могу протестировать. но если я попытаюсь загрузить связанный советник, я получил ответ "Не удалось установить форму Output[1] Err=5802. Я не уверен, откуда это взялось и важно ли это, и я не могу понять, откуда это взялось. . в документации сказано ERR_ONNX_NOT_SUPPORTED

5802

Свойство или значение не поддерживается MQL5, за этим следует сообщение ONNX Model Initialised? Есть ли у вас какие-либо предложения?
Zhuo Kai Chen
Zhuo Kai Chen | 24 мар. 2025 в 01:09
linfo2 catboost!pip install onnxruntime !pip install skl2onnx. по завершении я могу протестировать. но если я попытаюсь загрузить связанный советник, я получил ответ "Не удалось установить форму Output[1] Err=5802. Я не уверен, откуда это взялось и важно ли это, и я не могу понять, откуда это взялось. . в документации сказано ERR_ONNX_NOT_SUPPORTED

5802

Свойство или значение не поддерживается MQL5, за этим следует сообщение ONNX Model Initialised ? Есть ли у вас какие-либо предложения

Спасибо за напоминание. Часть pip install была проигнорирована, но пользователи должны установить соответствующую библиотеку, если они этого еще не сделали.

Ваша ошибка может быть вызвана тем, что размерность, используемая при обучении модели, отличается от размерности, используемой в вашем советнике. Например, если вы обучали модель с 5 признаками, то и в советнике вы должны вводить 5 признаков, а не 4 или 6. Более подробный обзор приведен в этой статье по ссылке. Надеюсь, это поможет. Если нет, пожалуйста, предоставьте больше информации.

От новичка до эксперта: Система автогеометрического анализа От новичка до эксперта: Система автогеометрического анализа
Геометрические паттерны предлагают трейдерам лаконичный способ интерпретации ценового движения. Многие аналитики рисуют линии тренда, прямоугольники и другие фигуры вручную, а затем основывают торговые решения на тех формациях, которые они видят. В настоящей статье мы рассмотрим автоматизированную альтернативу: использование MQL5 для обнаружения и анализа наиболее популярных геометрических паттернов. Мы разберем методологию, обсудим детали реализации и расскажем о том, как автоматическое распознавание паттернов может улучшить понимание рынка трейдером.
Торговый инструментарий MQL5 (Часть 5): Расширение EX5-библиотеки для управления историей с помощью функций позиции Торговый инструментарий MQL5 (Часть 5): Расширение EX5-библиотеки для управления историей с помощью функций позиции
В этой статье мы узнаем, как создавать экспортируемые EX5-функции для эффективного запроса и сохранения исторических данных о позициях. В этом пошаговом руководстве мы расширим EX5-библиотеку для управления историей (History Management), разработав модули, которые извлекают ключевые свойства последней закрытой позиции. К ним относятся чистая прибыль, продолжительность сделки, стоп-лосс и тейк-профит в пипсах, значения прибыли и другие важные данные.
Возможности Мастера MQL5, которые вам нужно знать (Часть 51): Обучение с подкреплением с помощью SAC Возможности Мастера MQL5, которые вам нужно знать (Часть 51): Обучение с подкреплением с помощью SAC
Soft Actor Critic (мягкий актер-критик) — это алгоритм обучения с подкреплением, использующий три нейронные сети — сеть актеров и две сети критиков. Такие модели машинного обучения объединены в партнерство "главный-подчиненный", где критики моделируются для повышения точности прогнозов сети актеров. Как обычно, рассмотрим, как эти идеи можно протестировать в качестве пользовательского сигнала советника, собранного с помощью Мастера.
От новичка до эксперта: Программирование японских свечей От новичка до эксперта: Программирование японских свечей
В настоящей статье сделаем первый шаг в программировании на MQL5, даже для совсем новичков. Мы покажем вам, как преобразовать знакомые свечные паттерны в полнофункциональный пользовательский индикатор. Свечные паттерны ценны тем, что они отражают реальное движение цены и сигнализируют о сдвигах на рынке. Вместо ручного сканирования графиков — подхода, чреватого ошибками и неэффективностью, — мы обсудим, как автоматизировать этот процесс с помощью индикатора, идентифицирующего и помечающего паттерны для вас. Попутно рассмотрим такие ключевые понятия, как индексация, временные ряды, средний истинный диапазон (для обеспечения точности при различной волатильности рынка), а также разработку пользовательской библиотеки свечных паттернов для многократного использования в будущих проектах.