Разработка инструментария для анализа Price Action (Часть 70): Автоматическое исполнение сделок по сигналам паттерна "флаг"
Содержание
- Введение
- Почему одних графических объектов недостаточно
- Архитектура сигналов
- Реализация слоя обмена данными для индикатора на MQL5
- Реализация слоя исполнения советника на MQL5
- Тестирование и проверка
- Заключение
Введение
Умение распознавать на графике паттерны "флаг" полезно, но одного визуального распознавания недостаточно, если мы хотим торговать по ним автоматически. В предыдущей статье (Часть 69) мы создали детектор паттернов "флаг" в реальном времени, который с помощью графических объектов рисует завершенные бычьи и медвежьи формации. Проблема в том, что для трейдера, который смотрит на график, эти построения удобны, но советник не может надежно использовать их как источник данных. Советнику непросто интерпретировать стрелки, трендовые линии или прямоугольники, если индикатор не передает сигналы в четком структурированном формате, который советник может считывать напрямую.
В Части 70 мы превращаем этот детектор в источник сигналов для автоматического исполнения сделок. Индикатор передает данные о пробое через три буфера: покупки, продажи и высоты флагштока. Затем советник считывает эти буферы через CopyBuffer(), применяет при необходимости дополнительные фильтры и исполняет сделки только при появлении подтвержденного сигнала.
Почему одних графических объектов недостаточно
Детектор флагов, который мы создали в Части 69, предназначен для наглядного отображения завершенных паттернов на графике. Как только бычий или медвежий флаг подтверждался, индикатор отображал структуру с помощью трендовых линий, прямоугольников, меток и стрелок пробоя. Такой подход эффективен для визуального анализа, потому что трейдер может сразу оценить сетап и решить, заслуживает ли он внимания.

Графические объекты полезны для визуализации, но не подходят как надежный канал данных для советника. Буферы решают эту проблему, предоставляя индикатору структурированный формат вывода данных, который советник может считывать напрямую.
Буферы индикатора решают эту задачу гораздо лучше:
- в советнике используется CopyBuffer() – это стандартный метод, который используется в советниках MQL5 для чтения индикаторных буферов;
- чтение данных из буферов выполняется быстро и эффективно;
- индикатор отвечает за обнаружение, а советник – за торговые решения;
- после создания одну и ту же систему сигналов можно использовать в разных советниках без дополнительной доработки.
Поэтому обновленный детектор теперь передает сигналы через отдельные буферы стрелок и дополнительный буфер высоты флагштока. Это небольшое изменение превращает индикатор из визуального инструмента в надежный источник сигналов для автоматизации.
Архитектура сигналов
Прежде чем создавать советника, нужно было точно определить, как индикатор будет передавать ему информацию. Решением стало использование трех отдельных буферов.
- BufferBuy[] хранит сигналы бычьего пробоя;
- BufferSell[] хранит сигналы медвежьего пробоя;
- а BufferPoleHeight[] хранит измеренную высоту флагштока для расчета параметров сделки.

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

В этой реализации добавлены три компонента:
- буферы сигналов покупки и продажи;
- скрытый буфер высоты флагштока для управления сделкой;
- и логику записи сигнала о пробое, которая срабатывает только после подтверждения.
1. Преобразование индикатора в источник сигналов на основе буферов
Первым изменением стало добавление трех буферов индикатора, чтобы советник мог считывать сигналы напрямую. Вместо того чтобы полностью полагаться на графические объекты, нарисованные на графике, индикатор теперь передает структурированные данные о пробоях через буферы, к которым советник может обращаться напрямую.
В обновленной реализации вводятся три буфера. Два из них отвечают за направленные сигналы пробоя, а третий хранит измеренную высоту флагштока для управления сделкой. BufferBuy[] получает значение только при пробое бычьего флага вверх. BufferSell[] работает аналогично для медвежьих пробоев. Все остальные значения остаются пустыми. Третий буфер, BufferPoleHeight[], сохраняет фактическую высоту флагштока на том же баре, чтобы советник мог использовать ее позже для управления сделкой.
#property copyright "Copyright 2026, Christian Benjamin." #property link "https://www.mql5.com/ru/users/lynnchris" #property version "2.0" #property indicator_chart_window #property indicator_buffers 3 #property indicator_plots 2 //--- visible arrow plots #property indicator_label1 "Buy arrow" #property indicator_type1 DRAW_ARROW #property indicator_color1 clrLime #property indicator_width1 2 #property indicator_label2 "Sell arrow" #property indicator_type2 DRAW_ARROW #property indicator_color2 clrRed #property indicator_width2 2 //--- arrow symbol codes (Wingdings) #define ARROW_UP 233 #define ARROW_DOWN 234 //--- buffers double BufferBuy[]; // holds bullish breakout signals (arrow placement) double BufferSell[]; // holds bearish breakout signals double BufferPoleHeight[]; // stores flagpole height for EA
Третий буфер, BufferPoleHeight[], не используется для визуализации. Его задача – сохранять размер обнаруженного флагштока в ценовых единицах, чтобы советник позже мог рассчитывать пропорциональные уровни стоп-лосса и тейк-профита на основе фактической структуры паттерна.
Видимые буферы настроены как направленные стрелки на графике: бычьи сигналы появляются под свечой пробоя, а медвежьи – над ней. Это делает визуальную часть индикатора наглядной для ручного анализа и одновременно создает удобный числовой слой передачи данных для автоматической торговли.
2. Инициализация буферов и настройка отображения
После объявления буферов мы настраиваем их должным образом в OnInit(). Здесь мы подготавливаем вычислительную среду, подключаем буферы к системе отрисовки MetaTrader и задаем структуру, которую позже будет считывать советник.
В OnInit() мы начинаем с создания хэндла ATR. Этому индикатору ATR нужен для проверки волатильности, поэтому, если создать хэндл не удается, мы сразу завершаем инициализацию с INIT_FAILED.
//+------------------------------------------------------------------+ //| Custom indicator initialization | //+------------------------------------------------------------------+ int OnInit() { //--- create ATR handle (20-period) atrHandle=iATR(_Symbol,PERIOD_CURRENT,20); if(atrHandle==INVALID_HANDLE) return INIT_FAILED; //--- bind buffers SetIndexBuffer(0,BufferBuy,INDICATOR_DATA); SetIndexBuffer(1,BufferSell,INDICATOR_DATA); SetIndexBuffer(2,BufferPoleHeight,INDICATOR_CALCULATIONS); //--- set arrow symbols PlotIndexSetInteger(0,PLOT_ARROW,ARROW_UP); PlotIndexSetInteger(1,PLOT_ARROW,ARROW_DOWN); //--- ensure standard indexing (oldest bar at index 0) ArraySetAsSeries(BufferBuy,false); ArraySetAsSeries(BufferSell,false); ArraySetAsSeries(BufferPoleHeight,false); //--- initialize buffers with empty values ArrayInitialize(BufferBuy,EMPTY_VALUE); ArrayInitialize(BufferSell,EMPTY_VALUE); ArrayInitialize(BufferPoleHeight,0.0); if(DebugMode) Print("Flag Detector v2.0 ",_Symbol); return INIT_SUCCEEDED; }
Буферы связываются с помощью SetIndexBuffer(), при этом высота флагштока хранится в вычислительном буфере, так как на графике она не отображается. Затем мы задаем символы стрелок с помощью PlotIndexSetInteger(), чтобы бычьи сигналы отображались стрелками вверх, а медвежьи – стрелками вниз. Благодаря этому подтверждения пробоя легко считываются прямо с графика.
Еще один важный шаг – задать ориентацию массивов. Настраивая буферы через ArraySetAsSeries(..., false), мы сохраняем хронологическую индексацию – от старых баров к новым. Это упрощает историческое сканирование, отслеживание активных паттернов и синхронизацию пробоев.
Наконец, мы инициализируем буферы значениями по умолчанию. Буферы покупки и продажи заполняются EMPTY_VALUE, поэтому сигнал появляется только при подтвержденном пробое. Буфер высоты флагштока инициализируется значением 0.0, так как он хранит измерительные данные, а не данные для отображения. Это дает советнику чистую структуру сигналов и позволяет не засорять график ложными или неинициализированными значениями.
3. Подтверждение пробоя и запись сигнала
Ключевая часть всей системы сосредоточена в функции UpdateActiveFlag(). Она непрерывно отслеживает каждую активную структуру флага, определяет, произошел ли пробой, и записывает сигнал только после подтверждения.
Функция загружает структуру из activeFlags[] и пропускает бар, если он уже был обработан, чтобы избежать дублирования вычислений при обновлениях в реальном времени.
Мы подтверждаем пробой только после закрытия свечи. Для бычьего флага цена должна закрыться выше верхней границы флага. Для медвежьего флага цена должна закрыться ниже нижней границы флага. Это делает сигналы надежнее, поскольку отфильтровывает множество ложных пробоев, которые возникают лишь на короткое время во время формирования свечи, прежде чем цена вернется в диапазон консолидации.
//+------------------------------------------------------------------+ //| Update an active flag – check for breakout or invalidation | //+------------------------------------------------------------------+ bool UpdateActiveFlag(int index,const double &high[],const double &low[], const double &close[],const datetime &time[], int newBar,int rates_total) { ActiveFlag af=activeFlags[index]; //--- already processed this bar if(newBar<=af.lastUpdate) return false; bool breakout=false; if(af.isBull) { if(close[newBar]>af.poleHigh) breakout=true; } else { if(close[newBar]<af.poleLow) breakout=true; } if(breakout) { if(IsTooClose(af.flagStart,newBar,af.isBull)) return true; //--- draw the completed pattern DrawSlantedPattern(af.poleStart,af.poleEnd,af.flagStart,newBar,af.isBull,high,low,time); //--- fill indicator buffers (signal output) if(af.isBull && newBar<ArraySize(BufferBuy)) { BufferBuy[newBar]=low[newBar]-10*_Point; if(newBar<ArraySize(BufferPoleHeight)) BufferPoleHeight[newBar]=af.poleLength; } else if(!af.isBull && newBar<ArraySize(BufferSell)) { BufferSell[newBar]=high[newBar]+10*_Point; if(newBar<ArraySize(BufferPoleHeight)) BufferPoleHeight[newBar]=af.poleLength; } //--- alert and log string msg=StringFormat("Flag Detector: %s flag on %s %s at %s", af.isBull?"Bull":"Bear",_Symbol,EnumToString(Period()), TimeToString(time[newBar])); DoAlert(msg,EnableSound,EnableNotification,EnableEmail); if(DebugMode) Print((af.isBull?"BULL":"BEAR")," Flag | ",TimeToString(time[newBar]), " | Pole height: ",af.poleLength); //--- record as drawn RecordDrawnFlag(af.poleStart,af.poleEnd,af.flagStart,newBar, time[af.flagStart],time[newBar],af.isBull); return true; } //--- no breakout: update extremes and check invalidation if(af.isBull) { if(low[newBar]<af.extreme) af.extreme=low[newBar]; } else { if(high[newBar]>af.extreme) af.extreme=high[newBar]; } double retrace=af.isBull?(af.poleHigh-af.extreme)/af.poleLength*100 :(af.extreme-af.poleLow)/af.poleLength*100; if(retrace>MaxRetracePercent) return true; if(MaxFlagBars>0 && newBar-af.flagStart+1>MaxFlagBars) return true; // too long //--- update structural counters if(newBar>af.flagStart) { int prev=newBar-1; if(af.isBull) { if(high[newBar]<high[prev]) af.pullbacks++; if(low[newBar] >low[prev]) af.pushes++; } else { if(low[newBar] >low[prev]) af.pullbacks++; if(high[newBar]<high[prev]) af.pushes++; } } af.lastUpdate=newBar; activeFlags[index]=af; return false; }
После подтверждения пробоя функция использует IsTooClose(), чтобы проверить, не слишком ли сильно она перекрывается с ранее завершенным паттерном. Это предотвращает появление повторяющихся сигналов во время плотных консолидаций, когда несколько формаций могут выглядеть почти одинаково.
После успешной проверки индикатор визуально завершает структуру с помощью DrawSlantedPattern(). После этого подтвержденный пробой записывается напрямую в сигнальные буферы. Бычьи сигналы записываются в BufferBuy[], а медвежьи – в BufferSell[]. Значения стрелок немного смещаются от свечи на величину _Point, чтобы маркеры оставались визуально четкими на графике.
В то же время измеренная высота флагштока сохраняется в BufferPoleHeight[] на той же свече пробоя. Синхронизация направленного сигнала и структурного измерения на одном и том же баре позволяет советнику впоследствии рассчитывать пропорциональные уровни стоп-лосса и тейк-профита непосредственно по обнаруженному паттерну.
Мы также оставили активной систему алертов, поэтому подтвержденные пробои могут вызывать звуковые сигналы, push-уведомления или уведомления по электронной почте в зависимости от включенных настроек. В режиме отладки также выводится дополнительная диагностическая информация.
Если пробой не происходит, структура остается под наблюдением. Функция обновляет экстремумы отката, пересчитывает глубину отката и признает сетап недействительным, если откат превышает допустимый порог. MaxFlagBars также используется, чтобы паттерны не оставались активными слишком долго после утраты структурной значимости.
4. Историческая стабильность и отсутствие перерисовки
Одна из главных задач при создании надежных советников – добиться того, чтобы сигналы одинаково вели себя как при тестировании на исторических данных, так и в реальной торговле. Если сигналы ведут себя по-разному при тестировании на исторических данных и в реальной торговле, стратегия становится ненадежной, независимо от того, насколько точным может казаться визуальное обнаружение.
Чтобы сохранить согласованность, индикатор построен на контролируемом обновлении буферов внутри OnCalculate(). Как только сигнал пробоя записывается в буфер на подтвержденной свече, он больше не изменяется при последующих перерасчетах.
//+------------------------------------------------------------------+ //| Main calculation loop | //+------------------------------------------------------------------+ int OnCalculate(const int32_t rates_total, const int32_t prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int32_t &spread[]) { //--- copy ATR buffer for all available bars if(CopyBuffer(atrHandle,0,0,rates_total,atrBuffer)!=rates_total) return 0; //--- reset buffer values for newly arrived bars (prevents repainting) for(int i=prev_calculated;i<rates_total;i++) { if(i<ArraySize(BufferBuy)) BufferBuy[i]=EMPTY_VALUE; if(i<ArraySize(BufferSell)) BufferSell[i]=EMPTY_VALUE; if(i<ArraySize(BufferPoleHeight)) BufferPoleHeight[i]=0.0; } //--- first run: full historical scan + initial active search if(prev_calculated==0) { ScanHistoricalFlags(rates_total,open,high,low,close,time); int activeScanStart=MathMax(0,rates_total-LookbackBars); for(int i=activeScanStart;i<=rates_total-MinFlagBars-1;i++) { int moveStart,moveEnd; bool isBullMove; if(FindThreeBarMove(i,rates_total,open,close,atrBuffer,moveStart,moveEnd,isBullMove)) TryAddActiveFlag(moveStart,moveEnd,isBullMove,high,low,time,rates_total); } } else { int newBars=rates_total-prev_calculated; if(newBars>0) { //--- update existing active flags (remove if broken/invalid) for(int i=ArraySize(activeFlags)-1;i>=0;i--) { bool remove=false; for(int bar=prev_calculated;bar<rates_total;bar++) if(UpdateActiveFlag(i,high,low,close,time,bar,rates_total)) {remove=true;break;} if(remove) RemoveActiveFlag(i); } //--- scan recent bars for new flagpoles (window = max(50, LookbackBars)) int scanWindow=MathMax(50,LookbackBars); int newPoleScanStart=MathMax(0,rates_total-scanWindow); for(int i=newPoleScanStart;i<=rates_total-3;i++) { int moveStart,moveEnd; bool isBullMove; if(FindThreeBarMove(i,rates_total,open,close,atrBuffer,moveStart,moveEnd,isBullMove)) TryAddActiveFlag(moveStart,moveEnd,isBullMove,high,low,time,rates_total); } } } return rates_total; } //+------------------------------------------------------------------+
Это поведение реализовано с помощью prev_calculated, который позволяет индикатору отделять историческую обработку от инкрементальных обновлений. Вместо перерасчета всего набора данных на каждом тике система обрабатывает только новые бары между prev_calculated и rates_total.
В OnCalculate() код сбрасывает значения буферов только для вновь добавленных баров; ранее подтвержденные сигналы остаются без изменений, что предотвращает перерисовку. BufferBuy[] и BufferSell[] очищаются значением EMPTY_VALUE, а BufferPoleHeight[] сбрасывается до 0.0. Поскольку подтвержденные сигналы лежат вне диапазона перерасчета, они остаются неизменными при последующих обновлениях.
Реализация также включает дополнительную структурную фильтрацию для подавления дублирующихся формаций. Во время волатильных консолидаций ценовое движение может за короткое время формировать перекрывающиеся флаговые структуры. Проверки перекрытия гарантируют, что сигналы пробоя будут генерироваться только для достаточно различимых и значимых формаций.
Реализация слоя исполнения советника на MQL5
Теперь, когда индикатор передает чистые сигналы через буферы, мы можем создать советник, который будет непосредственно открывать сделки. На этом этапе советник не пытается самостоятельно обнаруживать паттерны "флаг". Эту роль уже выполняет индикатор. Вместо этого советник использует OnInit() для загрузки пользовательского индикатора, OnTick() – для отслеживания новых баров, PassesFilters() – для проверки сигнала, а ExecuteTrade() – для открытия позиции, когда все условия выполнены.
Благодаря такому разделению код проще сопровождать. Индикатор отвечает за поиск паттернов и генерацию сигналов, а советник – только за фильтры, управление рисками и исполнение сделок. В результате оба компонента сохраняют свою специализацию, их проще сопровождать, а риск дублирования логики снижается.
1. Загрузка индикатора в OnInit()
Мы начинаем настройку советника в OnInit() с загрузки индикатора и подготовки всего необходимого. Первый шаг – загрузить пользовательский детектор флагов через iCustom(). Это создает хэндл индикатора, через который советник получает доступ к создаваемым индикатором буферам сигналов покупки, сигналов продажи и высоты флагштока. Если хэндл не удается инициализировать, советник возвращает INIT_FAILED, поскольку недоступен корректный источник сигнала.
//+------------------------------------------------------------------+ //| Expert initialization – load indicator and filter handles | //+------------------------------------------------------------------+ int OnInit() { //--- Load the custom flag pattern detector indiHandle = iCustom(_Symbol, _Period, "Flag_Pattern_Detector"); if(indiHandle == INVALID_HANDLE) return INIT_FAILED; //--- Set the magic number for trade identification trade.SetExpertMagicNumber(MagicNumber); //--- Create trend filter handle (if enabled) if(UseTrendFilter) maHandle = iMA(_Symbol, MA_Timeframe, MA_Period, 0, MODE_SMA, PRICE_CLOSE); //--- Create volume confirmation handle (if enabled) if(UseVolumeConfirmation) volMAHandle = iMA(_Symbol, _Period, VolumeMA_Period, 0, MODE_SMA, VOLUME_TICK); //--- Create EMA exit handle (if enabled) if(UseEMAExit) emaExitHandle = iMA(_Symbol, EMAExitTimeframe, EMAExitPeriod, 0, EMAExitMethod, PRICE_CLOSE); //--- Reset internal state eaStartTime = 0; hasPosition = false; return INIT_SUCCEEDED; }
Советник задает магическое число через trade.SetExpertMagicNumber(), чтобы управлять своими позициями независимо от других сделок. Мы также создаем хэндлы для необязательных фильтров – трендового и объемного – а также для выхода по EMA, если они включены. Если включена фильтрация по тренду, iMA() создает хэндл скользящей средней для подтверждения направления. Если включено подтверждение по объему, создается еще один хэндл скользящей средней для усреднения тикового объема. Советник также инициализирует хэндл EMA для управления выходом, если включен выход по EMA.
Наконец, при инициализации сбрасывается внутреннее состояние исполнения: сбрасываются переменные eaStartTime и hasPosition. Это гарантирует, что советник начнет работу с чистого рабочего состояния, прежде чем приступит к мониторингу сигналов пробоя в реальном времени через буферы индикатора.
2. Чтение буферов в OnTick()
В OnTick() выполняется основная логика. Сначала один раз запускается ScanStartupSignals(), чтобы распознать любой корректный сигнал, уже существовавший на момент подключения советника. После этого советник переходит в режим обычного мониторинга в реальном времени.
Если позиция уже открыта, OnTick() не ищет новые точки входа. Вместо этого функция управляет позицией: проверяет, что сделка все еще существует, и, если UseEMAExit включен, вызывает CheckEMAExit(). Это упорядочивает логику исполнения и не позволяет советнику открывать новые сделки, пока он еще сопровождает активную позицию.
//+------------------------------------------------------------------+ //| Main tick handler – entry signals and position management | //+------------------------------------------------------------------+ void OnTick() { //--- One‑time startup scan to catch signals that occurred right at attachment if(eaStartTime == 0) { eaStartTime = iTime(_Symbol, _Period, 0); ScanStartupSignals(); } //--- If a position is open, manage it (EMA exit). No new entries. if(hasPosition) { //--- Check if the position still exists if(!PositionSelectByTicket(currentTicket)) { hasPosition = false; return; } //--- Apply EMA exit rule if enabled if(UseEMAExit) CheckEMAExit(); return; } //--- No open position: wait for a new bar to check for entry signals static datetime prevBarTime = 0; datetime currBarTime = iTime(_Symbol, _Period, 0); if(currBarTime == prevBarTime) return; prevBarTime = currBarTime; //--- Prevent trading bars before the EA started (avoids backtest future‑leak) if(currBarTime < eaStartTime) return; //--- Copy the last two bars of each indicator buffer if(CopyBuffer(indiHandle, 0, 0, 2, bufBuy) != 2 || CopyBuffer(indiHandle, 1, 0, 2, bufSell) != 2 || CopyBuffer(indiHandle, 2, 0, 2, bufPoleHeight) != 2) return; //--- Examine the just‑closed bar (index 1) datetime closedBarTime = iTime(_Symbol, _Period, 1); bool newBuy = (bufBuy[1] != EMPTY_VALUE && bufBuy[1] != 0); bool newSell = (bufSell[1] != EMPTY_VALUE && bufSell[1] != 0); //--- If a fresh signal is present and passes all filters, execute the trade if((newBuy || newSell) && closedBarTime >= eaStartTime) { if(newBuy && closedBarTime != lastBuyBarTime && PassesFilters(true, closedBarTime)) { ExecuteTrade(true, bufPoleHeight[1]); lastBuyBarTime = closedBarTime; } else if(newSell && closedBarTime != lastSellBarTime && PassesFilters(false, closedBarTime)) { ExecuteTrade(false, bufPoleHeight[1]); lastSellBarTime = closedBarTime; } } }
Когда позиции нет, советник ждет нового бара, прежде чем проверять буферы индикатора. Мы используем простую проверку по времени бара, чтобы обрабатывать каждую закрытую свечу только один раз. Советник также игнорирует все бары, существовавшие до запуска, что помогает при тестировании на исторических данных избежать смещения из-за заглядывания вперед.
На новом баре OnTick() считывает через CopyBuffer() последние два значения буферов сигналов покупки и продажи, а также буфера высоты флагштока. Затем функция оценивает последний закрытый бар (индекс 1) и, если сигнал новый и проходит PassesFilters(), вызывает ExecuteTrade() с соответствующей высотой флагштока.
Эта функция служит связующим звеном между индикатором и слоем исполнения. Она считывает сигнал, проверяет его и передает в торговую логику только один раз для каждого подтвержденного пробоя.
3. Сканирование при запуске в ScanStartupSignals()
Мы добавили функцию ScanStartupSignals(), которая запускается один раз при первом подключении советника. Она проверяет последние 50 баров на случай, если действительный пробой произошел непосредственно перед загрузкой советника. Это важно, потому что индикатор мог сформировать сигнал пробоя прямо перед подключением советника, и слой исполнения должен уметь отреагировать на этот сетап, если он относится к активной сессии.
Сначала функция ограничивает проверку последними 50 барами или меньшим числом, если на графике доступно меньше истории. Затем она временно переводит массивы буферов в режим временного ряда, чтобы советник мог естественным образом проходить по недавним барам в обратном порядке. После копирования последних данных из буферов индикатора функция восстанавливает обычный режим индексации, используемый в остальных частях советника.
//+------------------------------------------------------------------+ //| Scan the last 50 bars for a signal that appeared before EA start | //+------------------------------------------------------------------+ void ScanStartupSignals() { int bars = Bars(_Symbol, _Period); int lookBack = MathMin(bars - 2, 50); if(lookBack < 1) return; //--- Temporarily reverse series order for easy backward iteration ArraySetAsSeries(bufBuy, true); ArraySetAsSeries(bufSell, true); ArraySetAsSeries(bufPoleHeight, true); //--- Copy enough history if(CopyBuffer(indiHandle, 0, 0, lookBack+1, bufBuy) < lookBack+1 || CopyBuffer(indiHandle, 1, 0, lookBack+1, bufSell) < lookBack+1 || CopyBuffer(indiHandle, 2, 0, lookBack+1, bufPoleHeight) < lookBack+1) { ArraySetAsSeries(bufBuy, false); ArraySetAsSeries(bufSell, false); ArraySetAsSeries(bufPoleHeight, false); return; } //--- Restore standard indexing ArraySetAsSeries(bufBuy, false); ArraySetAsSeries(bufSell, false); ArraySetAsSeries(bufPoleHeight, false); //--- Iterate from older to newer bar (i = lookBack = oldest, down to 1 = last closed) for(int i = lookBack; i >= 1; i--) { datetime barTime = iTime(_Symbol, _Period, i); if(barTime < eaStartTime) continue; bool buySignal = (bufBuy[i] != EMPTY_VALUE && bufBuy[i] != 0); bool sellSignal = (bufSell[i] != EMPTY_VALUE && bufSell[i] != 0); //--- Trade the most recent valid, filtered signal and stop scanning if(buySignal && barTime != lastBuyBarTime && PassesFilters(true, barTime)) { ExecuteTrade(true, bufPoleHeight[i]); lastBuyBarTime = barTime; break; } if(sellSignal && barTime != lastSellBarTime && PassesFilters(false, barTime)) { ExecuteTrade(false, bufPoleHeight[i]); lastSellBarTime = barTime; break; } } }
Затем функция проходит от более ранних баров в этом недавнем диапазоне к последнему закрытому бару и проверяет каждую свечу на наличие действительного сигнала на покупку или продажу. Любой сигнал, время которого предшествует eaStartTime, игнорируется; это не позволяет советнику реагировать на старую историю, существовавшую до подключения. Когда находится новый сигнал, функция пропускает его через PassesFilters() и, если он действителен, вызывает ExecuteTrade(), передавая значение высоты флагштока из буфера.
Такое стартовое сканирование позволяет советнику обрабатывать недавние сигналы без повторной обработки всей истории и без дублирования логики обнаружения индикатора.
4. Проверка сигнала в PassesFilters()
Прежде чем открывать сделку, мы пропускаем сигнал через PassesFilters(), чтобы убедиться, что он соответствует дополнительным условиям. Эта функция нужна, чтобы не торговать свежий сигнал пробоя вслепую, а открывать сделку только тогда, когда он соответствует выбранным рыночным условиям.
Первой проверкой служит фильтр тренда. Когда опция UseTrendFilter включена, советник считывает значение скользящей средней для сигнального бара и сравнивает его с ценой закрытия этого бара. Для сигналов на покупку цена должна находиться выше выбранной скользящей средней. Для сигналов на продажу – ниже нее. Это помогает согласовать входы с более широким направлением рынка и сократить число сделок против тренда.
//+------------------------------------------------------------------+ //| Entry filters (trend + volume) | //| Returns true if signal is valid | //+------------------------------------------------------------------+ bool PassesFilters(bool isBuy, datetime barTime) { //--- Get bar index once (critical optimization) int barIdx = iBarShift(_Symbol, _Period, barTime); if(barIdx < 0) return false; //--- 1. Trend filter (SMA alignment) if(UseTrendFilter) { int maShift = iBarShift(_Symbol, MA_Timeframe, barTime); if(maShift < 0) return false; double maVal[1]; if(CopyBuffer(maHandle, 0, maShift, 1, maVal) != 1) return false; double closePrice = iClose(_Symbol, _Period, barIdx); if(isBuy && closePrice <= maVal[0]) return false; if(!isBuy && closePrice >= maVal[0]) return false; } //--- 2. Volume confirmation (tick volume SMA filter) if(UseVolumeConfirmation) { int needBars = barIdx + VolumeMA_Period + 1; long vol[]; ArraySetAsSeries(vol, true); if(CopyTickVolume(_Symbol, _Period, 0, needBars, vol) < needBars) return false; double sum = 0.0; for(int i = barIdx; i < barIdx + VolumeMA_Period; i++) sum += (double)vol[i]; double avgVol = sum / VolumeMA_Period; double currVol = (double)vol[barIdx]; if(currVol < avgVol * VolumeMultiplier) return false; } //--- All filters passed return true; }
Вторая проверка – подтверждение по объему. Когда UseVolumeConfirmation включен, советник сравнивает тиковый объем пробойного бара с его средним значением. Если активность на пробое слишком слабая, сигнал отклоняется. Это добавляет еще один фильтр – перед исполнением требуется более заметная рыночная активность.
Если обе проверки пройдены, функция возвращает true, и советник может перейти к ExecuteTrade(). Таким образом, PassesFilters() служит четким фильтром между распознаванием сигнала и выставлением ордера.
5. Размещение сделки в ExecuteTrade()
Как только сигнал проходит все проверки, ExecuteTrade() открывает позицию. Перед открытием новой позиции советник сначала закрывает текущую, если hasPosition истинно, а currentTicket все еще указывает на активную сделку. Это ограничивает стратегию одной позицией одновременно и не допускает наложения позиций по одному и тому же символу.
После сброса состояния предыдущей сделки функция рассчитывает цену входа, используя текущую цену Ask для покупок и Bid для продаж. Затем она подготавливает уровни стоп-лосса и тейк-профита. Если включены динамические стоп-лосс и тейк-профит, мы рассчитываем их по фактической высоте флагштока, сохраненной в буфере. Это делает уровни SL/TP пропорциональными размеру обнаруженного паттерна. Для более крупных флагов целевые уровни устанавливаются дальше, а для более мелких остаются ближе.
//+------------------------------------------------------------------+ //| Execute a buy or sell trade with dynamic or fixed SL/TP | //+------------------------------------------------------------------+ void ExecuteTrade(bool isBuy, double poleHeight) { //--- Close any existing position first (only one trade at a time) if(hasPosition && PositionSelectByTicket(currentTicket)) trade.PositionClose(currentTicket, Slippage); hasPosition = false; //--- Determine entry price (ask for buy, bid for sell) double price = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = 0, tp = 0; //--- Calculate dynamic stop and take profit from flagpole height if(UseDynamicSLTP && poleHeight > 0) { sl = isBuy ? price - poleHeight * SL_Multiplier : price + poleHeight * SL_Multiplier; tp = isBuy ? price + poleHeight * TP_Multiplier : price - poleHeight * TP_Multiplier; } else { //--- Fallback to fixed values if dynamic mode is off or pole data missing if(FixedStopLoss > 0) sl = isBuy ? price - FixedStopLoss * _Point : price + FixedStopLoss * _Point; if(FixedTakeProfit > 0) tp = isBuy ? price + FixedTakeProfit * _Point : price - FixedTakeProfit * _Point; } //--- Send the order if(isBuy) trade.Buy(LotSize, _Symbol, price, sl, tp, "Flag Buy"); else trade.Sell(LotSize, _Symbol, price, sl, tp, "Flag Sell"); //--- Store position details for the EMA exit monitor if(PositionSelect(_Symbol)) { currentTicket = PositionGetInteger(POSITION_TICKET); hasPosition = true; } }
Если динамический расчет недоступен или отключен, функция использует фиксированные значения стоп-лосса и тейк-профита. Это дает советнику надежный резервный вариант даже при отсутствии данных о высоте флагштока или если пользователь предпочитает стандартную защиту в пунктах.
Когда уровни определены, ордер отправляется через trade.Buy() или trade.Sell() в зависимости от направления сигнала. После отправки ордера мы сохраняем тикет позиции, чтобы позже управлять ею. Это позволяет советнику и дальше сопровождать сделку, включая логику выхода по EMA.
6. Предотвращение дублирования сделок
Мы также отслеживаем время последнего бара, по которому была открыта сделка, чтобы один и тот же сигнал не использовался повторно. Перед вызовом ExecuteTrade() советник проверяет, относится ли текущий сигнал к бару, который уже был обработан. Если да, сигнал игнорируется.
//--- If a fresh signal is present and passes all filters, execute the trade if((newBuy || newSell) && closedBarTime >= eaStartTime) { if(newBuy && closedBarTime != lastBuyBarTime && PassesFilters(true, closedBarTime)) { ExecuteTrade(true, bufPoleHeight[1]); lastBuyBarTime = closedBarTime; } else if(newSell && closedBarTime != lastSellBarTime && PassesFilters(false, closedBarTime)) { ExecuteTrade(false, bufPoleHeight[1]); lastSellBarTime = closedBarTime; } }
Отслеживание времени бара необходимо, потому что индикатор может оставаться видимым на графике на протяжении многих тиков, а советник должен реагировать на подтвержденный пробой только один раз. Сочетание OnTick(), отслеживания последнего бара и CopyBuffer() гарантирует, что один и тот же сигнал не будет отработан более одного раза.
Сводка по слою исполнения
Индикатор генерирует сигнал через UpdateActiveFlag() и связанные функции обнаружения. Советник получает этот сигнал через буферы и обеспечивает управляемое и воспроизводимое исполнение сделок.
Тестирование и проверка
Главная цель этой части достигнута – теперь у нас есть рабочая связка между индикатором обнаружения флагов и советником. Благодаря этому советник может считывать данные о пробоях, применять подтверждающие фильтры и автоматически исполнять сделки со структурированным управлением рисками.
Я провел тестирование на исторических данных по XAUUSD с использованием высококачественных тиковых данных (качество моделирования 98%) в Тестере стратегий MT5 за период с 11 октября 2022 года по 14 мая 2026 года.

Ключевые результаты
- Начальный депозит: $10 000
- Чистая прибыль: +$22 181,20 (+221,81%)
- Профит-фактор: 1,62
- Фактор восстановления: 3,85
- Коэффициент Шарпа: 2,10
- Всего сделок: 266
- Доля прибыльных сделок: 42,86%
- Максимальная просадка по средствам: $5 765,60 (20,03%)

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

Результаты подтверждают, что интеграция индикатора с советником работает стабильно, а слой сигналов на основе буферов функционирует так, как задумано. Наилучшие результаты система показывает на направленных рынках, тогда как в боковике эффективность сигналов снижается.
По-прежнему есть возможности для улучшения. Имеет смысл изучить следующие направления на будущее:
- фильтрация волатильности на основе ATR для отсечения слабых пробойных условий;
- подтверждение тренда на нескольких таймфреймах для повышения точности определения направления;
- адаптивные пороги силы сигнала для меняющихся рыночных условий;
- расширенное управление сделками с частичными выходами и трейлинг-стопами;
- дополнительные уровни подтверждения с использованием вспомогательных индикаторов;
- расширенная проверка с помощью walk-forward-анализа и форвард-тестирования.
Заключение
В этой статье мы успешно превратили детектор паттерна "флаг" в полноценную автоматизированную торговую систему. Индикатор теперь публикует надежные сигналы через буферы, а советник считывает их, применяет свои фильтры и исполняет сделки с контролируемым управлением рисками.
Тестирование на исторических данных подтвердило, что эта интеграция остается стабильной на длительном историческом промежутке. Сигнальный слой не перерисовывается, сделки срабатывают последовательно, а советник предсказуемо реагирует на подтвержденные пробои. Это дает нам надежную и воспроизводимую основу для автоматической торговли по пробоям флага. Можно добавить больше фильтров и опций управления сделкой, но базовая интеграция уже готова и работает стабильно.
Краткое описание прикрепленных файлов приведено в таблице ниже.
| Файл | Тип | Описание |
|---|---|---|
| Flag_Pattern_Detector.mq5 | Пользовательский индикатор | Обнаруживает бычьи и медвежьи паттерны "флаг", подтверждает пробои, отрисовывает структуры на графике и публикует через буферы индикатора сигналы на покупку и продажу вместе со значениями высоты флагштока. |
| Flag_Pattern_EA.mq5 | Советник | Считывает буферы индикатора с помощью CopyBuffer(), применяет фильтры исполнения, управляет параметрами риска и автоматически открывает сделки по подтвержденным сигналам пробоя паттерна "флаг". |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22607
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
MLP (многослойный перцептрон) внутри советника MQL5: Обучение на истории без Python и без файлов весов
Торговые инструменты на MQL5 (Часть 32): Перекрестие, лупа и режим измерения
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования