Автоматизация классических рыночных методов в MQL5 (Часть 4): Трендовый шаблон Марка Минервини
Введение
В третьей части этой серии мы автоматизировали анализ стадий Стэна Вайнштейна — методологию, которая выделяет четыре стадии жизненного цикла рынка и предусматривает торговлю только на росте в стадии 2. Вайнштейн задает один вопрос: на каком этапе находится этот рынок? Марк Минервини задает более сложный вопрос: соответствует ли данный рынок одновременно всем восьми конкретным структурным требованиям?
Минервини — один из ныне живущих трейдеров, чья деятельность изучена наиболее подробно. Он выиграл чемпионат США по инвестированию в 1997 году с подтвержденной аудитом доходностью 155 % и вновь в 2021 году с подтвержденной аудитом доходностью 334.8 % в категории «миллион долларов». Его метод, который он называет SEPA — анализ конкретной точки входа, начинается с трендового шаблона: контрольного списка из восьми технических условий, которым рынок должен соответствовать, прежде чем вообще можно будет переходить к анализу его графика. Если хотя бы одно из условий не выполняется, инструмент исключается из рассмотрения. Без исключений.
Сила трендового шаблона заключается не в каком-либо отдельном условии. Каждое из этих условий, взятое отдельно, представляет собой базовый технический фильтр. Сила заключается в том, что необходимо одновременное выполнение всех восьми условий. Эта комбинация позволяет отбирать рынки с определенной структурой тренда: скользящие средние расположены в правильном порядке, цена находится выше них, расстояние от годового минимума подтверждает ценовой импульс, близость к годовому максимуму подтверждает лидерство, а 200-дневная скользящая средняя растет, что указывает на устойчивую поддержку институциональных инвесторов.
Трендовый шаблон был разработан для акций, к которым Минервини также применяет фундаментальные фильтры и рейтинг относительной силы (RS) от издания Investor’s Business Daily. В данной статье семь технических условий адаптированы для валютных пар на дневном таймфрейме. Восьмое условие (рейтинг относительной силы (RS)) невозможно воспроизвести в MQL5 без внешнего источника данных, поэтому оно заменяется фильтром ценового импульса на основе 14-периодного индекса относительной силы (RSI). Сигнал входа — пробой в паттерне сжатия волатильности (VCP), который представляет собой серию все более узких ценовых колебаний, завершающуюся пробоем сопротивления на высоком объеме. Здесь этот сигнал упрощен до пробоя максимума за 20 баров на растущем объеме.
Мы рассмотрим следующие темы:
- Восемь условий и то, что они измеряют
- Адаптация трендового шаблона для рынка Forex
- Реализация на языке MQL5
- Тестирование на исторических данных
- Известные ограничения
- Заключение
Восемь условий и то, что они измеряют
Ниже приведены восемь условий Минервини в том виде, в каком он их сформулировал в книге «Думай и торгуй как чемпион». Перед тем как реализовать их в коде, необходимо понять, что именно измеряет каждое из них.
Условие 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 необходимо внести три изменения.
- Таймфрейм. Условия Минервини заданы в торговых днях. Дневной таймфрейм в MetaTrader 5 напрямую соответствует этим условиям: 50-дневная, 150-дневная и 200-дневная скользящие средние доступны в виде стандартных индикаторов. Максимум и минимум за 52 недели определяются по последним 252 торговым дням — мы используем 250 баров как близкое значение.
- Отсутствие рейтинга относительной силы (RS). Для составления рейтинга относительной силы (RS) IBD требуется база данных всех акций, ранжированных по динамике цен за 12 месяцев. Для валютных пар в MetaTrader 5 аналога такого рейтинга нет. В условии 8 индекс относительной силы (RSI) с периодом 14 и значением выше 50 используется как заменяющий критерий оценки ценового импульса. Это известное упрощение, о котором говорится в разделе «Известные ограничения».
- Сигнал входа. Минервини открывает позиции на пробоях паттерна сжатия волатильности (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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.