English Deutsch 日本語
preview
Разработка инструментария для анализа Price Action (Часть 43): Вероятностный анализ свечных паттернов и пробоев

Разработка инструментария для анализа Price Action (Часть 43): Вероятностный анализ свечных паттернов и пробоев

MetaTrader 5Примеры |
338 0
Christian Benjamin
Christian Benjamin

Содержание



Введение

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

Распространенная проблема трейдеров – преждевременный вход: увидев бычий пин-бар, они сразу покупают, а затем получают убыточную позицию, если рынок не разворачивается. Разные инструменты и таймфреймы по-разному реагируют на один и тот же паттерн; у некоторых пар после определенных паттернов вероятность продолжения выше. Этот советник работает как инструмент вероятностного анализа: он просматривает исторические бары и оценивает, как часто каждый паттерн приводит к продолжению, развороту или успешному пробою на конкретной паре и таймфрейме. Такой эмпирический подход помогает трейдерам, работающим с Price Action, понять, какие паттерны надежнее работают как сигналы разворота или продолжения на тех рынках, где они торгуют.

Если внимательно посмотреть на схему выше, паттерн A – это бычье поглощение, похожее на сигнал на покупку; после его формирования рынок идет вверх, что подтверждает успешность сигнала. В точке B мы видим доджи, за которым следует медвежий пин-бар; затем рынок движется в пользу продавцов, что подтверждает медвежий сигнал. То же относится и к C: это медвежье поглощение дало сигнал на продажу и привело к ожидаемому снижению.

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



Обзор стратегии

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

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

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

Для каждого появления паттерна инструмент сохраняет несколько параметров: сам факт появления, время его появления и торговую активность на этом баре ("объем"). Затем для этого случая проверяются три исхода: двинулся ли сразу следующий закрытый бар в направлении, заданном паттерном; двигалась ли цена в эту сторону в какой-либо момент в пределах следующих N баров; и пробила ли цена максимум или минимум паттерна на заданное число пунктов с последующим подтверждением следующим баром. Результат каждой проверки фиксируется как положительный или отрицательный, а объемы для этих конкретных случаев также сохраняются.

Все результаты да/нет и объемы суммируются отдельно для пин-баров, поглощений и доджи. Для каждого паттерна инструмент хранит общее число появлений; число случаев с успешным следующим баром; число успехов в пределах N баров; число пробоев и подтвержденных пробоев; а также суммарную торговую активность по барам паттерна и по успешным случаям. Раздельный учет этих показателей позволяет измерить, как каждый паттерн ведет себя на конкретном символе и таймфрейме.
В самом простом варианте эти подсчеты переводятся в легко читаемые проценты. Формулы таковы:
  • Процент успеха на следующем баре = (Количество успешных случаев на следующем баре ÷ Общее количество случаев) × 100
  • Процент успеха в пределах N баров = (Количество успешных случаев в пределах N баров ÷ Общее количество случаев) × 100
  • Процент успешных пробоев = (Количество подтвержденных пробоев ÷ Общее число пробоев) × 100
Пример: Если инструмент нашел 50 пин-баров и в 15 случаях следующий бар двинулся как ожидалось, то 15 ÷ 50 = 0,30 → 0,30 × 100 = 30% успеха на следующем баре. Иногда полезно взвешивать результаты по торговой активности, а не по простым подсчетам. В этом случае инструмент суммирует объемы, а не число появлений. Формулы таковы:
  • Процент успеха на следующем баре (взвешенный по объему) = (Объем успешных паттернов на следующем баре ÷ Общий объем всех паттернов) × 100
  • Процент успеха в пределах N баров (взвешенный по объему) = (Объем успешных паттернов в пределах N баров ÷ Общий объем всех паттернов) × 100
  • Процент успешных пробоев (взвешенный по объему) = (Объем подтвержденных пробоев ÷ Общий объем пробоев) × 100

Пример: Если у 50 пин-баров суммарный объем составил 10 000, а у 15 успешных – 3 000, то 3 000 ÷ 10 000 = 0,30 → 0,30 × 100 = 30% успеха на следующем баре (взвешенного по объему).

Наконец, инструмент показывает показатели по каждому паттерну на информационной панели и при необходимости записывает их в CSV-файл. Для каждого паттерна вы обычно увидите общее число обнаружений, процент успеха на следующем баре, процент успеха в пределах N баров, количество пробоев и процент успешных пробоев, а также взвешенные по объему версии этих метрик, если они включены. Если паттерн ни разу не встречался, инструмент показывает 0% или "N/A" для процентов, которые нельзя вычислить. Практический совет прост: смотрите на обе метрики. Простые проценты показывают, как часто происходило то или иное событие, а взвешенные по объему – какой объем торгов стоял за этими исходами.


Реализация

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

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

Конфигурация и входные параметры

В верхней части файла расположен компактный и хорошо документированный набор входных параметров, который позволяет настраивать советник без редактирования кода. В числе этих параметров – окно проверки (LookbackBars), окно просмотра вперед (LookAheadBars), пороги пробоя (BreakoutPips, BreakoutConfirmBars), таймфрейм (TF), флаги визуализации и интерфейса, такие как ShowPatternMarkers и ShowControlButtons, а также параметры экспорта и взвешивания, такие как ExportCSV и UseVolumeWeighting. Вынос этих настроек во входные параметры упрощает быстрые эксперименты и показывает, какие именно параметры важны при настройке и сравнении результатов.

//--- Inputs, user-configurable parameters
input int    LookbackBars        = 2000;       // how many bars to scan
input int    LookAheadBars       = 5;          // how many bars forward to look for follow-up
input int    BreakoutPips        = 5;          // breakout threshold in pips
input int    BreakoutConfirmBars = 1;          // bars after breakout used for confirmation
input ENUM_TIMEFRAMES TF        = PERIOD_CURRENT;
input bool   ExportCSV          = false;
input string CSVFileName        = "PatternStats.csv";

input bool   ShowPatternMarkers = true;
input int    MaxMarkers         = 500;
input int    MarkerFontSize     = 12;
input int    MarkerOffsetPips   = 3;
input int    MarkerStackWindow  = 2;

input bool   UseVolumeWeighting = true;
input bool   ShowControlButtons = true;

Модель данных для статистики паттернов

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

struct PatternStats
  {
   string name;
   int    total;
   int    nextSameDir;
   int    withinLookAheadSame;
   int    breakouts;
   int    breakoutSuccess;
   double volTotal;
   double weightedNextSameDir;
   double weightedWithin;
   double weightedBreakouts;
   double weightedBreakoutSuccess;
  };

// Example declarations
PatternStats pinbar, engulfing, dojiStats;

Инициализация (OnInit)

Задачи инициализации последовательно выполняются в OnInit(); код обнуляет всю статистику через ResetStats(), присваивает читаемые имена каждому экземпляру PatternStats, подготавливает счетчики маркеров для каждого бара и при необходимости создает кнопки управления, если включена интерактивность. С помощью EventSetTimer() запускается короткий таймер событий, а затем сразу однократно вызывается Analyze(), чтобы результаты появились на графике сразу после запуска советника - это обеспечивает повторяемое поведение при перезапусках и безопасную работу во время активной торговли.

int OnInit()
  {
   // reset stats and name patterns
   ResetStats();
   pinbar.name     = "Pinbar";
   engulfing.name  = "Engulfing";
   dojiStats.name  = "Doji";

   // prepare marker counts buffer
   ArrayResize(markerCounts, 64);
   for(int i=0;i<ArraySize(markerCounts);i++) markerCounts[i]=0;

   // create UI if requested
   if(ShowControlButtons) CreateControlButtons();

   // run periodically to detect new bars
   EventSetTimer(5);

   // initial analysis
   Analyze();
   return(INIT_SUCCEEDED);
  }

Очистка перед выходом (OnDeinit)

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

void OnDeinit(const int reason)
  {
   EventKillTimer();
   DeleteAllOurObjects();   // removes only objects with EA prefixes
   ChartRedraw();
  }

Реактивные обновления (OnTimer и OnChartEvent)

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

void OnTimer()
  {
   static datetime lastTime = 0;
   datetime t = iTime(_Symbol, TF, 1); // previous completed bar
   if(t == lastTime) return;
   lastTime = t;
   Analyze();
  }

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK && StringFind(sparam, "Btn_") == 0)
     {
      if(StringCompare(sparam, "Btn_Pinbar") == 0) { enabledPinbar = !enabledPinbar; UpdateButtonLabel("Btn_Pinbar", pinbar, enabledPinbar); Analyze(); }
      if(StringCompare(sparam, "Btn_Engulfing") == 0) { enabledEngulfing = !enabledEngulfing; UpdateButtonLabel("Btn_Engulfing", engulfing, enabledEngulfing); Analyze(); }
      if(StringCompare(sparam, "Btn_Doji") == 0) { enabledDoji = !enabledDoji; UpdateButtonLabel("Btn_Doji", dojiStats, enabledDoji); Analyze(); }
     }
  }

Функции распознавания паттернов

Каждое правило паттерна реализовано как краткая параметризованная вспомогательная функция, например IsPinbar(shift), IsEngulfing(shift) и IsDoji(shift). Эти функции читают только нужные поля бара и применяют настраиваемые пороги, такие как PinbarBodyPct, PinbarTailPct и DojiBodyPct, возвращая направленные значения (+1, -1, 0), что упрощает дальнейшую агрегацию и позволяет реализовать приоритетную или комбинированную логику паттернов.

int IsPinbar(int shift)
  {
   if(IsDoji(shift)) return 0;
   double o = iOpen(_Symbol, TF, shift), c = iClose(_Symbol, TF, shift);
   double h = iHigh(_Symbol, TF, shift), l = iLow(_Symbol, TF, shift);
   double range = h - l;
   if(range <= 0) return 0;
   double body = MathAbs(c - o);
   if(body > PinbarBodyPct * range) return 0;
   double upperTail = h - MathMax(o, c);
   double lowerTail = MathMin(o, c) - l;
   if(lowerTail >= PinbarTailPct * range && upperTail <= (1.0 - PinbarTailPct) * range) return +1;
   if(upperTail >= PinbarTailPct * range && lowerTail <= (1.0 - PinbarTailPct) * range) return -1;
   return 0;
  }

int IsEngulfing(int shift)
  {
   int totalBars = iBars(_Symbol, TF);
   if(totalBars <= shift + 1) return 0;
   double o1 = iOpen(_Symbol, TF, shift), c1 = iClose(_Symbol, TF, shift);
   double o2 = iOpen(_Symbol, TF, shift + 1), c2 = iClose(_Symbol, TF, shift + 1);
   double body1 = c1 - o1, body2 = c2 - o2;
   if(body1 > 0 && body2 < 0 && o1 <= c2 && c1 >= o2) return +1;
   if(body1 < 0 && body2 > 0 && o1 >= c2 && c1 <= o2) return -1;
   return 0;
  }

bool IsDoji(int shift)
  {
   double o = iOpen(_Symbol, TF, shift), c = iClose(_Symbol, TF, shift);
   double h = iHigh(_Symbol, TF, shift), l = iLow(_Symbol, TF, shift);
   double range = h - l;
   if(range <= 0) return false;
   return (MathAbs(c - o) <= DojiBodyPct * range);
  }

Основной цикл анализа (Analyze)

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

void Analyze()
  {
   ResetStats();
   int totalBars = iBars(_Symbol, TF);
   if(totalBars < 3) return;
   int maxIndex = MathMin(totalBars - 1, LookbackBars);
   if(maxIndex < 2) return;

   // pip scaling
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double factor = (digits == 5 || digits == 3) ? 10.0 : 1.0;
   double breakoutPoints = BreakoutPips * point * factor;

   ArrayResize(markerCounts, MathMax(3, maxIndex + 3));
   for(int i=0;i<ArraySize(markerCounts);i++) markerCounts[i]=0;

   if(ShowPatternMarkers) DeletePatternObjects();

   int drawn=0;
   for(int shift=2; shift<=maxIndex; shift++)
     {
      int eng = IsEngulfing(shift);
      int pin = IsPinbar(shift);
      bool doj = IsDoji(shift);
      bool matched = false;

      if(eng != 0 && enabledEngulfing)
        {
         UpdateStatsOnPattern(engulfing, shift, eng, LookAheadBars, breakoutPoints);
         if(ShowPatternMarkers && drawn < MaxMarkers) { DrawPatternOnChartWithStacking(shift, "Engulfing", eng, MarkerFontSize, MarkerOffsetPips, factor); drawn++; }
         matched = true;
        }
      if(!matched && pin != 0 && enabledPinbar)
        {
         UpdateStatsOnPattern(pinbar, shift, pin, LookAheadBars, breakoutPoints);
         if(ShowPatternMarkers && drawn < MaxMarkers) { DrawPatternOnChartWithStacking(shift, "Pinbar", pin, MarkerFontSize, MarkerOffsetPips, factor); drawn++; }
         matched = true;
        }
      if(!matched && doj && enabledDoji)
        {
         UpdateStatsOnPattern(dojiStats, shift, 0, LookAheadBars, breakoutPoints);
         if(ShowPatternMarkers && drawn < MaxMarkers) { DrawPatternOnChartWithStacking(shift, "Doji", 0, MarkerFontSize, MarkerOffsetPips, factor); drawn++; }
        }
     }

   if(ExportCSV) SaveToCSV();
   DrawDashboard();
   DrawSummaryHeaderAndLabel();

   if(ShowControlButtons)
     {
      UpdateButtonLabel("Btn_Pinbar", pinbar, enabledPinbar);
      UpdateButtonLabel("Btn_Engulfing", engulfing, enabledEngulfing);
      UpdateButtonLabel("Btn_Doji", dojiStats, enabledDoji);
     }

   ChartRedraw();
  }

Обновление статистики (UpdateStatsOnPattern)

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

void UpdateStatsOnPattern(PatternStats &s, int shift, int direction, int lookAhead, double breakoutPoints)
  {
   s.total++;
   double vol = (double)iVolume(_Symbol, TF, shift);
   if(vol <= 0) vol = 1.0;
   s.volTotal += vol;

   // next-bar direction
   if(shift - 1 >= 0)
     {
      double nO = iOpen(_Symbol, TF, shift - 1), nC = iClose(_Symbol, TF, shift - 1);
      int nDir = (nC > nO) ? +1 : (nC < nO) ? -1 : 0;
      if(direction == 0)
        {
         if(nDir != 0) { s.nextSameDir++; s.weightedNextSameDir += vol; }
        }
      else
        {
         if(nDir == direction) { s.nextSameDir++; s.weightedNextSameDir += vol; }
        }
     }

   // within lookahead
   bool anySame = false; double weightedAny = 0.0;
   int totalBars = iBars(_Symbol, TF);
   for(int k=1; k<=lookAhead; k++)
     {
      int idx = shift - k;
      if(idx < 1 || idx > totalBars - 1) break;
      double o = iOpen(_Symbol, TF, idx), c = iClose(_Symbol, TF, idx);
      double v = (double)iVolume(_Symbol, TF, idx); if(v <= 0) v = 1.0;
      int d = (c > o) ? +1 : (c < o) ? -1 : 0;
      if(direction == 0)
        {
         if(d != 0) { anySame = true; weightedAny += v; break; }
        }
      else
        {
         if(d == direction) { anySame = true; weightedAny += v; break; }
        }
     }
   if(anySame) { s.withinLookAheadSame++; s.weightedWithin += (weightedAny > 0.0 ? weightedAny : vol); }

   // breakout detection and confirmation
   double highP = iHigh(_Symbol, TF, shift), lowP = iLow(_Symbol, TF, shift);
   bool breakout = false;
   if(shift - 1 >= 0)
     {
      double nextHigh = iHigh(_Symbol, TF, shift - 1), nextLow = iLow(_Symbol, TF, shift - 1);
      if(direction == +1 && nextHigh >= highP + breakoutPoints) breakout = true;
      else if(direction == -1 && nextLow <= lowP - breakoutPoints) breakout = true;
      else if(direction == 0 && (nextHigh >= highP + breakoutPoints || nextLow <= lowP - breakoutPoints)) breakout = true;
     }

   if(breakout)
     {
      s.breakouts++;
      s.weightedBreakouts += vol;
      int confirmShift = shift - 1 - BreakoutConfirmBars;
      if(confirmShift >= 0 && confirmShift <= totalBars - 1)
        {
         double cO = iOpen(_Symbol, TF, confirmShift), cC = iClose(_Symbol, TF, confirmShift);
         double cVol = (double)iVolume(_Symbol, TF, confirmShift); if(cVol <= 0) cVol = 1.0;
         int cDir = (cC > cO) ? +1 : (cC < cO) ? -1 : 0;
         bool confirmed = false;
         if(direction == +1 && cC >= highP + breakoutPoints) confirmed = true;
         else if(direction == -1 && cC <= lowP - breakoutPoints) confirmed = true;
         else if(direction == 0 && (cC >= highP + breakoutPoints || cC <= lowP - breakoutPoints)) confirmed = true;
         if(confirmed && (direction == 0 ? cDir != 0 : cDir == direction))
           {
            s.breakoutSuccess++;
            s.weightedBreakoutSuccess += cVol;
           }
        }
     }
  }

Вычисление процентов и экспорт в CSV

Преобразование простых подсчетов или взвешенных сумм в наглядные проценты централизовано в функции ComputePct(); если включена опция UseVolumeWeighting, возвращаются проценты, взвешенные по объему; в противном случае используются простые проценты по частоте. Если опция ExportCSV включена, функция SaveToCSV() записывает файл с разделителем табуляции, заголовками и одной строкой на каждый паттерн, чтобы последующий анализ в Python, R или табличных редакторах был простым и воспроизводимым.

double ComputePct(double weightedValue, PatternStats &s, int simpleCount, int total)
  {
   if(UseVolumeWeighting)
     {
      if(s.volTotal <= 0.0) return 0.0;
      return 100.0 * weightedValue / s.volTotal;
     }
   else
     {
      if(total <= 0) return 0.0;
      return 100.0 * simpleCount / total;
     }
  }

void SaveToCSV()
  {
   int handle = FileOpen(CSVFileName, FILE_WRITE|FILE_CSV|FILE_ANSI, '\t');
   if(handle == INVALID_HANDLE) { Print("Failed to open CSV: ", CSVFileName); return; }
   FileWrite(handle, "Pattern","Total","Next%","Within%","Breakouts","Success%","VolTotal","W_Next%","W_Within%","W_Breakouts%","W_Success%");
   WriteStatsLine(handle, pinbar);
   WriteStatsLine(handle, engulfing);
   WriteStatsLine(handle, dojiStats);
   FileClose(handle);
  }

void WriteStatsLine(int handle, PatternStats &s)
  {
   double nextPct   = ComputePct(s.weightedNextSameDir, s, s.nextSameDir, s.total);
   double withinPct = ComputePct(s.weightedWithin, s, s.withinLookAheadSame, s.total);
   double successPct = s.breakouts > 0 ? 100.0 * s.breakoutSuccess / s.breakouts : 0.0;
   double wBreakoutsPct = s.volTotal > 0.0 ? 100.0 * s.weightedBreakouts / s.volTotal : 0.0;
   double wSuccessPct   = s.weightedBreakouts > 0.0 ? 100.0 * s.weightedBreakoutSuccess / s.weightedBreakouts : 0.0;
   FileWrite(handle, s.name, s.total, DoubleToString(nextPct,1), DoubleToString(withinPct,1),
             s.breakouts, DoubleToString(successPct,1), DoubleToString(s.volTotal,0),
             DoubleToString(nextPct,1), DoubleToString(withinPct,1), DoubleToString(wBreakoutsPct,1), DoubleToString(wSuccessPct,1));
  }

Визуализация на графике, маркеры и наложение

Визуальные маркеры рисуются функцией DrawPatternOnChartWithStacking(), которая размещает компактные текстовые стрелки или символы на баре паттерна и располагает их по вертикали, чтобы избежать наложения. За размещение маркеров по уровням отвечают массив markerCounts[] для каждого бара и параметр MarkerStackWindow. Маркеры имеют цветовую кодировку и намеренно сделаны ненавязчивыми, чтобы сразу давать контекст и не заслонять движение цены.

void DrawPatternOnChartWithStacking(int shift, string patternName, int direction, int fontSize, int offsetPips, double factor)
  {
   string name = "Pattern_" + patternName + "_" + IntegerToString(shift);
   if(ObjectFind(0, name) != -1) ObjectDelete(0, name);

   datetime t = iTime(_Symbol, TF, shift);
   double highP = iHigh(_Symbol, TF, shift), lowP = iLow(_Symbol, TF, shift);
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double offset = offsetPips * point * factor;
   double priceBase = (highP + lowP) / 2.0;
   if(direction > 0) priceBase = lowP - offset;
   else if(direction < 0) priceBase = highP + offset;

   int idx = shift;
   if(idx >= ArraySize(markerCounts)) ArrayResize(markerCounts, idx + 5);
   int stack = markerCounts[idx]++;
   double stackOffset = (stack / MarkerStackWindow) * offset * 1.2;
   if(direction > 0) priceBase -= stackOffset; else priceBase += stackOffset;

   string arrow = (direction>0 ? "▲" : direction<0 ? "▼" : "◆");
   string text = arrow + " " + StringSubstr(patternName, 0, 3);
   ObjectCreate(0, name, OBJ_TEXT, 0, t, priceBase);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, 0);
   ObjectSetInteger(0, name, OBJPROP_BACK, 1);
   ObjectSetInteger(0, name, OBJPROP_COLOR, (direction>0 ? clrLime : direction<0 ? clrRed : clrLightBlue));
  }

Информационная панель и метки

Компактная информационная панель на графике, создаваемая функцией DrawDashboard(), выводит стандартные столбцы Pattern, Total, Next%, Within%, Breakouts и Success%, а функция DrawSummaryHeaderAndLabel() создает краткую сводку в левом верхнем углу для быстрой проверки. Использование единых префиксов в именах объектов позволяет советнику надежно удалять эти элементы интерфейса, когда они больше не нужны.

void DrawDashboard()
  {
   string rectName = "Dash_Background";
   if(ObjectFind(0, rectName) != -1) ObjectDelete(0, rectName);
   ObjectCreate(0, rectName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, rectName, OBJPROP_CORNER, 0);
   ObjectSetInteger(0, rectName, OBJPROP_XDISTANCE, 8);
   ObjectSetInteger(0, rectName, OBJPROP_YDISTANCE, 70);
   ObjectSetInteger(0, rectName, OBJPROP_XSIZE, 660);
   ObjectSetInteger(0, rectName, OBJPROP_YSIZE, 80);
   ObjectSetInteger(0, rectName, OBJPROP_BACK, 1);
   ObjectSetInteger(0, rectName, OBJPROP_COLOR, clrDarkSlateGray);

   // header and rows omitted for brevity, see DrawDashboard in main code for full cell logic
  }

void DrawSummaryHeaderAndLabel()
  {
   string header = "SummaryHeader";
   if(ObjectFind(0, header) != -1) ObjectDelete(0, header);
   ObjectCreate(0, header, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, header, OBJPROP_CORNER, 0);
   ObjectSetInteger(0, header, OBJPROP_XDISTANCE, 10);
   ObjectSetInteger(0, header, OBJPROP_YDISTANCE, 12);
   ObjectSetInteger(0, header, OBJPROP_FONTSIZE, 11);
   ObjectSetString(0, header, OBJPROP_TEXT, "Candlestick Pattern Stats Summary");
   ObjectSetInteger(0, header, OBJPROP_COLOR, clrBlue);

   string sum = "SummaryLabel";
   if(ObjectFind(0, sum) != -1) ObjectDelete(0, sum);
   ObjectCreate(0, sum, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, sum, OBJPROP_CORNER, 0);
   ObjectSetInteger(0, sum, OBJPROP_XDISTANCE, 10);
   ObjectSetInteger(0, sum, OBJPROP_YDISTANCE, 30);
   ObjectSetInteger(0, sum, OBJPROP_FONTSIZE, 9);
   ObjectSetInteger(0, sum, OBJPROP_BACK, 1);
   ObjectSetString(0, sum, OBJPROP_TEXT, FormatStatsForLabel(pinbar) + "\n" + FormatStatsForLabel(engulfing) + "\n" + FormatStatsForLabel(dojiStats));
  }

Кнопки управления и динамические метки

Кнопки с автоматическим подбором ширины создаются функцией CreateAutoSizedButton() и обновляются в реальном времени через UpdateButtonLabel(). Каждая кнопка показывает короткий идентификатор, состояние ON/OFF и текущие метрики Next% и Succ%. Функция EstimateButtonWidth() предотвращает обрезку текста, а цвет кнопок меняется в зависимости от состояния, чтобы пользователь мог интерактивно настраивать, какие паттерны участвуют в статистике.

void CreateAutoSizedButton(string name, int x, int y, string text)
  {
   if(ObjectFind(0, name) != -1) ObjectDelete(0, name);
   ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, 1);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetString(0, name, OBJPROP_FONT, ButtonFont);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, ButtonFontSize);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   int w = EstimateButtonWidth(text, ButtonFontSize);
   int h = ButtonFontSize + ButtonPaddingY * 2;
   ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clrWhite);
  }

void UpdateButtonLabel(string name, PatternStats &s, bool enabled)
  {
   if(ObjectFind(0, name) == -1) return;
   string onoff = enabled ? "ON" : "OFF";
   double nextPct = ComputePct(s.weightedNextSameDir, s, s.nextSameDir, s.total);
   double successPct = s.breakouts > 0 ? 100.0 * s.breakoutSuccess / s.breakouts : 0.0;
   string shortName = (StringCompare(s.name, "Pinbar") == 0) ? "Pin" : (StringCompare(s.name,"Engulfing")==0) ? "Eng" : "Doj";
   string txt = shortName + ": " + onoff + "  Next% " + DoubleToString(nextPct,1) + "  Succ% " + DoubleToString(successPct,1);
   ObjectSetString(0, name, OBJPROP_TEXT, txt);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, EstimateButtonWidth(txt, ButtonFontSize));
   if(enabled) { ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clrWhite); ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlack); }
   else { ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clrLightGray); ObjectSetInteger(0, name, OBJPROP_COLOR, clrDimGray); }
  }

int EstimateButtonWidth(const string txt, const int fontSize)
  {
   int len = StringLen(txt);
   double charW = fontSize * 0.75;
   int base = (int)MathCeil(len * charW) + ButtonPaddingX * 2 + 10;
   if(base < 100) base = 100;
   return base;
  }

Служебные операции и управление объектами

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

void DeleteAllOurObjects()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string nm = ObjectName(0, i);
      if(StringFind(nm, "Pattern_") == 0 || StringFind(nm, "Dash_") == 0 || StringFind(nm, "Btn_") == 0
         || StringCompare(nm, "SummaryLabel") == 0 || StringCompare(nm, "SummaryHeader") == 0)
         ObjectDelete(0, nm);
     }
  }


Тестирование

В этом разделе мы оцениваем советник на Step Index и Boom 300 Index, чтобы проверить устойчивость инструмента на синтетических инструментах с разными профилями волатильности.

  • Step Index

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

В приведенном примере информационная панель отображает показатели успешности: 40,2% для пин-бара и 31,7% для поглощения. Для доджи показатель составляет 65,1%; однако в этом анализе доджи рассматривается как нейтральный сигнал, поскольку высокий показатель в основном обусловлен фильтрацией пробоев, а не направленным смещением. Поэтому результат для доджи лучше трактовать как указание на склонность инструмента к продолжению движения или реализации пробоя, а не как простой бычий или медвежий сигнал.

Результаты также экспортируются в CSV для независимой проверки. Чтобы создать файлы, откройте входные параметры советника и установите ExportCSV = true; имя сводного файла можно изменить через CSVFileName. Советник записывает сводный CSV-файл в папку Files терминала для последующего автономного анализа.

input bool   ExportCSV          = true;
input string CSVFileName        = "PatternStats.csv";

  • Boom 300 Index

На Boom 300 Index измеренные показатели успешности составляют 40,0% для поглощения, 42,1% для пин-бара и 78,6% для доджи. Эти значения показывают, что на этом инструменте после появления доджи по правилам советника гораздо чаще реализуются сценарии продолжения движения или пробоя, тогда как поглощение и пин-бар демонстрируют более скромные направленные результаты.

Если сравнить Boom 300 со Step Index, то эффективность пин-бара на обоих инструментах практически одинакова, что говорит об определенной устойчивости этого паттерна. При этом поглощение на Boom 300 работает заметно лучше, чем на Step Index; это означает, что в рамках текущих параметров советника и рассматриваемой выборки сигналы поглощения чаще дают положительную отработку на Boom 300.


Заключение

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

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

Читайте другие мои статьи здесь.

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

Прикрепленные файлы |
Тестер стратегий для Python и MetaTrader 5 (Часть 02): Работа с барами, тиками и реализация встроенных функций в симуляторе Тестер стратегий для Python и MetaTrader 5 (Часть 02): Работа с барами, тиками и реализация встроенных функций в симуляторе
В этой статье мы представим функции, аналогичные тем, которые предоставляет модуль Python–MetaTrader 5, предоставляя симулятору привычный интерфейс и собственный механизм внутренней обработки баров и тиков.
Разработка инструментария для анализа Price Action (Часть 42): Интерактивное тестирование на графике с кнопочной логикой и статистическими уровнями Разработка инструментария для анализа Price Action (Часть 42): Интерактивное тестирование на графике с кнопочной логикой и статистическими уровнями
В мире, где важны скорость и точность, инструменты анализа должны быть столь же умными, как и рынки, на которых мы торгуем. В этой статье представлен советник с кнопочной логикой – интерактивная система, которая мгновенно преобразует исходные ценовые данные в значимые статистические уровни. Одним кликом мыши он вычисляет и отображает среднее, отклонение, процентили и другие показатели, превращая продвинутую аналитику в понятные сигналы на графике. Он выделяет зоны, где цена с наибольшей вероятностью отскочит, откатится или пробьет уровень, что делает анализ и быстрее, и практичнее.
Построение моделей волатильности в MQL5 (Часть I): Первичная реализация Построение моделей волатильности в MQL5 (Часть I): Первичная реализация
В этой статье мы представляем библиотеку MQL5 для моделирования волатильности, разработанную так, чтобы функционировать аналогично пакету arch в Python. В настоящее время библиотека поддерживает спецификацию распространённых моделей условного среднего: HAR, AR, Constant Mean и Zero Mean, а также моделей условной волатильности: Constant Variance, ARCH и GARCH.
От матриц к модели: Как запустить ML-пайплайн в MQL5 и довести его до ONNX От матриц к модели: Как запустить ML-пайплайн в MQL5 и довести его до ONNX
Показано, как организовать согласованный ML-конвейер в MetaTrader 5 с разделением ролей: Python обучает и экспортирует модель в ONNX, MQL5 воспроизводит нормализацию и PCA через matrix/vector и выполняет инференс. Такой подход делает входы модели стабильными и проверяемыми, а тестер стратегий MetaTrader 5 даёт метрики для анализа поведения системы.