English
preview
Автоматизация классических рыночных методов в MQL5 (Часть 4): Трендовый шаблон Марка Минервини

Автоматизация классических рыночных методов в MQL5 (Часть 4): Трендовый шаблон Марка Минервини

MetaTrader 5 — Трейдинг |
15 0
Tola Moses Hector
Tola Moses Hector

Введение

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

Минервини — один из ныне живущих трейдеров, чья деятельность изучена наиболее подробно. Он выиграл чемпионат США по инвестированию в 1997 году с подтвержденной аудитом доходностью 155 % и вновь в 2021 году с подтвержденной аудитом доходностью 334.8 % в категории «миллион долларов». Его метод, который он называет SEPA — анализ конкретной точки входа, начинается с трендового шаблона: контрольного списка из восьми технических условий, которым рынок должен соответствовать, прежде чем вообще можно будет переходить к анализу его графика. Если хотя бы одно из условий не выполняется, инструмент исключается из рассмотрения. Без исключений.

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

Трендовый шаблон был разработан для акций, к которым Минервини также применяет фундаментальные фильтры и рейтинг относительной силы (RS) от издания Investor’s Business Daily. В данной статье семь технических условий адаптированы для валютных пар на дневном таймфрейме. Восьмое условие (рейтинг относительной силы (RS)) невозможно воспроизвести в MQL5 без внешнего источника данных, поэтому оно заменяется фильтром ценового импульса на основе 14-периодного индекса относительной силы (RSI). Сигнал входа — пробой в паттерне сжатия волатильности (VCP), который представляет собой серию все более узких ценовых колебаний, завершающуюся пробоем сопротивления на высоком объеме. Здесь этот сигнал упрощен до пробоя максимума за 20 баров на растущем объеме.

Мы рассмотрим следующие темы:

  1. Восемь условий и то, что они измеряют
  2. Адаптация трендового шаблона для рынка Forex
  3. Реализация на языке MQL5
  4. Тестирование на исторических данных
  5. Известные ограничения
  6. Заключение


Восемь условий и то, что они измеряют

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

Условие 1: Цена выше 150-дневной и 200-дневной скользящих средних. Цена должна одновременно находиться выше как 150-дневной простой скользящей средней, так и 200-дневной простой скользящей средней. Это фильтры долгосрочного тренда. Если цена находится ниже хотя бы одного из этих уровней, восходящий тренд на рынке не считается подтвержденным независимо от недавнего ценового импульса.

Условие 2: 150-дневная скользящая средняя находится выше 200-дневной скользящей средней. Это подтверждает, что среднесрочный тренд в течение значительного периода времени находился выше долгосрочного тренда. Когда 150-дневная скользящая средняя пересекает 200-дневную снизу вверх, это сигнализирует о том, что среднесрочный тренд теперь сильнее долгосрочного базового тренда.

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

Условие 4: 50-дневная скользящая средняя находится выше как 150-дневной, так и 200-дневной. Краткосрочный тренд должен находиться выше обоих долгосрочных трендов. Такое взаимное расположение скользящих средних подтверждает усиление ценового импульса: динамика за последние 50 дней сильнее, чем за предшествующие периоды в 150 и 200 дней.

Условие 5: Цена находится выше 50-дневной скользящей средней. Последний тренд должен обеспечить поддержку текущей цены. Если рынок торгуется ниже своей 50-дневной скользящей средней, такая коррекция ставит под сомнение наличие восходящего ценового импульса.

Условие 6: Цена как минимум на 25 % превышает свой 52-недельный минимум. Это подтверждает, что действительно произошло накопление позиций. Рынок, который поднялся от годового минимума всего на 5 %, еще не набрал значительного ценового импульса. Минервини указывает 25 % в книге «Думай и торгуй как чемпион» (в более ранней книге — 30 %) — мы используем 25 %.

Условие 7: Цена находится не более чем на 25 % ниже своего 52-недельного максимума. Это подтверждает лидерство. Рынок, торгующийся на 60 % ниже годового максимума, сталкивается с серьезным сопротивлением выше текущей цены. Если цена находится не более чем на 25 % ниже максимума, рынок близок к зоне пробоя: он относится к лидерам, а не к отстающим.

Условие 8: Рейтинг относительной силы (RS) не ниже 70. В исходной методике Минервини речь идет о рейтинге относительной силы (RS) издания Investor’s Business Daily — показателе, рассчитываемом по собственной методике издания и отражающем динамику цены акции относительно всех остальных акций за 12 месяцев. Этот рейтинг недоступен для валютных пар в MetaTrader 5. Вместо этого рейтинга мы используем условие, что 14-периодный индекс относительной силы (RSI) находится выше 50, для подтверждения восходящего ценового импульса. Это упрощение сохраняет смысл условия — ценовой импульс должен быть направлен вверх — и при этом позволяет выполнять расчет исключительно на основе ценовых данных.

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


Адаптация шаблона для рынка Форекс

Для применения этого метода к валютным парам в MetaTrader 5 необходимо внести три изменения.

  1. Таймфрейм. Условия Минервини заданы в торговых днях. Дневной таймфрейм в MetaTrader 5 напрямую соответствует этим условиям: 50-дневная, 150-дневная и 200-дневная скользящие средние доступны в виде стандартных индикаторов. Максимум и минимум за 52 недели определяются по последним 252 торговым дням — мы используем 250 баров как близкое значение.
  2. Отсутствие рейтинга относительной силы (RS). Для составления рейтинга относительной силы (RS) IBD требуется база данных всех акций, ранжированных по динамике цен за 12 месяцев. Для валютных пар в MetaTrader 5 аналога такого рейтинга нет. В условии 8 индекс относительной силы (RSI) с периодом 14 и значением выше 50 используется как заменяющий критерий оценки ценового импульса. Это известное упрощение, о котором говорится в разделе «Известные ограничения».
  3. Сигнал входа. Минервини открывает позиции на пробоях паттерна сжатия волатильности (VCP), для выявления которого необходимо определить серию из трёх–четырёх ценовых колебаний с последовательно сужающимся диапазоном внутри базы консолидации. Полноценное выявление VCP — тема для отдельной статьи. В этом советнике используется упрощённый сигнал входа: закрытие выше максимума за 20 баров при объёме торгов не менее 1.5 среднего объёма за 20 баров. Это позволяет учитывать пробой в рамках VCP без выявления и подсчёта ценовых колебаний. Если все восемь условий выполнены и соблюдены критерии пробоя, советник открывает позицию.


Реализация на языке MQL5

Мы создаём советник по частям, объясняя каждый компонент, прежде чем показывать код.

Подключаемые файлы и входные параметры

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

//+------------------------------------------------------------------+
//|                                              TrendTemplateEA.mq5 |
//|                                Copyright 2026, Tola Moses Hector |
//|                                          https://t.me/tolahector |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Tola Moses Hector"
#property link      "https://t.me/tolahector"
#property version   "1.00"
#property description "Automating Classic Market Methods in MQL5 Part 4"
#property description "Mark Minervini Trend Template EA"
#property description "All 8 conditions must pass simultaneously before entry"
#property description "Daily timeframe recommended"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Trend Template MAs ==="
input int    InpMA50Period       = 50;    // 50-day moving average period
input int    InpMA150Period      = 150;   // 150-day moving average period
input int    InpMA200Period      = 200;   // 200-day moving average period
input int    InpMA200SlopeBars   = 20;    // Bars to confirm 200-day MA uptrend

input group "=== 52-Week Range ==="
input int    InpYearBars         = 250;   // Bars representing one year
input double InpMinAboveLowPct   = 25.0;  // Minimum percent above 52-week low
input double InpMaxBelowHighPct  = 25.0;  // Maximum percent below 52-week high

input group "=== Entry Trigger ==="
input int    InpBreakoutBars     = 20;    // Bars for breakout high calculation
input double InpVolBreakoutMult  = 1.5;   // Volume multiplier required at breakout
input int    InpVolAvgPeriod     = 20;    // Bars for average volume calculation
input int    InpRSIPeriod        = 14;    // RSI period for condition 8
input double InpRSIMin           = 50.0;  // Minimum RSI for entry

input group "=== Entry and Risk ==="
input double InpRiskPercent      = 1.0;   // Risk per trade as percent of balance
input double InpRR               = 3.0;   // Risk-reward ratio
input int    InpATRPeriod        = 14;    // ATR period for stop placement
input double InpATRMult          = 2.0;   // ATR multiplier for stop distance

input group "=== Exit ==="
input bool   InpExitBelowMA50   = true;   // Exit if close below 50-day MA
input bool   InpTradeShorts      = false;  // Trade short setups (mirror conditions)

input group "=== General ==="
input int    InpMagicNumber      = 111001; // Magic number
input int    InpSlippage         = 10;     // Slippage in points
input bool   InpShowLabels       = true;   // Draw labels on chart

Входные параметры организованы так, чтобы отражать структуру самого трендового шаблона. Первая группа включает три скользящие средние и период подтверждения наклона. Вторая группа охватывает условия, связанные с 52-недельным диапазоном. Третья группа относится к сигналу входа — сигналу пробоя, который срабатывает после выполнения всех восьми условий шаблона. Обратите внимание, что значение параметра «InpTradeShorts» по умолчанию равно false. Трендовый шаблон, по сути, представляет собой фильтр для бычьего рынка. Торговля короткими позициями с использованием зеркальных условий возможна, но требует тщательной проверки: обратные условия определены менее чётко, чем исходные восемь.

Глобальные переменные и хендлы индикаторов

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade g_trade;                                    // Trade execution object
int    g_ma50_handle  = INVALID_HANDLE;            // 50-day MA handle
int    g_ma150_handle = INVALID_HANDLE;            // 150-day MA handle
int    g_ma200_handle = INVALID_HANDLE;            // 200-day MA handle
int    g_atr_handle   = INVALID_HANDLE;            // ATR handle
int    g_rsi_handle   = INVALID_HANDLE;            // RSI handle
datetime g_last_bar   = 0;                         // Last processed bar time
bool   g_in_trade     = false;                     // Position tracking flag

Пять хендлов индикаторов обеспечивают всё необходимое: три скользящие средние для проверки их взаимного расположения, средний истинный диапазон (ATR) для расчёта расстояния до стоп-лосса и индекс относительной силы (RSI) для условия 8. Все пять создаются один раз в функции «OnInit()» и освобождаются в функции «OnDeinit()».

Помощные функции

Функции «PipSize()» и «CalcLots()» идентичны тем, что приведены в частях 1–3 данной серии. Они приводятся в полном объеме для полноты картины.

//+------------------------------------------------------------------+
//| Returns pip size for the current symbol                          |
//+------------------------------------------------------------------+
double PipSize()
  {
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); // Get symbol digits
   return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point; // Return pip size
  }

//+------------------------------------------------------------------+
//| Calculates lot size from risk percent and stop distance in pips  |
//+------------------------------------------------------------------+
double CalcLots(double sl_pips)
  {
   double balance    = AccountInfoDouble(ACCOUNT_BALANCE);                  // Get balance
   double risk_money = balance * InpRiskPercent / 100.0;                    // Monetary risk
   double tick_val   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);  // Tick value
   double tick_size  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);   // Tick size
   double pip_size   = PipSize();                                           // Get pip size
   if(tick_size <= 0 || tick_val <= 0 || sl_pips <= 0)
      return 0;                                                             // Validate inputs
   double pip_value  = (pip_size / tick_size) * tick_val;                   // Pip monetary value
   double lots       = risk_money / (sl_pips * pip_value);                  // Raw lot size
   double step       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);       // Volume step
   double min_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);        // Minimum lot
   double max_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);        // Maximum lot
   lots = MathFloor(lots / step) * step;                                    // Normalize to step
   return MathMax(min_lot, MathMin(max_lot, lots));                         // Clamp to limits
  }

Отрисовка на графике

//+------------------------------------------------------------------+
//| Places a text label at the specified time and price              |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;                                                               // Check flag
   string obj = "TTM_" + name;                                              // Build object name
   ObjectDelete(0, obj);                                                    // Remove existing
   ObjectCreate(0, obj, OBJ_TEXT, 0, time, price);                          // Create text object
   ObjectSetString(0, obj, OBJPROP_TEXT, text);                             // Set text content
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);                            // Set color
   ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9);                           // Set font size
   ChartRedraw(0);                                                          // Redraw chart
  }

//+------------------------------------------------------------------+
//| Removes all chart objects created by this EA                     |
//+------------------------------------------------------------------+
void ClearLabels()
  {
   int total = ObjectsTotal(0);                                             // Get object count
   for(int i = total - 1; i >= 0; i--)                                      // Iterate backwards
     {
      string name = ObjectName(0, i);                                       // Get object name
      if(StringFind(name, "TTM_") == 0)
         ObjectDelete(0, name);                                             // Delete if EA's
     }
   ChartRedraw(0);                                                          // Redraw chart
  }

Проверка восьми условий

Это ключевая часть советника. Функция «CheckTrendTemplate()» проверяет все восемь условий и возвращает значение true только в том случае, если каждое из них выполняется. Если какое-либо условие не выполняется, функция немедленно возвращает значение false и записывает в журнал, какое именно условие заблокировало вход. Такие записи в журнале важны при тестировании: они позволяют точно определить, какое именно условие отсеивает потенциальные сделки на каждом конкретном баре.

//+------------------------------------------------------------------+
//| Evaluates all 8 Trend Template conditions                        |
//| Returns true only if every condition passes simultaneously       |
//+------------------------------------------------------------------+
bool CheckTrendTemplate()
  {
//--- Copy all indicator values needed
   double ma50_buf[], ma150_buf[], ma200_buf[], rsi_buf[];
   ArraySetAsSeries(ma50_buf,  true);                                                 // Set as series
   ArraySetAsSeries(ma150_buf, true);                                                 // Set as series
   ArraySetAsSeries(ma200_buf, true);                                                 // Set as series
   ArraySetAsSeries(rsi_buf,   true);                                                 // Set as series
   int bars_needed = InpMA200SlopeBars + 2;                                           // Bars needed
   if(CopyBuffer(g_ma50_handle,  0, 1, bars_needed, ma50_buf)  < bars_needed)
      return false;                                                                   // Copy MA50
   if(CopyBuffer(g_ma150_handle, 0, 1, bars_needed, ma150_buf) < bars_needed)
      return false;                                                                   // Copy MA150
   if(CopyBuffer(g_ma200_handle, 0, 1, bars_needed, ma200_buf) < bars_needed)
      return false;                                                                   // Copy MA200
   if(CopyBuffer(g_rsi_handle,   0, 1, 1, rsi_buf)             < 1)
      return false;                                                                   // Copy RSI
   double price   = iClose(_Symbol, PERIOD_CURRENT, 1);                               // Last close
   double ma50    = ma50_buf[0];                                                      // Current MA50
   double ma150   = ma150_buf[0];                                                     // Current MA150
   double ma200   = ma200_buf[0];                                                     // Current MA200
   double ma200_past = ma200_buf[InpMA200SlopeBars];                                  // MA200 one month ago
   double rsi     = rsi_buf[0];                                                       // Current RSI
//--- Get 52-week high and low
   double high_buf[], low_buf[];                                                      // Year high/low buffers
   ArraySetAsSeries(high_buf, true);                                                  // Set as series
   ArraySetAsSeries(low_buf,  true);                                                  // Set as series
   if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, InpYearBars, high_buf) < InpYearBars)
      return false;                                                                   // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, 1, InpYearBars, low_buf)  < InpYearBars)
      return false;                                                                   // Copy lows
   double year_high = 0, year_low = DBL_MAX;                                          // Init year range
   for(int i = 0; i < InpYearBars; i++)                                               // Scan year bars
     {
      year_high = MathMax(year_high, high_buf[i]);                                    // Update year high
      year_low  = MathMin(year_low,  low_buf[i]);                                     // Update year low
     }
//--- Condition 1: Price above MA150 and MA200
   if(price <= ma150)
     { Print("TTM: C1 FAIL — price below MA150"); return false; }                     // Condition 1 failed
   if(price <= ma200)
     { Print("TTM: C1 FAIL — price below MA200"); return false; }                     // Condition 1 failed
//--- Condition 2: MA150 above MA200
   if(ma150 <= ma200)
     { Print("TTM: C2 FAIL — MA150 not above MA200"); return false; }                 // Condition 2 failed
//--- Condition 3: MA200 trending up for at least one month
   if(ma200 <= ma200_past)
     { Print("TTM: C3 FAIL — MA200 not rising"); return false; }                      // Condition 3 failed
//--- Condition 4: MA50 above MA150 and MA200
   if(ma50 <= ma150)
     { Print("TTM: C4 FAIL — MA50 not above MA150"); return false; }                  // Condition 4 failed
   if(ma50 <= ma200)
     { Print("TTM: C4 FAIL — MA50 not above MA200"); return false; }                  // Condition 4 failed
//--- Condition 5: Price above MA50
   if(price <= ma50)
     { Print("TTM: C5 FAIL — price below MA50"); return false; }                      // Condition 5 failed
//--- Condition 6: Price at least 25% above 52-week low
   if(year_low <= 0)
      return false;                                                                   // Invalid year low
   double above_low_pct = ((price - year_low) / year_low) * 100.0;                    // Percent above low
   if(above_low_pct < InpMinAboveLowPct)
     {
      Print(StringFormat("TTM: C6 FAIL — %.1f%% above low (need %.1f%%)",
                         above_low_pct, InpMinAboveLowPct));                          // Log failure
      return false;                                                                   // Condition 6 failed
     }
//--- Condition 7: Price within 25% of 52-week high
   if(year_high <= 0)
      return false;                                                                   // Invalid year high
   double below_high_pct = ((year_high - price) / year_high) * 100.0;                 // Percent below high
   if(below_high_pct > InpMaxBelowHighPct)
     {
      Print(StringFormat("TTM: C7 FAIL — %.1f%% below high (max %.1f%%)",
                         below_high_pct, InpMaxBelowHighPct));                        // Log failure
      return false;                                                                   // Condition 7 failed
     }
//--- Condition 8: RSI above minimum (momentum substitute for RS rating)
   if(rsi < InpRSIMin)
     {
      Print(StringFormat("TTM: C8 FAIL — RSI %.1f below %.1f", rsi, InpRSIMin));      // Log failure
      return false;                                                                   // Condition 8 failed
     }
   Print(StringFormat("TTM: ALL PASS | Price:%.5f | MA50:%.5f | MA150:%.5f | MA200:%.5f | AboveLow:%.1f%% | BelowHigh:%.1f%% | RSI:%.1f",
                      price, ma50, ma150, ma200, above_low_pct, below_high_pct, rsi));// Log pass
   return true;                                                                       // All conditions passed
  }

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

Сигнал входа на пробое

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

//+------------------------------------------------------------------+
//| Returns true if last bar closed above the 20-bar high on volume  |
//+------------------------------------------------------------------+
bool IsBreakout(bool is_long)
  {
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                     // Last bar close
//--- Get breakout reference level
   if(is_long)
     {
      double high_buf[];                                                    // High buffer
      ArraySetAsSeries(high_buf, true);                                     // Set as series
      if(CopyHigh(_Symbol, PERIOD_CURRENT, 2, InpBreakoutBars, high_buf) < InpBreakoutBars)
         return false;                                                      // Copy highs
      double ref_high = 0;                                                  // Reference high
      for(int i = 0; i < InpBreakoutBars; i++)
         ref_high = MathMax(ref_high, high_buf[i]);                         // Find period high
      if(close1 <= ref_high)
         return false;                                                      // No breakout
     }
   else
     {
      double low_buf[];                                                     // Low buffer
      ArraySetAsSeries(low_buf, true);                                      // Set as series
      if(CopyLow(_Symbol, PERIOD_CURRENT, 2, InpBreakoutBars, low_buf) < InpBreakoutBars)
         return false;                                                      // Copy lows
      double ref_low = DBL_MAX;                                             // Reference low
      for(int i = 0; i < InpBreakoutBars; i++)
         ref_low = MathMin(ref_low, low_buf[i]);                            // Find period low
      if(close1 >= ref_low)
         return false;                                                      // No breakout
     }
//--- Check volume confirmation
   long vol_buf[];                                                          // Volume buffer
   ArraySetAsSeries(vol_buf, true);                                         // Set as series
   if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 1, InpVolAvgPeriod + 1, vol_buf) < InpVolAvgPeriod + 1)
      return false;                                                         // Copy volumes
   long   breakout_vol = vol_buf[0];                                        // Breakout bar volume
   double avg_vol      = 0;                                                 // Average accumulator
   for(int i = 1; i <= InpVolAvgPeriod; i++)
      avg_vol += (double)vol_buf[i];                                        // Sum prior volumes
   avg_vol /= InpVolAvgPeriod;                                              // Compute average
   bool vol_ok = ((double)breakout_vol > avg_vol * InpVolBreakoutMult);     // Volume check
   Print(StringFormat("TTM: Breakout | BreakoutVol:%I64d | AvgVol:%.0f | Ratio:%.2f | VolOK:%s",
                      breakout_vol, avg_vol,
                      (double)breakout_vol / avg_vol, vol_ok ? "YES" : "NO")); // Log result
   return vol_ok;                                                           // Return result
  }

Опорный уровень пробоя определяется по барам начиная с бара с индексом 2, без учета текущего бара, чтобы максимум самого бара пробоя не влиял на этот уровень. Это позволяет избежать распространенной ошибки в коде, из-за которой создается впечатление, что бар всегда пробивает собственный максимум. Подтверждение объемом выполняется по той же схеме, что и в частях 1–3: объем текущего бара должен быть больше среднего объема за предыдущие 20 баров, умноженного на 1.5.

Исполнение торговых операций

//+------------------------------------------------------------------+
//| Opens a long trade on Trend Template + breakout confirmation     |
//+------------------------------------------------------------------+
void OpenLong()
  {
   double atr_buf[];                                                        // ATR buffer
   ArraySetAsSeries(atr_buf, true);                                         // Set as series
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;                                                               // Copy ATR
   double atr    = atr_buf[0];                                              // ATR value
   double ask    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);                   // Current ask
   double sl     = ask - atr * InpATRMult;                                  // Stop loss price
   double sl_pip = (ask - sl) / PipSize();                                  // Stop in pips
   double tp     = ask + sl_pip * InpRR * PipSize();                        // Take profit price
   double lots   = CalcLots(sl_pip);                                        // Calculate lot size
   if(lots <= 0)
      return;                                                               // Invalid lot size
   long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);  // Broker stop level
   double min_dist = stop_lv * _Point;                                      // Minimum distance
   if(ask - sl < min_dist)
      sl = ask - min_dist - _Point;                                         // Adjust SL if needed
   if(tp - ask < min_dist)
      tp = ask + min_dist + _Point;                                         // Adjust TP if needed
   if(g_trade.Buy(lots, _Symbol, ask, sl, tp, "Minervini TT Long"))         // Open long
     {
      datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                      // Get bar time
      DrawLabel("ENTRY", t1,
                iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 5,
                "TT LONG", clrDodgerBlue);                                  // Draw label
      Print(StringFormat("TTM: LONG opened | Lots:%.2f | Ask:%.5f | SL:%.5f | TP:%.5f",
                         lots, ask, sl, tp));                               // Log trade
      g_in_trade = true;                                                    // Update flag
     }
  }
//+------------------------------------------------------------------+
//| Opens a short trade on inverted Trend Template + breakdown       |
//+------------------------------------------------------------------+
void OpenShort()
  {
   if(!InpTradeShorts)
      return;                                                               // Shorts disabled
   double atr_buf[];                                                        // ATR buffer
   ArraySetAsSeries(atr_buf, true);                                         // Set as series
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;                                                               // Copy ATR
   double atr    = atr_buf[0];                                              // ATR value
   double bid    = SymbolInfoDouble(_Symbol, SYMBOL_BID);                   // Current bid
   double sl     = bid + atr * InpATRMult;                                  // Stop loss price
   double sl_pip = (sl - bid) / PipSize();                                  // Stop in pips
   double tp     = bid - sl_pip * InpRR * PipSize();                        // Take profit price
   double lots   = CalcLots(sl_pip);                                        // Calculate lot size
   if(lots <= 0)
      return;                                                               // Invalid lot size
   long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);  // Broker stop level
   double min_dist = stop_lv * _Point;                                      // Minimum distance
   if(sl - bid < min_dist)
      sl = bid + min_dist + _Point;                                         // Adjust SL if needed
   if(bid - tp < min_dist)
      tp = bid - min_dist - _Point;                                         // Adjust TP if needed
   if(g_trade.Sell(lots, _Symbol, bid, sl, tp, "Minervini TT Short"))       // Open short
     {
      datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                      // Get bar time
      DrawLabel("ENTRY", t1,
                iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 5,
                "TT SHORT", clrCrimson);                                    // Draw label
      Print(StringFormat("TTM: SHORT opened | Lots:%.2f | Bid:%.5f | SL:%.5f | TP:%.5f",
                         lots, bid, sl, tp));                               // Log trade
      g_in_trade = true;                                                    // Update flag
     }
  }

Управление выходом

Правила выхода из позиций по методу Минервини четко сформулированы. Он закрывает длинные позиции, когда цена закрывается ниже 50-дневной скользящей средней при значительном объеме торгов: это сигнализирует об ослаблении поддержки институциональных инвесторов, лежащей в основе роста на стадии 2. Функция «CheckExit()» проверяет это условие на каждом новом баре для любой открытой позиции.

//+------------------------------------------------------------------+
//| Checks whether open positions should be exited                   |
//+------------------------------------------------------------------+
void CheckExit()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)                                            // Iterate positions
     {
      ulong ticket = PositionGetTicket(i);                                                   // Get ticket
      if(!PositionSelectByTicket(ticket))
         continue;                                                                           // Select position
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)
         continue;                                                                           // Check symbol
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber)
         continue;                                                                           // Check magic
      long   pos_type = PositionGetInteger(POSITION_TYPE);                                   // Position type
      double close1   = iClose(_Symbol, PERIOD_CURRENT, 1);                                  // Last close
      double ma50_buf[];                                                                     // MA50 buffer
      ArraySetAsSeries(ma50_buf, true);                                                      // Set as series
      if(CopyBuffer(g_ma50_handle, 0, 1, 1, ma50_buf) < 1)
         continue;                                                                           // Copy MA50
      double ma50 = ma50_buf[0];                                                             // Current MA50
      bool exit_long  = InpExitBelowMA50 && pos_type == POSITION_TYPE_BUY  && close1 < ma50; // Long exit
      bool exit_short = InpExitBelowMA50 && pos_type == POSITION_TYPE_SELL && close1 > ma50; // Short exit
      if(exit_long || exit_short)                                                            // Exit condition met
        {
         if(g_trade.PositionClose(ticket))                                                   // Close position
           {
            string dir = (pos_type == POSITION_TYPE_BUY) ? "LONG" : "SHORT";                 // Direction
            Print(StringFormat("TTM: %s closed — MA50 breach. Ticket:%I64u", dir, ticket));  // Log exit
            g_in_trade = false;                                                              // Update flag
            ClearLabels();                                                                   // Clear chart
           }
        }
     }
  }

OnInit, OnDeinit и OnTick

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_ma50_handle  = iMA(_Symbol, PERIOD_CURRENT, InpMA50Period,  0, MODE_SMA, PRICE_CLOSE); // Create MA50
   g_ma150_handle = iMA(_Symbol, PERIOD_CURRENT, InpMA150Period, 0, MODE_SMA, PRICE_CLOSE); // Create MA150
   g_ma200_handle = iMA(_Symbol, PERIOD_CURRENT, InpMA200Period, 0, MODE_SMA, PRICE_CLOSE); // Create MA200
   g_atr_handle   = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod);                            // Create ATR
   g_rsi_handle   = iRSI(_Symbol, PERIOD_CURRENT, InpRSIPeriod, PRICE_CLOSE);               // Create RSI
   if(g_ma50_handle  == INVALID_HANDLE ||                                                   // Check MA50
      g_ma150_handle == INVALID_HANDLE ||                                                   // Check MA150
      g_ma200_handle == INVALID_HANDLE ||                                                   // Check MA200
      g_atr_handle   == INVALID_HANDLE ||                                                   // Check ATR
      g_rsi_handle   == INVALID_HANDLE)                                                     // Check RSI
     {
      Print("TrendTemplateEA: Indicator handle creation failed.");                          // Log error
      return INIT_FAILED;                                                                   // Return failure
     }
   g_trade.SetExpertMagicNumber(InpMagicNumber);                                            // Set magic number
   g_trade.SetDeviationInPoints(InpSlippage);                                               // Set slippage
   g_in_trade = false;                                                                      // No initial trade
   g_last_bar = 0;                                                                          // Initialize bar time
   Print("TrendTemplateEA initialized | Symbol:", _Symbol,
         " | TF:", EnumToString(Period()),
         " | Magic:", InpMagicNumber);                                                      // Log initialization
   return INIT_SUCCEEDED;                                                                   // Return success
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_ma50_handle);                                                         // Release MA50
   IndicatorRelease(g_ma150_handle);                                                        // Release MA150
   IndicatorRelease(g_ma200_handle);                                                        // Release MA200
   IndicatorRelease(g_atr_handle);                                                          // Release ATR
   IndicatorRelease(g_rsi_handle);                                                          // Release RSI
   ClearLabels();                                                                           // Remove chart objects
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);                                // Current bar open time
   if(current_bar == g_last_bar)
      return;                                                                               // Skip if same bar
   g_last_bar = current_bar;                                                                // Update last bar time
//--- Check exit conditions for any open position
   CheckExit();                                                                             // Check exit signals
   if(g_in_trade)
      return;                                                                               // No new entry while in trade
//--- Evaluate Trend Template for long setup
   if(CheckTrendTemplate())                                                                 // All 8 conditions pass
     {
      if(IsBreakout(true))                                                                  // Breakout confirmed
        {
         OpenLong();                                                                        // Enter long
         return;                                                                            // Exit tick
        }
     }
//--- Evaluate inverse template for short setup
   if(InpTradeShorts && CheckInverseTemplate())                                             // Inverse conditions pass
     {
      if(IsBreakout(false))                                                                 // Breakdown confirmed
         OpenShort();                                                                       // Enter short
     }
  }

Функция «OnTick()» намеренно сделана простой. Сначала она проверяет условия выхода, затем — условия трендового шаблона, а после этого — сигнал входа на пробое. Функция «CheckInverseTemplate()» для коротких позиций, в которой все восемь условий зеркальны, вызывается только при включенном параметре «InpTradeShorts».

Обратный шаблон для коротких позиций

Когда параметр «InpTradeShorts» включен, советник применяет инвертированные версии всех восьми условий, чтобы выявлять рынки с подтвержденным снижением на стадии 4.

//+------------------------------------------------------------------+
//| Evaluates the inverse of all 8 conditions for short setups       |
//+------------------------------------------------------------------+
bool CheckInverseTemplate()
  {
   double ma50_buf[], ma150_buf[], ma200_buf[], rsi_buf[];
   ArraySetAsSeries(ma50_buf,  true);                                       // Set as series
   ArraySetAsSeries(ma150_buf, true);                                       // Set as series
   ArraySetAsSeries(ma200_buf, true);                                       // Set as series
   ArraySetAsSeries(rsi_buf,   true);                                       // Set as series
   int bars_needed = InpMA200SlopeBars + 2;                                 // Bars needed
   if(CopyBuffer(g_ma50_handle,  0, 1, bars_needed, ma50_buf)  < bars_needed)
      return false;                                                         // Copy MA50
   if(CopyBuffer(g_ma150_handle, 0, 1, bars_needed, ma150_buf) < bars_needed)
      return false;                                                         // Copy MA150
   if(CopyBuffer(g_ma200_handle, 0, 1, bars_needed, ma200_buf) < bars_needed)
      return false;                                                         // Copy MA200
   if(CopyBuffer(g_rsi_handle,   0, 1, 1, rsi_buf)             < 1)
      return false;                                                         // Copy RSI
   double price      = iClose(_Symbol, PERIOD_CURRENT, 1);                  // Last close
   double ma50       = ma50_buf[0];                                         // Current MA50
   double ma150      = ma150_buf[0];                                        // Current MA150
   double ma200      = ma200_buf[0];                                        // Current MA200
   double ma200_past = ma200_buf[InpMA200SlopeBars];                        // MA200 one month ago
   double rsi        = rsi_buf[0];                                          // Current RSI
   double high_buf[], low_buf[];
   ArraySetAsSeries(high_buf, true);                                        // Set as series
   ArraySetAsSeries(low_buf,  true);                                        // Set as series
   if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, InpYearBars, high_buf) < InpYearBars)
      return false;                                                         // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, 1, InpYearBars, low_buf)  < InpYearBars)
      return false;                                                         // Copy lows
   double year_high = 0, year_low = DBL_MAX;                                // Init year range
   for(int i = 0; i < InpYearBars; i++)
     {
      year_high = MathMax(year_high, high_buf[i]);                          // Update year high
      year_low  = MathMin(year_low,  low_buf[i]);                           // Update year low
     }
//--- Inverse conditions: price below MAs, MAs stacked downward, MA200 declining
   if(price >= ma150)
      return false;                                                         // Price above MA150 — skip
   if(price >= ma200)
      return false;                                                         // Price above MA200 — skip
   if(ma150 >= ma200)
      return false;                                                         // MA150 not below MA200 — skip
   if(ma200 >= ma200_past)
      return false;                                                         // MA200 not declining — skip
   if(ma50  >= ma150)
      return false;                                                         // MA50 not below MA150 — skip
   if(ma50  >= ma200)
      return false;                                                         // MA50 not below MA200 — skip
   if(price >= ma50)
      return false;                                                         // Price above MA50 — skip
   if(year_high <= 0)
      return false;                                                         // Invalid year high
   double below_high_pct = ((year_high - price) / year_high) * 100.0;       // Percent below high
   if(below_high_pct < InpMinAboveLowPct)
      return false;                                                         // Not far enough below high
   if(year_low <= 0)
      return false;                                                         // Invalid year low
   double above_low_pct = ((price - year_low) / year_low) * 100.0;          // Percent above low
   if(above_low_pct > InpMaxBelowHighPct)
      return false;                                                         // Too far above low
   if(rsi > (100.0 - InpRSIMin))
      return false;                                                         // RSI too high for short
   Print("TTM: INVERSE ALL PASS — short setup confirmed");                  // Log pass
   return true;                                                             // All inverse conditions passed
  }
//+------------------------------------------------------------------+

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


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

Чтобы протестировать советник, откройте тестер стратегий MetaTrader 5 и задайте следующие настройки:

Настройки тестирования

  • Символ: EURUSD
  • Временной интервал: Дневной (D1)
  • Режим моделирования: Каждый тик на основе реальных тиков
  • Начальный депозит: 10 000 долларов
  • Период тестирования: 1 января 2015 г. – 31 декабря 2024 г.

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

Входные параметры советника

Скользящие средние

  • InpMA50Period = 50
  • InpMA150Period = 150
  • InpMA200Period = 200
  • InpMA200SlopeBars = 20

Фильтры по ценовому диапазону и пробою

  • InpYearBars = 250
  • InpMinAboveLowPct = 25
  • InpMaxBelowHighPct = 25
  • InpBreakoutBars = 20
  • InpVolBreakoutMult = 1.5

Фильтр по индексу относительной силы (RSI)

  • InpRSIPeriod = 14
  • InpRSIMin = 50

Управление рисками

  • InpRiskPercent = 1.0
  • InpRR = 3.0

Настройки стоп-лосса на основе среднего истинного диапазона (ATR)

  • InpATRPeriod = 14
  • InpATRMult = 2.0

Чего ожидать

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

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

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

Входные параметры для демонстрации

Рис. 1. Входные параметры для демонстрации


Демонстрация

Рис. 2. Демонстрация

График баланса и средств

Рис. 3. График баланса и средств

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

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

Результаты тестирования: входы в рынок

Рис. 5. Результаты тестирования — входы в рынок


Известные ограничения

Рейтинг относительной силы (RS) невозможно воспроизвести в MQL5. Восьмое условие Минервини требует использования рейтинга относительной силы (RS) от Investor’s Business Daily — оценки по закрытой методике, отражающей динамику цены акции относительно всех остальных акций за последние 12 месяцев. Для валютных пар аналогов нет. Замена этого требования условием «индекс относительной силы (RSI) выше 50» сохраняет его направленность на оценку ценового импульса, но является упрощением. Это самое существенное отклонение от первоначальной спецификации Минервини.

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

Для расчета 200-дневной скользящей средней требуется значительный исторический ряд данных. Для надежного расчета 200-дневной скользящей средней советнику требуется не менее 250 баров истории до вызова «OnInit()». В тестере стратегий убедитесь, что доступен достаточный объем исторических данных за период до даты начала тестирования. Настройка «Период прогрева» в тестере позволяет добавить историю за период до даты начала тестирования.

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

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


Заключение

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

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

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

Весь код был скомпилирован и протестирован в MetaTrader 5. Полный файл исходного кода «TrendTemplateEA.mq5» прилагается к этой статье. Скопируйте файл в папку «MQL5\Experts\» и скомпилируйте его в MetaEditor; дополнительные зависимости не требуются. Перед запуском на реальном счете всегда проводите тестирование на демо-счете.

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

Прикрепленные файлы |
TrendTemplateEA.mq5 (34.04 KB)
Изолирующий лес средствами MQL5: Обнаружение аномалий качества исполнения заявок Изолирующий лес средствами MQL5: Обнаружение аномалий качества исполнения заявок
Пошаговое руководство по созданию изолирующего леса (Isolation Forest), реализованного непосредственно на MQL5 и ориентированного на показатели исполнения, а не на цену. В руководстве подробно описаны пять признаков, построение деревьев и расчет оценки аномальности по длине пути, обучение на скользящем окне, ведение CSV-журнала, а также сохранение и восстановление модели с использованием FILE_COMMON. Все это интегрировано в OnTradeTransaction(). Полученный механизм защитной остановки торговли (circuit breaker) в реальном времени выявляет необычные исполнения заявок и применяет контролируемые меры реагирования, чтобы стабилизировать реальную торговлю при изменении условий исполнения.
Разработка собственного торгового советника (Часть 1): От ненадежного скрипта к работающему советнику Разработка собственного торгового советника (Часть 1): От ненадежного скрипта к работающему советнику
В этой статье основное внимание уделяется архитектуре торгового советника, а не разработке торговых сигналов. Взяв за основу советник с недостатками, использующий пересечение скользящих средних, мы добавляем определение появления нового бара для предотвращения повторных входов, магический номер и учет открытых позиций, уровни стоп-лосса и тейк-профита, рассчитываемые на основе ATR, а также проверку данных и результатов торговых операций и базовые защитные механизмы. Вы получаете практическую базу для создания и тестирования сложных систем.
Как обнаруживать и нормализовать объекты графика в MQL5 (Часть 2): Сбор и структурирование данных из сложных аналитических объектов Как обнаруживать и нормализовать объекты графика в MQL5 (Часть 2): Сбор и структурирование данных из сложных аналитических объектов
Нарисованные вручную аналитические объекты, такие как инструменты Фибоначчи и вилы Эндрюса, не видны автоматизированной торговой логике. В статье предлагается расширение базового детектора, позволяющее извлекать опорные точки, массивы уровней и геометрические смещения из сложных объектов. Вам предстоит реализовать многократно используемый сборщик, который нормализует необработанные данные графика, преобразуя их в структурированные массивы в памяти для принятия решений в рамках торговой стратегии.
MetaTrader 5 как производитель сообщений Kafka: Рассылка сигналов нескольким торговым терминалам MetaTrader 5 как производитель сообщений Kafka: Рассылка сигналов нескольким торговым терминалам
В статье подробно описан производитель Kafka, реализованный непосредственно на MQL5 и использующий сетевой протокол Kafka через TCP-сокеты без дополнительных библиотек. Он реализует кодирование RecordBatch v2, varint и вычисление CRC32C, а также пакетную отправку, подтверждения (acks) и логику повторных попыток — и всё это без вспомогательного процесса или DLL. Используйте его для публикации торговых сигналов в формате JSON из одного терминала в Kafka, где информационные панели и другие сервисы подписываются на них независимо друг от друга.