English Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 37): Использование моделей свечных графиков и ИИ в трейдинге

Машинное обучение и Data Science (Часть 37): Использование моделей свечных графиков и ИИ в трейдинге

MetaTrader 5Торговые системы |
94 6
Omega J Msigwa
Omega J Msigwa

Содержание



Введение

Первая стратегия, которую я когда-либо использовал в торговле, была основана на свечных графиках. Сейчас я бы не назвал это стратегией, но моя первая сделка была открыта благодаря некоторым свечным паттернам, о которых я узнал из книги "Библия свечного трейдинга" Хоммы Мунехисы, которую мне посоветовал друг.

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

Эта концепция зародилась в 1700-х годах благодаря торговцу Хомме Мунехисе, который считается самым успешным трейдером в истории. Известный в свое время как "бог рынков", благодаря своим открытиям он заработал более 10 миллиардов долларов в пересчете на сегодняшние деньги. 

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

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


Устройство свечи

Тени или фитили свечей показывают дневные максимумы и минимумы цен, а также их соотношение с ценами открытия и закрытия. Форма свечи меняется в зависимости от соотношения цен открытия, максимума, минимума и закрытия (Open, High, Low и Close, OHLC). Существует множество моделей свечного анализа, предложенных Мунехисой в свое время, а некоторые из них появились в торговле совсем недавно.

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


Свечные паттерны

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

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

Формат кодирования для CTALib основан на TALib (Technical Analysis Library) - библиотеке технического анализа на языках C#, C++ и Python, которая включает в себя несколько функций для обнаружения свечных паттернов.

Белая свеча

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

Мы можем реализовать ее в коде так.

bool CTALib::CDLWHITECANDLE(double open, double close)
 {
   return (close > open);
 }

Черная свеча

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

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

bool CTALib::CDLBLACKCANDLE(double open,double close)
 {
   return (open > close);
 }

Дожи

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

Существуют несколько вариаций, как показано на изображении ниже.

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

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

bool CTALib::CDLDOJI(double open,double close, double torrelance=3)
 {
   return (fabs(open-close)<torrelance*_Point); //A torrelance in market points
 }

Дожи-стрекоза

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

дожи-стрекоза

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

bool CTALib::CDLDRAGONFLYDOJI(double open, 
                              double high, 
                              double low, 
                              double close, 
                              double body_torrelance = 3, 
                              double shadow_ratio = 2.0)
{
   double body_size    = MathAbs(open - close);
   double upper_shadow = upperShadowCalc(open, close, high);
   double lower_shadow = lowerShadowCalc(open, close, low);

   //--- Body is very small (like a Doji)
   if (CDLDOJI(open, close, body_torrelance))
   {
      //--- Lower shadow is significantly longer than upper shadow
      if (lower_shadow > upper_shadow * shadow_ratio)
         return true;
   }

   return false;
}

Дожи-надгробие

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

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

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

bool CTALib::CDLGRAVESTONEDOJI(double open, 
                               double high, 
                               double low, 
                               double close, 
                               double body_torrelance = 3, 
                               double shadow_ratio = 2.0)
{
   double body_size    = MathAbs(open - close);
   double upper_shadow = upperShadowCalc(open, close, high);
   double lower_shadow = lowerShadowCalc(open, close, low);

   //--- Body is very small (like a Doji)
   if (CDLDOJI(open, close, body_torrelance))
   {
      //--- Lower shadow is significantly longer than upper shadow
      if (upper_shadow > lower_shadow * shadow_ratio)
         return true;
   }

   return false;
}

Молот

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

Свечной паттерн "молот"

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

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

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

bool CTALib::CDLHAMMER(double open, 
                       double high, 
                       double low, 
                       double close, 
                       double min_body_percentage = 0.2,       // To avoid being a doji
                       double lower_shadow_ratio = 2.0,        // Lower shadow at least 2x the body
                       double upper_shadow_max_ratio = 0.3)    // Upper shadow must be small
{
   double body_size      = MathAbs(open - close);
   double total_range    = high - low + DBL_EPSILON;
   double upper_shadow   = upperShadowCalc(open, close, high);
   double lower_shadow   = lowerShadowCalc(open, close, low);
   double body_percentage = body_size / total_range;

   return (
      body_percentage >= min_body_percentage &&
      lower_shadow >= lower_shadow_ratio * body_size &&
      upper_shadow <= upper_shadow_max_ratio * body_size
   );
}

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

Перевернутый молот

Похож на молот, но с небольшим основанием, длинной верхней тенью и маленькой нижней тенью.

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

перевернутый молот

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

bool CTALib::CDLINVERTEDHAMMER(double open, 
                               double high, 
                               double low, 
                               double close, 
                               double min_body_percentage = 0.2,        // Avoid doji
                               double upper_shadow_ratio = 2.0,         // Upper shadow must be long
                               double lower_shadow_max_ratio = 0.3)     // Lower shadow should be small
{
   double body_size        = MathAbs(open - close);
   double total_range      = high - low + DBL_EPSILON;
   double upper_shadow     = upperShadowCalc(open, close, high);
   double lower_shadow     = lowerShadowCalc(open, close, low);
   double body_percentage  = body_size / total_range;

   return (
      body_percentage >= min_body_percentage &&
      upper_shadow >= upper_shadow_ratio * body_size &&
      lower_shadow <= lower_shadow_max_ratio * body_size
   );
}

Волчок

Это свеча с небольшим телом в центре и длинными тенями по бокам.

свечной паттерн "волчок"

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

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

bool CTALib::CDLSPINNINGTOP(double open,
                            double high,
                            double low,
                            double close, 
                            double body_percentage_threshold = 0.3, 
                            double shadow_ratio = 2.0,
                            double shadow_symmetry_tolerance = 0.3)
{
   double body_size      = MathAbs(open - close);
   double total_range    = high - low + DBL_EPSILON;
   double upper_shadow   = upperShadowCalc(open, close, high);
   double lower_shadow   = lowerShadowCalc(open, close, low);
   double body_percentage = body_size / total_range;

   //--- Calculate shadow symmetry ratio
   double shadow_diff = MathAbs(upper_shadow - lower_shadow);
   double shadow_sum = upper_shadow + lower_shadow + DBL_EPSILON;
   double symmetry_ratio = shadow_diff / shadow_sum; // Closer to 0 = more balanced

   return (
      body_percentage < body_percentage_threshold && // Body is small compared to candle size
      upper_shadow > body_size * shadow_ratio && // Both shadows are significantly larger than the body
      lower_shadow > body_size * shadow_ratio &&
      symmetry_ratio <= shadow_symmetry_tolerance //Shadows are roughly equal (symmetrical)
   );
}

Бычий марубозу

"Марубозу" переводится с японского как "остриженный", что указывает на свечу без тени.

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

бычий марубозу

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

bool CTALib::CDLBULLISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2)
 {
   return (MathAbs(open - low) <= (tolerance*_Point) &&  MathAbs(close - high) <= (tolerance*_Point) && close > open);
 }

Медвежий марубозу

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

свечной паттерн "медвежий марубозу"

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

bool CTALib::CDLBEARISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2)
 {
   return (MathAbs(open - high) <= (tolerance*_Point) && MathAbs(close - low) <= (tolerance*_Point) && close < open);
 }

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

Тренды — это важнейший фактор, который следует учитывать.


Индикатор обнаружения cвечных паттернов

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

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

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

Индикатор, основанный на свечных графиках, будет иметь 5 буферов и одно отображение на основном графике.

Имя файла: Candlestick Identifier.mq5

#property indicator_chart_window
#property indicator_buffers 5
#property indicator_plots 1

#property indicator_type1 DRAW_COLOR_CANDLES
#property indicator_color1 clrDodgerBlue, clrOrange, clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

double OpenBuff[];
double HighBuff[];
double LowBuff[];
double CloseBuff[];
double ColorBuff[];

#include <ta-lib.mqh> //!important for candlestick patterns

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
   if (rates_total<1)
     return rates_total;
   
    for(int i = prev_calculated; i < rates_total; i++)
     {
      OpenBuff[i]  = open[i];
      HighBuff[i]  = high[i];
      LowBuff[i]   = low[i];
      CloseBuff[i] = close[i];
      
      //---
      
      if (close[i]>open[i])
         ColorBuff[i] = 1.0; 
      else
         ColorBuff[i] = 0.0;
      
      //---
      
      double padding = MathAbs(high[i] - low[i]) * 0.2; // 20% padding
      
      if (CTALib::CDLDOJI(open[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding, "Doji", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLDRAGONFLYDOJI(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"DragonFly Doji", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLGRAVESTONEDOJI(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"GraveStone Doji", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLHAMMER(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Hammer", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLINVERTEDHAMMER(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Inverted Hammer", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLSPINNINGTOP(open[i], high[i], low[i], close[i], 0.3, 2.0))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Spinning Top", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLBULLISHMARUBOZU(open[i], high[i], low[i], close[i], 2))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bullish Marubozu", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLBEARISHMARUBOZU(open[i], high[i], low[i], close[i], 2))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bearish Marubozu", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
     }
     
//--- return value of prev_calculated for next call
   return(rates_total);
  }

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

Индикатор свечных паттернов

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

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


Сбор свечных паттернов для машинного обучения

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

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

#include <ta-lib.mqh> //Contains CTALib class for candlestick patterns detection
#include <MALE5\Pandas\pandas.mqh> //https://www.mql5.com/en/articles/17030

input datetime start_date = D'2005.01.01';
input datetime end_date = D'2023.01.01';

input string symbol = "XAUUSD";
input ENUM_TIMEFRAMES timeframe = PERIOD_D1;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
   vector open, high, low, close;
   
   open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date);
   high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date);
   low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date);
   close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date);
   
   CDataFrame df;
   
   vector cdl_patterns = {};
   cdl_patterns = CTALib::CDLWHITECANDLE(open, close);
   df.insert("White Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLBLACKCANDLE(open, close);
   df.insert("Black Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLDOJI(open, close);
   df.insert("Doji Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLDRAGONFLYDOJI(open, high, low, close);
   df.insert("Dragonflydoji Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLGRAVESTONEDOJI(open, high, low, close);
   df.insert("Gravestonedoji Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLHAMMER(open, high, low, close);
   df.insert("Hammer Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLINVERTEDHAMMER(open, high, low, close);
   df.insert("Invertedhammer Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLSPINNINGTOP(open, high, low, close);
   df.insert("Spinningtop Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLBULLISHMARUBOZU(open, high, low, close);
   df.insert("BullishMarubozu Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLBEARISHMARUBOZU(open, high, low, close);
   df.insert("BearishMarubozu Candle", cdl_patterns);
   
   df.insert("Open", open);
   df.insert("High", high);
   df.insert("Low", low);
   df.insert("Close", close);
   
   df.to_csv(StringFormat("CandlestickPatterns.%s.%s.csv",symbol,EnumToString(timeframe)), true);
  }

Мы также собираем значения OHLC (Open, High, Low и Close) для подготовки целевой переменной, а также на случай, если что-то случится и они нам понадобятся.


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

Теперь, когда у нас есть набор данных, давайте загрузим эти данные в скрипт Python (Jupyter Notebook).

import pandas as pd

symbol = "XAUUSD"
df = pd.read_csv(f"/kaggle/input/forex-candlestick-patterns/CandlestickPatterns.{symbol}.PERIOD_D1.csv")

df

Выходные параметры

Белая свеча Черная свеча Дожи Стрекоза Надгробие Молот Перевернутый молот Волчок Бычий марубозу Медвежий марубозу Open High Low Close
0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 438.45 438.71 426.72 429.55
1 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 429.52 430.18 423.71 427.51
2 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 427.50 428.77 425.10 426.58
3 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 426.31 427.85 420.17 421.37
4 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 421.39 425.48 416.57 419.02
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...


Самая большая сложность при работе с данными в виде свечей заключается в создании целевой переменной.

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

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

Если Close[следующий бар] > Close[текущий бар], это указывает на бычий тренд, поэтому мы присваиваем целевой уровень 1.

В противном случае, если это условие не выполняется, это указывает на медвежье движение, поэтому мы присваиваем метку 0.

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

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

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

Другой способ решения этой проблемы — введение сигнала класса hold, обозначаемого -1 во всех строках, где все паттерны свечей равны 0 (false) за исключением столбцов белых и черных свечей, однако это приведет к серьезной проблеме дисбаланса классов, которую мы уже рассматривали в предыдущей статье, однако при этом такой подход не смог решить проблему.

А пока давайте приступим к подготовке целевой переменной.

lookahead = 1

new_df = df.copy()
new_df["future_close"] = new_df["Close"].shift(-lookahead)
new_df.dropna(inplace=True)  # Drop NaNs caused by the shift operation

signal = []
for i in range(len(new_df)):  # Iterate over rows, not columns
    if new_df["future_close"].iloc[i] > new_df["Close"].iloc[i]:
        signal.append(1)
    else:
        signal.append(0)

new_df["Signal"] = signal

Затем мы разделили предикторы на двумерный массив X, при этом отбрасываются ненужные признаки, такие как значения OHLC, столбец, который мы хотим предсказать (целевой), и функция future_close, которую мы использовали для определения целевого столбца. Мы также назначаем целевой столбец Signal массиву y.

X = new_df.drop(columns=[
    "Signal",
    "Open",
    "High",
    "Low",
    "Close",
    "future_close"
])

y = new_df["Signal"]

# Split data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, shuffle=False)

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

from catboost import CatBoostClassifier
from sklearn.utils.class_weight import compute_class_weight

# Automatically calculate class weights
classes = np.unique(y)
weights = compute_class_weight(class_weight='balanced', classes=classes, y=y)
class_weights = dict(zip(classes, weights))

# Define the base model
model = CatBoostClassifier(
    iterations=1000,
    learning_rate=0.01,
    depth=5,
    loss_function='Logloss',
    class_weights=class_weights,
    verbose=100
)

model.fit(X_train, y_train) # Training the classifier

Результаты.

0:      learn: 0.6930586        total: 3.64ms   remaining: 3.64s
100:    learn: 0.6897625        total: 136ms    remaining: 1.21s
200:    learn: 0.6888030        total: 269ms    remaining: 1.07s
300:    learn: 0.6883559        total: 401ms    remaining: 931ms
400:    learn: 0.6881469        total: 532ms    remaining: 795ms
500:    learn: 0.6879966        total: 661ms    remaining: 658ms
600:    learn: 0.6879013        total: 789ms    remaining: 524ms
700:    learn: 0.6878311        total: 916ms    remaining: 391ms
800:    learn: 0.6877729        total: 1.04s    remaining: 260ms
900:    learn: 0.6877273        total: 1.17s    remaining: 129ms
999:    learn: 0.6876900        total: 1.3s     remaining: 0us
<catboost.core.CatBoostClassifier at 0x798cc6d08dd0>

Давайте оценим эту модель на данных, которые она ранее не видела (тестовая выборка).

y_pred = model.predict(X_test)

print("\nClassification Report:\n", classification_report(y_test, y_pred))

Результаты.

Classification Report:
               precision    recall  f1-score   support

           0       0.49      0.55      0.52       429
           1       0.58      0.52      0.55       511

    accuracy                           0.53       940
   macro avg       0.53      0.53      0.53       940
weighted avg       0.54      0.53      0.53       940

Результаты показывают, что средняя точность модели составляет 0,58 и 0,49 для классов 1 и 0 соответственно. Хотя мы можем полагаться на эту модель для предсказания класса 1, что она делает с вероятностью 58%, мы не можем полагаться на нее для предсказания класса 0. В последнем случае мы будем просто гадать.

Общая точность составляет 53% из 100%, что в этом случае является реалистичным показателем. Это лучше, чем подбрасывать монетку.

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

import matplotlib.pyplot as plt

# Get feature importances
importances = model.get_feature_importance()
feature_names = X_train.columns if hasattr(X_train, 'columns') else [f'feature_{i}' for i in range(X_train.shape[1])]

# Create DataFrame for plotting
feat_imp_df = pd.DataFrame({
    'Feature': feature_names,
    'Importance': importances
}).sort_values(by='Importance', ascending=False)

# Plot
plt.figure(figsize=(7, 3))
plt.barh(feat_imp_df['Feature'], feat_imp_df['Importance'])
plt.gca().invert_yaxis()  # Highest importance on top
plt.title('Feature Importances')
plt.xlabel('Importance')
plt.ylabel('Feature')
plt.tight_layout()
plt.show()

Результаты.

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

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

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

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

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


Завершение работы над моделью

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

Во-первых, нам необходимо сохранить нашу модель в формате ONNX, совместимом с MQL5 и MetaTrader 5.

model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X_train.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

# And save.
with open(f"CatBoost.CDLPatterns.{symbol}.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

Более подробную информацию о сохранении модели Catboost можно найти здесь.

Советник довольно прост.

#include <Trade\Trade.mqh> //The trading module
#include <Trade\PositionInfo.mqh> //Position handling module
#include <ta-lib.mqh> //For candlestick patterns
#include <Catboost.mqh> //Has a class for deploying a catboost model

CTrade m_trade;
CPositionInfo m_position;

CCatboostClassifier catboost;

input int magic_number = 21042025;
input int slippage = 100;
input string symbol_ = "XAUUSD";
input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1;
input int lookahead = 1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
      
   if (!MQLInfoInteger(MQL_TESTER))
     if (!ChartSetSymbolPeriod(0, symbol_, timeframe_))
       {
         printf("%s failed to set symbol %s and timeframe %s, Check these values. Err = %d",__FUNCTION__,symbol_,EnumToString(timeframe_),GetLastError());
         return INIT_FAILED;
       }
  
//---
   
   if (!catboost.Init(StringFormat("CatBoost.CDLPatterns.%s.onnx",symbol_), ONNX_COMMON_FOLDER)) //Initialize the catboost model
      return INIT_FAILED;
   
//---

   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetMarginMode();
   m_trade.SetTypeFillingBySymbol(Symbol());
           
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
   double open = iOpen(Symbol(), Period(), 1),
          high = iHigh(Symbol(), Period(), 1),
          low  = iLow(Symbol(), Period(), 1), 
          close = iClose(Symbol(), Period(), 1);   
   
   vector x = {
               CTALib::CDLWHITECANDLE(open, close),
               CTALib::CDLBLACKCANDLE(open, close),
               CTALib::CDLDOJI(open, close),
               CTALib::CDLDRAGONFLYDOJI(open, high, low, close),
               CTALib::CDLGRAVESTONEDOJI(open, high, low, close),
               CTALib::CDLHAMMER(open, high, low, close),
               CTALib::CDLINVERTEDHAMMER(open, high, low, close),
               CTALib::CDLSPINNINGTOP(open, high, low, close),
               CTALib::CDLBULLISHMARUBOZU(open, high, low, close),
               CTALib::CDLBEARISHMARUBOZU(open, high, low, close)
              };
       
   long signal = catboost.predict(x).cls; //Predicted class

   MqlTick ticks;
   if (!SymbolInfoTick(Symbol(), ticks))
      {
         printf("Failed to obtain ticks information, Error = %d",GetLastError());
         return;
      }
      
   double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);   
   
   if (signal == 1) 
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, Symbol(), ticks.ask,0,0);
     }
     
   if (signal == 0)
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, Symbol(), ticks.bid,0,0);
     } 
    
    CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }

После инициализации модели Catboost в формате ONNX, сохраненной в общей папке,

внутри функции OnTick получаем значения Open, High, Low и Close для ранее закрытых баров и передаем их функциям из CTALib для обнаружения паттернов свечей, а затем используем полученные результаты для прогнозирования в векторе х

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

X_train.columns

В нашей окончательной модели у нас было:

Index(['White Candle', 'Black Candle', 'Doji Candle', 'Dragonflydoji Candle',
       'Gravestonedoji Candle', 'Hammer Candle', 'Invertedhammer Candle',
       'Spinningtop Candle', 'BullishMarubozu Candle',
       'BearishMarubozu Candle'],
      dtype='object')

Этот порядок был сохранен внутри советника.

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

Настройки тестера.

Результаты.

 

Сходство результатов длинных и коротких позиций поразительно: за эти два года было открыто 257 коротких позиций. На 2 сделки меньше, чем количество длинных позиций, которое составило 259.

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

Чтобы гарантировать соблюдение уникальных свечных паттернов, необходимо проверить, когда все специальные паттерны свечей равны 0 (false), то есть не обнаружены моделью, и предотвращать открытие сделки в этом случае.

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

void OnTick()
  {
//---
   
   double open = iOpen(Symbol(), Period(), 1),
          high = iHigh(Symbol(), Period(), 1),
          low  = iLow(Symbol(), Period(), 1), 
          close = iClose(Symbol(), Period(), 1);
   
   
   vector x = {
               CTALib::CDLWHITECANDLE(open, close),
               CTALib::CDLBLACKCANDLE(open, close),
               CTALib::CDLDOJI(open, close),
               CTALib::CDLDRAGONFLYDOJI(open, high, low, close),
               CTALib::CDLGRAVESTONEDOJI(open, high, low, close),
               CTALib::CDLHAMMER(open, high, low, close),
               CTALib::CDLINVERTEDHAMMER(open, high, low, close),
               CTALib::CDLSPINNINGTOP(open, high, low, close),
               CTALib::CDLBULLISHMARUBOZU(open, high, low, close),
               CTALib::CDLBEARISHMARUBOZU(open, high, low, close)
              };
   
   vector patterns = {
                        CTALib::CDLDOJI(open, close),
                        CTALib::CDLDRAGONFLYDOJI(open, high, low, close),
                        CTALib::CDLGRAVESTONEDOJI(open, high, low, close),
                        CTALib::CDLHAMMER(open, high, low, close),
                        CTALib::CDLINVERTEDHAMMER(open, high, low, close),
                        CTALib::CDLSPINNINGTOP(open, high, low, close),
                        CTALib::CDLBULLISHMARUBOZU(open, high, low, close),
                        CTALib::CDLBEARISHMARUBOZU(open, high, low, close)
                     }; //Store all the special patterns 
    
   long signal = catboost.predict(x).cls; //Predicted class

   MqlTick ticks;
   if (!SymbolInfoTick(Symbol(), ticks))
      {
         printf("Failed to obtain ticks information, Error = %d",GetLastError());
         return;
      }
      
   double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);   
   
   if (signal == 1 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, Symbol(), ticks.ask,0,0);
     }
     
   if (signal == 0 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, Symbol(), ticks.bid,0,0);
     } 
    
    CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }

Результаты тестирования.

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

Процент прибыльных сделок составляет 54,55%, что очень близко к общей точности 0,53 (53%), полученной в классификационном отчете. Это сходство указывает на то, что мы на правильном пути.


Заключение

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

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

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

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

 

Оставайтесь с нами и вносите свой вклад в разработку алгоритмов машинного обучения для языка MQL5 в этом GitHub-репозитории.


Источники и ссылки


Таблица вложений

Имя файла Описание/использование
Experts\CandlestickPatterns AI-EA.mq5 Советник, использующий модель Catboost, которая делает прогнозы на основе свечных паттернов.
Indicators\Candlestick Identifier.mq5 Индикатор для отображения свечных паттернов на графике.
Scripts\Candlestick Patterns Collect.mq5 Скрипт для сбора данных о свечных паттернах и сохранения этой информации в CSV-файл.
Include\Catboost.mqh Библиотека, содержащая классы для загрузки, инициализации и развертывания классификатора catboost для прогнозирования рынка.
Include\pandas.mqh Модуль Pandas для хранения и обработки данных.
Include\ta-lib.mqh Библиотека технического анализа, содержащая класс для обнаружения свечных паттернов.
Common\Files\*.csv CSV-файлы, содержащие свечные данные для использования в машинном обучении.
Common\Files\*.onnx Модели машинного обучения в формате ONNX.
CandlestickMarket Prediction.ipynb Скрипт на Python (Jupyter noteboot) для обучения модели Catboost.


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

Прикрепленные файлы |
Attachments.zip (183.92 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
Zhuo Kai Chen
Zhuo Kai Chen | 1 мая 2025 в 05:37
Очень понравилась эта попытка! Очень интересный подход к разметке свечных паттернов для ML.
Stanislav Korotky
Stanislav Korotky | 1 мая 2025 в 10:17
Использование свечных конфигураций вместо базового ценового действия подобно использованию 256-цветного индексированного изображения вместо истинного цвета - вы теряете множество нюансов, которые могут быть важны. Более того, в зависимости от временного смещения брокера свечные паттерны могут полностью меняться на одной и той же истории котировок. И даже если вы не используете крупные таймфреймы (такие как D1 (в вашем случае) или H4), простое искусственное смещение времени (минут) внутри часа приведет к совершенно разным формациям свечей. Ненадежно, не приносит никакой пользы.
Omega J Msigwa
Omega J Msigwa | 1 мая 2025 в 13:08
Stanislav Korotky свечные паттерны могут полностью меняться на одной и той же истории котировок. И даже если вы не используете крупные таймфреймы (такие как D1 (в вашем случае) или H4), простое искусственное смещение времени (минут) внутри часа приведет к совершенно разным формациям свечей. Ненадежно, не приносит никакой пользы.

Я уже объяснял это в статье.

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

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

Ценовое действие очень важно, это нельзя отрицать.

Stanislav Korotky
Stanislav Korotky | 1 мая 2025 в 13:35
Omega J Msigwa #:

Я уже объяснял это в статье.

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

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

Ценовое действие очень важно, это нельзя отрицать.

Ваша цитата не имеет отношения к основной идее моего высказывания.

Anil Varma
Anil Varma | 23 дек. 2025 в 10:58

Привет проект ML будет основан на этих линиях.

Желаю вам счастливого Рождества ...

Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Нейросети в трейдинге: Гибридные модели прогнозирования с управляемой смесью распределений (Lattice) Нейросети в трейдинге: Гибридные модели прогнозирования с управляемой смесью распределений (Lattice)
Статья разбирает гибридную систему Lattice: базовый LSTM, архетипы, soft/hard assignment и confidence-based binary gating для управления неопределённостью. Включён Tail-Aware модуль для моделирования тяжёлых хвостов и локально взрывных участков. Приведена реализация в MQL5 с выносом вычислительно тяжёлых частей в OpenCL и GPU (смесь экспертов, генерация и градиенты). Практический эффект — более надёжные сигналы входа/выхода и количественная поддержка риск-контроля.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Возможности Мастера MQL5, которые вам нужно знать (Часть 60): Обучение на основе вывода (Wasserstein-VAE) с использованием скользящей средней и стохастического осциллятора Возможности Мастера MQL5, которые вам нужно знать (Часть 60): Обучение на основе вывода (Wasserstein-VAE) с использованием скользящей средней и стохастического осциллятора
Мы завершаем наше исследование взаимодополняющей пары скользящей средней и стохастического осциллятора рассмотрением роль обучения на основе вывода (inference-learning) после обучения с учителем и обучения с подкреплением. В данном случае существует множество способов обучения, однако наш подход заключается в использовании вариационных автоэнкодеров. Мы проведем исследование на Python, а затем экспортируем нашу обученную модель с помощью ONNX для использования в созданном Мастером советнике в MetaTrader.