English
preview
Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта

Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта

MetaTrader 5 — Трейдинг |
35 1
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

Трейдеры, торгующие на открытии сессий, часто попадают в ловушку первого движения цены после начала килл-зоны. Цена выходит за пределы диапазона открытия, снимает ликвидность за максимумом или минимумом сессии, а затем резко разворачивается. Этот начальный ложный импульс (Judas Swing) призван выбить стопы трейдеров, которые пытаются догнать первое движение цены. Чтобы последовательно торговать такие развороты, нужен четкий порядок действий: определить направление, оценить, является ли текущая цена дорогой или дешевой относительно диапазона сессии, и подтвердить снятие ликвидности до разворота рыночной структуры. Эта статья предназначена для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, которые хотят автоматизировать торговлю на разворотах в рамках торговых сессий на основе концепций Smart Money.

В предыдущей статье (часть 50) мы автоматизировали стратегию Turtle Soup на основе снятия ликвидности. В этой статье мы создадим программу, реализующую модель Bread and Butter Judas Swing. Программа сочетает дневную направленность, определяемую по данным старшего таймфрейма, килл-зоны, заданные по нью-йоркскому времени, и зоны премии и дисконта, рассчитанные по диапазону каждой сессии. Программа активирует сетап только после снятия ликвидности за экстремумом сессии с выходом цены в нужную зону. Затем она ждет смены рыночной структуры и открывает позицию в соответствии с дневной направленностью. Мы рассмотрим следующие темы:

  1. Принцип работы модели Bread and Butter Judas Swing
  2. Реализация на MQL5
  3. Тестирование на исторических данных
  4. Заключение


Принцип работы модели Bread and Butter Judas Swing

Модель Bread and Butter Judas Swing основана на одной идее: первое агрессивное движение цены после открытия сессии часто является ловушкой, а не началом тренда. Стопы, расположенные за недавним максимумом или минимумом сессии, представляют собой ликвидность, необходимую крупным участникам для исполнения ордеров, поэтому цену направляют к ним, после чего она разворачивается. Мы торгуем против этого ложного импульса, а не следуем за ним, но только если такую сделку допускает фильтр направления. Таким фильтром служит дневная направленность, определяемая на старшем таймфрейме: подтвержденное закрытие бара за последним свинговым максимумом или минимумом задает бычью или медвежью направленность и определяет, в какую сторону разрешено открывать позиции при развороте.

Время и положение цены дополняют условия сетапа. Мы торгуем только в заданных килл-зонах (Лондон, Нью-Йорк и Азия), ориентируясь на нью-йоркское время. В течение каждой килл-зоны мы строим диапазон по текущим максимуму и минимуму и делим его пополам: выше середины находится зона премии, ниже — зона дисконта. В зоне премии предпочтительны продажи, в зоне дисконта — покупки. При медвежьей направленности требуется снятие ликвидности над максимумом сессии с выходом цены в зону премии, а при бычьей — под минимумом сессии с выходом цены в зону дисконта. Снятие ликвидности — это подготовка, а не вход в рынок. Мы ждем смены рыночной структуры: цена должна вернуться за уровень последнего малого свинга в сторону дневной направленности, а бар — закрыться за этим уровнем. Затем мы открываем позицию, размещаем стоп за экстремумом, за которым была снята ликвидность, и задаем цель с фиксированным соотношением прибыли и риска. Ниже кратко представлены наши цели.

STRATEGY ROADMAP


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

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

//+------------------------------------------------------------------+
//|                                   Bread and Butter Engine EA.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

//--- Define the EA version string shown in the startup log
#define EA_VERSION "1.00"

//--- Include the standard library for order execution
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum TradeDirection
  {
   TRADE_BOTH,        // Both directions
   TRADE_BUYS_ONLY,   // Bullish setups only (buys)
   TRADE_SELLS_ONLY   // Bearish setups only (sells)
  };

enum LotSizingMode
  {
   LOTS_FIXED,        // Fixed lot size
   LOTS_RISK_PERCENT  // Risk percent of balance (auto lot)
  };

enum StopLossMode
  {
   SL_AUTO,           // Structural: beyond the swept extreme + buffer
   SL_MANUAL          // Fixed distance from entry (points)
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "GENERAL"
input long           InpMagicNumber        = 1107;              // Magic number
input LotSizingMode  InpLotSizingMode      = LOTS_RISK_PERCENT; // Lot sizing mode
input double         InpFixedLots          = 0.01;              // Fixed lot (Fixed mode)
input double         InpRiskPercent        = 0.5;               // Risk per trade, percent of balance (Risk mode)
input TradeDirection InpTradeDirection     = TRADE_BOTH;        // Allowed trade direction
input bool           InpAllowMultiple      = false;             // Allow several open trades at once
input string         InpOrderComment       = "BB";              // Order comment

input group "STOP LOSS"
input StopLossMode   InpStopLossMode       = SL_AUTO;           // Stop-loss mode
input int            InpStopBufferPoints   = 50;                // Auto SL: buffer beyond the swept extreme (points)
input int            InpManualStopPoints   = 11000;             // Manual SL: fixed distance from entry (points; symbol-specific)

input group "TAKE PROFIT"
input double         InpRewardRiskRatio    = 1.0;               // Reward-to-risk ratio (TP = R:R x stop distance)

input group "TRAILING STOP"
input bool           InpUseTrailingStop    = false;             // Use trailing stop
input int            InpMinProfitPoints    = 1000;              // Minimum profit points to activate trailing
input int            InpTrailPoints        = 300;               // Trailing stop points

input group "PARTIAL CLOSE (OPTIONAL)"
input bool           InpUsePartialClose    = false;             // Bank a partial at the first target
input double         InpPartialAtRR        = 1.0;               // Partial target in R
input double         InpPartialPercent     = 50.0;              // Percent of the position to close at the partial

input group "DAILY BIAS (HIGHER-TIMEFRAME STRUCTURE)"
input ENUM_TIMEFRAMES InpBiasTimeframe     = PERIOD_H4;         // Higher-timeframe bias chart
input int            InpBiasSwingLookback  = 5;                 // Bias swing pivot lookback (bars each side)

input group "KILL ZONES (NEW YORK TIME)"
input int            InpBrokerGmtOffset    = 0;                 // Broker/server GMT offset (hours). New York EST/EDT handled automatically.
input bool           InpUseLondon          = true;              // Trade the London kill zone
input string         InpLondonStart        = "02:00";           // London start (NY time HH:MM)
input string         InpLondonEnd          = "05:00";           // London end (NY time HH:MM)
input bool           InpUseNewYork         = true;              // Trade the New York kill zone
input string         InpNewYorkStart       = "08:20";           // New York start (NY time HH:MM)
input string         InpNewYorkEnd         = "11:00";           // New York end (NY time HH:MM)
input bool           InpUseAsia            = false;             // Trade the Asia kill zone
input string         InpAsiaStart          = "19:00";           // Asia start (NY time HH:MM)
input string         InpAsiaEnd            = "22:00";           // Asia end (NY time HH:MM)

input group "SETUP (JUDAS SWEEP INTO PREMIUM / DISCOUNT)"
input bool           InpRequireSweep       = true;              // Require a liquidity sweep of the recent extreme
input int            InpSweepLookback      = 20;                // Bars (entry TF) for the swept high/low
input int            InpMinSessionBars     = 3;                 // Entry-TF bars into the session before a range is valid

input group "ENTRY (MARKET STRUCTURE SHIFT)"
input ENUM_TIMEFRAMES InpEntryTimeframe    = PERIOD_CURRENT;    // Entry / trigger timeframe (current chart TF)
input int            InpMssSwingLookback   = 3;                 // Entry swing pivot lookback (bars each side)
input int            InpMaxWaitBars        = 24;                // Max entry-TF bars to wait for the MSS, then disarm

input group "LOGGING"
input bool           InpShowLogs           = true;              // Print log messages to the Journal
input string         InpLogPrefix          = "BB> ";            // Log prefix

input group "VISUALS (CHART ONLY)"
input bool           InpDrawVisuals        = true;              // Draw setup structure on the chart
input bool           InpShowSwingMarkers   = true;              // Draw entry-TF swing markers
input int            InpMarkerSize         = 10;                // Sweep / BOS / entry marker size (Wingdings 3)
input color          InpBullColor          = clrDodgerBlue;     // Bullish setup color
input color          InpBearColor          = clrRed;            // Bearish setup color
input color          InpSessionColor       = clrSlateGray;      // Kill-zone box color
input color          InpEqColor            = clrGoldenrod;      // Equilibrium line color
input color          InpSweepColor         = clrMagenta;        // Liquidity-sweep marker color
input color          InpMssColor           = clrDarkViolet;     // MSS line color
input color          InpSwingHighColor     = clrDarkOrange;     // Swing-high marker color
input color          InpSwingLowColor      = clrDodgerBlue;     // Swing-low marker color
input color          InpBiasHighColor      = clrForestGreen;    // Bias swing-high level (break = bullish)
input color          InpBiasLowColor       = clrOrangeRed;      // Bias swing-low level (break = bearish)
input color          InpPremiumColor       = C'255,232,232';    // Premium zone tint (very light)
input color          InpDiscountColor      = C'230,240,255';    // Discount zone tint (very light)

//+------------------------------------------------------------------+
//| Per-ticket trade record for partial and trailing management      |
//+------------------------------------------------------------------+
struct TradeRecord
  {
   ulong  ticket;       // Store the position ticket
   bool   isBull;       // Mark true for a buy position
   double entryPrice;   // Store the fill price
   double initialStop;  // Store the initial stop price
   double riskDistance; // Store the entry-to-initial-stop distance
   bool   partialTaken; // Mark true once the partial is banked
  };

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade   Trade;                        // Trade execution object
int      SymDigits;                    // Cached symbol digits
double   SymPoint;                     // Cached symbol point size

datetime g_lastEntryBar = 0;           // Last processed entry-TF bar time
datetime g_lastBiasBar  = 0;           // Last processed bias-TF bar time
int      g_bias         = 0;           // Current daily bias: +1 bull, -1 bear, 0 flat

//--- Bias-TF structure tracking
double   g_biasSwingHigh     = 0.0;    // Last confirmed bias-TF swing high
datetime g_biasSwingHighTime = 0;      // Time of the last bias swing high
double   g_biasSwingLow      = 0.0;    // Last confirmed bias-TF swing low
datetime g_biasSwingLowTime  = 0;      // Time of the last bias swing low

//--- Session (kill-zone) state
string   g_activeSession   = "";       // Active kill-zone name ("" when none)
datetime g_sessionStart    = 0;        // Server time the session became active
double   g_sessionHigh     = 0.0;      // Running session high
double   g_sessionLow      = 0.0;      // Running session low
int      g_sessionBarCount = 0;        // Entry-TF bars counted this session
bool     g_sessionTraded   = false;    // Flag a setup already fired this session

//--- Cached kill-zone windows in minutes since NY midnight (parsed once)
int      g_londonStart = 0, g_londonEnd = 0;   // London window bounds
int      g_nyStart     = 0, g_nyEnd     = 0;   // New York window bounds
int      g_asiaStart   = 0, g_asiaEnd   = 0;   // Asia window bounds

//--- Cached New York DST offset (recomputed once per day)
int      g_nyOffset    = -5;           // Current NY GMT offset in hours
int      g_nyOffsetDay = -1;           // Day-of-year the offset was resolved

//--- Entry-TF swing tracking for the MSS trigger
double   g_lastSwingHigh     = 0.0;    // Last confirmed entry-TF swing high
datetime g_lastSwingHighTime = 0;      // Time of the last entry swing high
double   g_lastSwingLow      = 0.0;    // Last confirmed entry-TF swing low
datetime g_lastSwingLowTime  = 0;      // Time of the last entry swing low

//--- Armed setup state
bool     g_armed        = false;       // True while waiting for the MSS trigger
int      g_setupDir     = 0;           // Armed direction: +1 buy, -1 sell
double   g_setupExtreme = 0.0;         // Swept extreme used as the stop anchor
double   g_mssLevel     = 0.0;         // Structure level whose break confirms entry
datetime g_armBarTime   = 0;           // Entry-TF bar time the setup was armed

TradeRecord g_trades[];                // Per-ticket trade records


Здесь мы формируем основу программы. Мы определяем макрос EA_VERSION с информацией о версии и подключаем Trade.mqh для исполнения ордеров. Затем объявляем три перечисления — TradeDirection, LotSizingMode и StopLossMode, которые представляют варианты настройки в виде понятных выпадающих списков Мы представляем входные параметры по группам: общие настройки, стоп-лосс, соотношение прибыли и риска, трейлинг, опциональное частичное закрытие позиции, дневная направленность, килл-зоны по нью-йоркскому времени, сетап со снятием ликвидности, триггер входа, ведение лога и визуальное оформление графика. Мы объявляем структуру TradeRecord для хранения тикета, направления, цены входа, начального стопа и исходного расстояния от цены входа до стопа по каждой позиции. Благодаря этому расчеты для трейлинга и частичного закрытия всегда опираются на риск, заданный при входе, а не на положение стопа, который уже мог сдвинуться. Наконец, мы сохраняем между барами глобальное состояние: направленность, активную сессию и ее диапазон, подтвержденные свинги и активированный сетап. Массив g_trades содержит по одной записи на каждую открытую позицию.

Работа со временем и торговыми сессиями

Прежде чем выполнять логику сетапа, программа должна определить, открылся ли новый бар и какая килл-зона активна. Мы создаем небольшой набор вспомогательных функций для работы со временем: они обнаруживают новый бар, разбирают заданные интервалы сессий, учитывают летнее время в Нью-Йорке (DST) и определяют название активной сессии.

//+------------------------------------------------------------------+
//| Detect the open of a new entry-TF bar                            |
//+------------------------------------------------------------------+
bool IsNewEntryBar()
  {
//--- Read the current entry-TF bar time
   datetime t = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Report a new bar and store its time when it changes
   if(t != g_lastEntryBar) { g_lastEntryBar = t; return true; }
//--- Report no new bar
   return false;
  }

//+------------------------------------------------------------------+
//| Convert an HH:MM string to minutes since midnight                |
//+------------------------------------------------------------------+
int ParseHHMM(string hhmm)
  {
//--- Split the text on the colon separator
   string parts[];
   int n = StringSplit(hhmm, (ushort)':', parts);
//--- Fall back to zero on a malformed value
   if(n < 2) return 0;
//--- Parse the hour and minute components
   int hh = (int)StringToInteger(parts[0]);
   int mm = (int)StringToInteger(parts[1]);
//--- Combine into total minutes since midnight
   return (hh * 60 + mm);
  }

//+------------------------------------------------------------------+
//| Compute UTC time of the Nth Sunday of a month at a given hour    |
//+------------------------------------------------------------------+
datetime NthSundayUtc(int year, int month, int nth, int atHourUtc)
  {
//--- Build the first day of the month
   MqlDateTime t;
   t.year = year; t.mon = month; t.day = 1; t.hour = 0; t.min = 0; t.sec = 0;
   datetime first = StructToTime(t);
//--- Resolve the weekday of that first day
   TimeToStruct(first, t);
//--- Find the day-of-month of the first Sunday (0 = Sunday)
   int firstSunday = 1 + ((7 - t.day_of_week) % 7);
//--- Step forward to the requested Nth Sunday
   int day = firstSunday + (nth - 1) * 7;
//--- Build the final timestamp at the requested UTC hour
   MqlDateTime r;
   r.year = year; r.mon = month; r.day = day; r.hour = atHourUtc; r.min = 0; r.sec = 0;
   return StructToTime(r);
  }

//+------------------------------------------------------------------+
//| Resolve the New York GMT offset for a given UTC moment           |
//+------------------------------------------------------------------+
int NyGmtOffset(datetime utc)
  {
//--- Break the UTC moment into calendar fields
   MqlDateTime t; TimeToStruct(utc, t);
//--- Bound US DST: 2nd Sunday of March to 1st Sunday of November
   datetime dstStart = NthSundayUtc(t.year, 3, 2, 7);
   datetime dstEnd   = NthSundayUtc(t.year, 11, 1, 6);
//--- Return EDT inside the DST window
   if(utc >= dstStart && utc < dstEnd) return -4;
//--- Return EST outside the DST window
   return -5;
  }

//+------------------------------------------------------------------+
//| Get the current New York time as minutes since midnight          |
//+------------------------------------------------------------------+
int NyMinutesNow()
  {
//--- Convert server time to UTC using the broker offset
   datetime utc = TimeCurrent() - InpBrokerGmtOffset * 3600;
//--- Break UTC into fields to detect a day change
   MqlDateTime u; TimeToStruct(utc, u);
//--- Recompute the NY DST offset only once per day
   if(u.day_of_year != g_nyOffsetDay)
     {
      //--- Cache the day and its resolved offset
      g_nyOffsetDay = u.day_of_year;
      g_nyOffset    = NyGmtOffset(utc);
     }
//--- Shift UTC into New York time and return minutes of day
   MqlDateTime t; TimeToStruct(utc + g_nyOffset * 3600, t);
   return t.hour * 60 + t.min;
  }

//+------------------------------------------------------------------+
//| Test whether a minute-of-day sits inside a window                |
//+------------------------------------------------------------------+
bool InWindow(int now, int start, int end)
  {
//--- Handle a normal same-day window
   if(start <= end) return (now >= start && now < end);
//--- Handle a window that wraps past midnight
   return (now >= start || now < end);
  }

//+------------------------------------------------------------------+
//| Resolve the active kill zone for the current New York time       |
//+------------------------------------------------------------------+
string GetActiveSession()
  {
//--- Read the current New York minute of day
   int now = NyMinutesNow();
//--- Return London when enabled and inside its window
   if(InpUseLondon  && InWindow(now, g_londonStart, g_londonEnd)) return "LONDON";
//--- Return New York when enabled and inside its window
   if(InpUseNewYork && InWindow(now, g_nyStart,     g_nyEnd))     return "NEWYORK";
//--- Return Asia when enabled and inside its window
   if(InpUseAsia    && InWindow(now, g_asiaStart,   g_asiaEnd))   return "ASIA";
//--- Report no active kill zone
   return "";
  }

Мы создаем вспомогательные функции для работы со временем, которые обеспечивают выполнение стратегии один раз на бар и определяют активную килл-зону. С помощью IsNewEntryBar мы определяем первый тик каждого бара на таймфрейме входа, чтобы ресурсоемкая логика выполнялась только один раз на бар. С помощью ParseHHMM мы преобразуем каждое заданное время сессии в количество минут с полуночи. Преобразование выполняется один раз при запуске, поэтому во время работы стратегии повторно разбирать строки не требуется. Мы учитываем летнее время в Нью-Йорке в два этапа. С помощью NthSundayUtc и NyGmtOffset определяем границы периода DST (со второго воскресенья марта по первое воскресенье ноября): в этот период возвращаем смещение относительно UTC минус четыре часа, вне его — минус пять часов. Затем с помощью NyMinutesNow получаем количество минут с начала текущих суток по нью-йоркскому времени, пересчитывая смещение только при смене дня. Наконец, с помощью InWindow мы проверяем, попадает ли текущее время в заданный интервал, в том числе переходящий через полночь. С помощью GetActiveSession возвращаем первую включенную килл-зону, в интервал которой попадает текущее время, или пустую строку, если ни одна килл-зона не активна.

Опорные точки, расчет размера позиции и учет сделок

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

//+------------------------------------------------------------------+
//| Scan for the latest confirmed swing pivot on a timeframe         |
//+------------------------------------------------------------------+
bool ScanPivot(ENUM_TIMEFRAMES tf, int lookback, bool &isHigh, bool &isLow,
               double &hiPrice, double &loPrice, datetime &pivotTime)
  {
//--- Assume no pivot until proven
   isHigh = false; isLow = false;
//--- Clamp the lookback to at least one bar each side
   int lb = MathMax(1, lookback);
//--- Require enough history to test both sides
   if(iBars(_Symbol, tf) < lb * 2 + 2) return false;
//--- Center on the candidate bar with lb closed bars to its right
   int shift = lb + 1;
   pivotTime = iTime(_Symbol, tf, shift);
   hiPrice   = iHigh(_Symbol, tf, shift);
   loPrice   = iLow(_Symbol, tf, shift);
//--- Assume both a high and a low pivot until a neighbor breaks it
   bool hh = true, ll = true;
//--- Compare the candidate against lb bars on each side
   for(int j = 1; j <= lb; j++)
     {
      //--- Reject the high if any neighbor is at least as high
      if(iHigh(_Symbol, tf, shift - j) >= hiPrice || iHigh(_Symbol, tf, shift + j) >= hiPrice) hh = false;
      //--- Reject the low if any neighbor is at least as low
      if(iLow(_Symbol, tf, shift - j)  <= loPrice || iLow(_Symbol, tf, shift + j)  <= loPrice) ll = false;
     }
//--- Publish the pivot classification
   isHigh = hh; isLow = ll;
//--- Report whether either a high or low pivot formed
   return (hh || ll);
  }

//+------------------------------------------------------------------+
//| Check whether a trade direction is permitted                     |
//+------------------------------------------------------------------+
bool IsDirectionAllowed(bool isBull)
  {
//--- Allow everything when both directions are enabled
   if(InpTradeDirection == TRADE_BOTH) return true;
//--- Allow only buys in buys-only mode
   if(InpTradeDirection == TRADE_BUYS_ONLY) return isBull;
//--- Otherwise allow only sells
   return !isBull;
  }

//+------------------------------------------------------------------+
//| Convert risk percent and stop distance into a lot size           |
//+------------------------------------------------------------------+
double CalcLotsByRisk(double entry, double stop)
  {
//--- Derive the money to risk from the account balance
   double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0;
//--- Measure the stop distance in points
   double stopPoints = MathAbs(entry - stop) / SymPoint;
//--- Abort on a zero stop distance
   if(stopPoints <= 0) return 0;
//--- Read the tick value and tick size for the symbol
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
//--- Abort on invalid tick metrics
   if(tickValue <= 0 || tickSize <= 0) return 0;
//--- Convert tick value into money per point
   double valuePerPoint = tickValue / tickSize * SymPoint;
//--- Abort on an invalid per-point value
   if(valuePerPoint <= 0) return 0;
//--- Size the position so the stop loss equals the risk money
   double lots = riskMoney / (stopPoints * valuePerPoint);
//--- Read the broker volume constraints
   double volMin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volMax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
//--- Snap the lot size down to the volume step
   if(volStep > 0) lots = MathFloor(lots / volStep) * volStep;
//--- Clamp within limits and normalize to two decimals
   return NormalizeDouble(MathMax(volMin, MathMin(volMax, lots)), 2);
  }

//+------------------------------------------------------------------+
//| Resolve the lot size for a trade by the selected mode            |
//+------------------------------------------------------------------+
double ResolveLots(double entry, double stop)
  {
//--- Pick fixed lots or risk-based lots by the sizing mode
   double lots = (InpLotSizingMode == LOTS_FIXED) ? InpFixedLots : CalcLotsByRisk(entry, stop);
//--- Read the broker volume constraints
   double volMin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volMax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
//--- Snap the lot size down to the volume step
   if(volStep > 0) lots = MathFloor(lots / volStep) * volStep;
//--- Clamp within the allowed range
   lots = MathMax(volMin, MathMin(volMax, lots));
//--- Normalize to two decimals
   return NormalizeDouble(lots, 2);
  }

//+------------------------------------------------------------------+
//| Find a trade record index by ticket                              |
//+------------------------------------------------------------------+
int FindTradeRecord(ulong ticket)
  {
//--- Scan the records for a matching ticket
   for(int i = 0; i < ArraySize(g_trades); i++)
      if(g_trades[i].ticket == ticket) return i;
//--- Report not found
   return -1;
  }

//+------------------------------------------------------------------+
//| Count this EA's open positions on the current symbol             |
//+------------------------------------------------------------------+
int CountOurPositions()
  {
//--- Start the running count at zero
   int count = 0;
//--- Walk every open position from last to first
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      //--- Select the position by its ticket
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      //--- Skip positions from another EA
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
      //--- Skip positions on another symbol
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      //--- Count this position as ours
      count++;
     }
//--- Return the total owned positions
   return count;
  }

//+------------------------------------------------------------------+
//| Append a new trade record for a freshly opened position          |
//+------------------------------------------------------------------+
void AddTradeRecord(ulong ticket, bool isBull, double entry, double stop)
  {
//--- Ignore an invalid or already-tracked ticket
   if(ticket == 0 || FindTradeRecord(ticket) >= 0) return;
//--- Grow the record array by one slot
   int n = ArraySize(g_trades);
   ArrayResize(g_trades, n + 1);
//--- Populate the new record from the fill details
   g_trades[n].ticket       = ticket;
   g_trades[n].isBull       = isBull;
   g_trades[n].entryPrice   = entry;
   g_trades[n].initialStop  = stop;
   g_trades[n].riskDistance = MathAbs(entry - stop);
   g_trades[n].partialTaken = false;
  }

//+------------------------------------------------------------------+
//| Drop records whose positions have closed                         |
//+------------------------------------------------------------------+
void PruneTradeRecords()
  {
//--- Walk records backward so removals stay safe
   for(int i = ArraySize(g_trades) - 1; i >= 0; i--)
      //--- Remove the record once its position no longer exists
      if(!PositionSelectByTicket(g_trades[i].ticket))
        {
         //--- Shift later records down over the gap
         for(int j = i; j < ArraySize(g_trades) - 1; j++) g_trades[j] = g_trades[j + 1];
         //--- Shrink the array by one slot
         ArrayResize(g_trades, ArraySize(g_trades) - 1);
        }
  }

//+------------------------------------------------------------------+
//| Adopt any of our open positions missing a record                 |
//+------------------------------------------------------------------+
void SyncTradeRecords()
  {
//--- Walk every open position from last to first
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      //--- Select the position by its ticket
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      //--- Skip positions from another EA
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
      //--- Skip positions on another symbol
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      //--- Skip positions already tracked
      if(FindTradeRecord(ticket) >= 0) continue;
      //--- Grow the record array by one slot
      int n = ArraySize(g_trades);
      ArrayResize(g_trades, n + 1);
      //--- Rebuild the record from live position data
      g_trades[n].ticket       = ticket;
      g_trades[n].isBull       = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      g_trades[n].entryPrice   = PositionGetDouble(POSITION_PRICE_OPEN);
      g_trades[n].initialStop  = PositionGetDouble(POSITION_SL);
      g_trades[n].riskDistance = MathAbs(g_trades[n].entryPrice - g_trades[n].initialStop);
      //--- Mark adopted positions as already partialed to avoid a surprise scale-out
      g_trades[n].partialTaken = true;
     }
  }

//+------------------------------------------------------------------+
//| Test whether a fresh liquidity sweep of the extreme occurred     |
//+------------------------------------------------------------------+
bool SweepDone(bool forSell)
  {
//--- Treat the sweep as satisfied when the filter is off
   if(!InpRequireSweep) return true;
//--- Clamp the lookback to at least two bars
   int n = MathMax(2, InpSweepLookback);
//--- Require enough history for the lookback plus buffer
   if(iBars(_Symbol, InpEntryTimeframe) < n + 3) return false;
//--- For a sell, test a take-out of the prior N-bar high
   if(forSell)
     {
      //--- Locate the highest high across the prior window
      int idx = iHighest(_Symbol, InpEntryTimeframe, MODE_HIGH, n, 2);
      if(idx < 0) return false;
      //--- Confirm the last closed bar pushed above it
      return (iHigh(_Symbol, InpEntryTimeframe, 1) > iHigh(_Symbol, InpEntryTimeframe, idx));
     }
//--- For a buy, locate the lowest low across the prior window
   int idx2 = iLowest(_Symbol, InpEntryTimeframe, MODE_LOW, n, 2);
   if(idx2 < 0) return false;
//--- Confirm the last closed bar pushed below it
   return (iLow(_Symbol, InpEntryTimeframe, 1) < iLow(_Symbol, InpEntryTimeframe, idx2));
  }

//+------------------------------------------------------------------+
//| Reset the armed setup state                                      |
//+------------------------------------------------------------------+
void Disarm(string reason)
  {
//--- Do nothing when no setup is armed
   if(!g_armed) return;
//--- Clear the armed flag
   g_armed = false;
//--- Log the disarm with its direction and reason
   Log((g_setupDir > 0 ? "Bullish" : "Bearish") + " setup disarmed: " + reason + ".");
  }

Мы создаем вспомогательные функции, обеспечивающие логику сетапа и входа. С помощью ScanPivot мы находим последний подтвержденный свинг на заданном таймфрейме. Для этого проверяем бар-кандидат и заданное число закрытых баров с каждой стороны. Кандидат отклоняется, если хотя бы один из этих баров имеет такой же или более высокий максимум либо такой же или более низкий минимум — в зависимости от типа искомого свинга. Так определяется фрактальная опорная точка, от которой зависят и направленность, и структура для входа. С помощью IsDirectionAllowed мы проверяем допустимость направления согласно настройке, а с помощью CalcLotsByRisk и ResolveLots рассчитываем размер позиции так, чтобы убыток при срабатывании стопа соответствовал заданной сумме риска. Полученный объем приводим к установленному брокером шагу и ограничиваем допустимыми пределами. Мы синхронизируем записи программы с данными терминала с помощью нескольких вспомогательных функций. CountOurPositions обеспечивает соблюдение ограничения на одну открытую позицию, AddTradeRecord сохраняет исходный риск вновь открытой позиции, PruneTradeRecords удаляет записи о закрытых позициях, а SyncTradeRecords добавляет записи о позициях, для которых их еще нет. Такие позиции помечаются как уже частично закрытые, чтобы перезапуск не привел к неожиданному частичному закрытию. Наконец, с помощью SweepDone мы подтверждаем недавнее снятие ликвидности за экстремумом предыдущих N баров, а с помощью Disarm деактивируем сетап и записываем причину в лог.

Отрисовка горизонтального уровня

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

//+------------------------------------------------------------------+
//| Draw or update a horizontal trend-line level                     |
//+------------------------------------------------------------------+
void DrawHLevel(string name, datetime t1, datetime t2, double price, color clr, ENUM_LINE_STYLE style, int width)
  {
//--- Create the object on first use, otherwise move both anchors
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_TREND, 0, t1, price, t2, price);
   else
     {
      //--- Move the left anchor
      ObjectMove(0, name, 0, t1, price);
      //--- Move the right anchor
      ObjectMove(0, name, 1, t2, price);
     }
//--- Apply the line color, style and width
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE, style);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
//--- Keep the line as a segment, not a ray
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
//--- Draw the line in the foreground
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
  }

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

Отображение сессии, направленности и сетапа

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

//+------------------------------------------------------------------+
//| Draw or refresh the active kill-zone box and equilibrium         |
//+------------------------------------------------------------------+
void DrawSessionBox()
  {
//--- Skip when visuals are off or no session is active
   if(!VisualsAllowed() || g_activeSession == "" || g_sessionStart == 0) return;
//--- Build a per-session id so the box persists across bars
   string id  = IntegerToString((int)g_sessionStart);
//--- Extend the right edge to the current time
   datetime t2 = TimeCurrent();
//--- Compute the equilibrium at the range midpoint
   double eq = (g_sessionHigh + g_sessionLow) / 2.0;
//--- Fill the premium (sell) zone above equilibrium
   DrawRectFill("BB_Prem_" + id, g_sessionStart, g_sessionHigh, t2, eq, InpPremiumColor);
//--- Fill the discount (buy) zone below equilibrium
   DrawRectFill("BB_Disc_" + id, g_sessionStart, eq, t2, g_sessionLow, InpDiscountColor);
//--- Outline the full session range
   DrawRect("BB_Sess_" + id, g_sessionStart, g_sessionHigh, t2, g_sessionLow, InpSessionColor);
//--- Label the session by name
   DrawText("BB_SessTxt_" + id, g_sessionStart, g_sessionHigh, " " + g_activeSession, InpSessionColor, ANCHOR_LOWER);
//--- Draw the dotted equilibrium line
   DrawHLevel("BB_EQ_" + id, g_sessionStart, t2, eq, InpEqColor, STYLE_DOT, 1);
//--- Label the equilibrium line
   DrawText("BB_EQTxt_" + id, t2, eq, " EQ", InpEqColor, ANCHOR_LEFT);
//--- Label the premium half
   DrawText("BB_PremTxt_" + id, t2, (g_sessionHigh + eq) / 2.0, "Premium ", InpBearColor, ANCHOR_RIGHT);
//--- Label the discount half
   DrawText("BB_DiscTxt_" + id, t2, (g_sessionLow + eq) / 2.0, "Discount ", InpBullColor, ANCHOR_RIGHT);
  }

//+------------------------------------------------------------------+
//| Draw the active higher-timeframe bias structure levels           |
//+------------------------------------------------------------------+
void DrawBiasStructure()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Anchor the level lines to the current entry-TF bar
   datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Draw the bias high whose break turns order flow bullish
   if(g_biasSwingHigh > 0 && g_biasSwingHighTime > 0)
     {
      //--- Draw the bias-high line
      DrawHLevel("BB_BiasHigh", g_biasSwingHighTime, now, g_biasSwingHigh, InpBiasHighColor, STYLE_SOLID, 1);
      //--- Label the bias-high line
      DrawText("BB_BiasHighTxt", now, g_biasSwingHigh, " Bias High", InpBiasHighColor, ANCHOR_LEFT);
     }
//--- Draw the bias low whose break turns order flow bearish
   if(g_biasSwingLow > 0 && g_biasSwingLowTime > 0)
     {
      //--- Draw the bias-low line
      DrawHLevel("BB_BiasLow", g_biasSwingLowTime, now, g_biasSwingLow, InpBiasLowColor, STYLE_SOLID, 1);
      //--- Label the bias-low line
      DrawText("BB_BiasLowTxt", now, g_biasSwingLow, " Bias Low", InpBiasLowColor, ANCHOR_LEFT);
     }
  }

//+------------------------------------------------------------------+
//| Draw or extend the MSS trigger line for the armed setup          |
//+------------------------------------------------------------------+
void DrawMssLine()
  {
//--- Skip when visuals are off or no setup is armed
   if(!VisualsAllowed() || !g_armed || g_mssLevel <= 0) return;
//--- Build a per-setup id from the arm bar time
   string id = IntegerToString((int)g_armBarTime);
//--- Extend the line to the current entry-TF bar
   datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Draw the dash-dot MSS trigger line
   DrawHLevel("BB_MSS_" + id, g_armBarTime, now, g_mssLevel, InpMssColor, STYLE_DASHDOT, 1);
//--- Label the MSS line
   DrawText("BB_MSSTxt_" + id, now, g_mssLevel, " MSS", InpMssColor, ANCHOR_LEFT);
  }

//+------------------------------------------------------------------+
//| Draw the entry, stop and target levels with an entry arrow       |
//+------------------------------------------------------------------+
void DrawEntryLevels(bool isBull, datetime t, double entry, double stop, double takeProfit)
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build a per-entry id from the entry time
   string id = "BB_Ent_" + IntegerToString((int)t);
//--- Span the level lines a fixed number of bars to the right
   datetime t2 = t + (datetime)(PeriodSeconds(InpEntryTimeframe) * 30);
//--- Draw the entry line
   DrawHLevel(id + "_e",  t, t2, entry,      clrDodgerBlue, STYLE_SOLID, 2);
//--- Draw the stop-loss line
   DrawHLevel(id + "_sl", t, t2, stop,       C'220,60,60',  STYLE_DASH,  1);
//--- Draw the take-profit line
   DrawHLevel(id + "_tp", t, t2, takeProfit, C'0,200,80',   STYLE_DASH,  1);
//--- Read the trigger bar high and low for arrow placement
   double barHigh = iHigh(_Symbol, InpEntryTimeframe, 1);
   double barLow  = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Draw the direction arrow at the trigger bar extreme
   DrawMarker(id + "_a", t, isBull ? barLow : barHigh, isBull,
              isBull ? InpBullColor : InpBearColor, isBull ? ANCHOR_UPPER : ANCHOR_LOWER);
  }

//+------------------------------------------------------------------+
//| Draw an entry-TF swing marker with its label                     |
//+------------------------------------------------------------------+
void DrawSwingMarker(bool isHigh, datetime t, double price, string label, color clr)
  {
//--- Skip when visuals or swing markers are disabled
   if(!VisualsAllowed() || !InpShowSwingMarkers) return;
//--- Build a unique tag for a high or low marker
   string tag = (isHigh ? "BB_SWH_" : "BB_SWL_") + IntegerToString((int)t);
//--- Place a dot on the pivot, above a high or below a low
   int dotAnchor = isHigh ? ANCHOR_BOTTOM : ANCHOR_TOP;
   DrawArrow(tag, t, price, 159, clr, dotAnchor);
//--- Anchor the label text opposite the dot side
   ENUM_ANCHOR_POINT txtAnchor = ANCHOR_LEFT_UPPER;
   if(isHigh) txtAnchor = ANCHOR_LEFT_LOWER;
//--- Draw the H/HH/LH or L/LL/HL label beside the dot
   DrawText(tag + "_t", t, price, label, clr, txtAnchor);
  }

Здесь мы отображаем весь сетап на графике с помощью DrawHLevel и аналогичных вспомогательных функций для прямоугольников, текста и маркеров. С помощью DrawSessionBox мы закрашиваем верхнюю половину диапазона над уровнем равновесия — зону премии — и нижнюю половину под ним — зону дисконта, обводим границы диапазона и подписываем зоны. Так понятие дорогой и дешевой цены получает наглядное представление. С помощью DrawBiasStructure мы отображаем максимум и минимум, пробой которых меняет направленность потока ордеров; с помощью DrawMssLine продлеваем штрихпунктирную линию триггера, пока сетап активирован; с помощью DrawEntryLevels наносим линии входа, стопа и цели со стрелкой направления; с помощью DrawSwingMarker помечаем каждую подтвержденную опорную точку как H/HH/LH или L/LL/HL. В результате на графике с первого взгляда видны направленность, зоны сессии, еще не сработавший триггер и сделка, как показано ниже.

ARMED SETUP SAMPLE

Определение направленности и отслеживание структуры

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

//+------------------------------------------------------------------+
//| Update the higher-timeframe bias from market structure           |
//+------------------------------------------------------------------+
void UpdateBias()
  {
//--- Only recompute on a fresh bias-TF bar
   datetime bt = iTime(_Symbol, InpBiasTimeframe, 0);
   if(bt == g_lastBiasBar) return;
   g_lastBiasBar = bt;
//--- Refresh the latest bias-TF swing points
   bool isHigh, isLow; double hi, lo; datetime t;
   if(ScanPivot(InpBiasTimeframe, InpBiasSwingLookback, isHigh, isLow, hi, lo, t))
     {
      //--- Store a new swing high when found
      if(isHigh && t != g_biasSwingHighTime) { g_biasSwingHigh = hi; g_biasSwingHighTime = t; }
      //--- Store a new swing low when found
      if(isLow  && t != g_biasSwingLowTime)  { g_biasSwingLow  = lo; g_biasSwingLowTime  = t; }
     }
//--- Remember the prior bias to detect a change
   int prevBias = g_bias;
//--- Read the last closed bias-TF close
   double c1 = iClose(_Symbol, InpBiasTimeframe, 1);
//--- Turn bullish on a close above the last swing high
   if(g_biasSwingHigh > 0 && c1 > g_biasSwingHigh)      g_bias = 1;
//--- Turn bearish on a close below the last swing low
   else if(g_biasSwingLow > 0 && c1 < g_biasSwingLow)   g_bias = -1;
//--- Act only when the bias actually flips
   if(g_bias != prevBias)
     {
      //--- Log the new directional bias
      Log("Bias -> " + (g_bias > 0 ? "BULLISH" : (g_bias < 0 ? "BEARISH" : "NEUTRAL")));
      //--- Mark the break of structure on its broken swing level
      if(g_bias != 0 && VisualsAllowed())
        {
         //--- Select the broken level for the new bias
         bool bull = (g_bias > 0);
         double brokenLevel = bull ? g_biasSwingHigh : g_biasSwingLow;
         //--- Anchor the marker to the bar that broke structure
         datetime bosBar = iTime(_Symbol, InpBiasTimeframe, 1);
         string nm = "BB_BOS_" + IntegerToString((int)bosBar);
         color clr = bull ? InpBiasHighColor : InpBiasLowColor;
         //--- Draw the break-of-structure marker on its line
         DrawMarker(nm, bosBar, brokenLevel, bull, clr, bull ? ANCHOR_UPPER : ANCHOR_LOWER);
         //--- Anchor the label above a bullish break or below a bearish one
         ENUM_ANCHOR_POINT a = ANCHOR_LEFT_LOWER;
         if(bull) a = ANCHOR_LEFT_UPPER;
         //--- Label the break of structure
         DrawText(nm + "_t", bosBar, brokenLevel, bull ? " BULLISH BOS" : " BEARISH BOS", clr, a);
        }
     }
  }

//+------------------------------------------------------------------+
//| Detect the latest confirmed entry-TF swing high and low          |
//+------------------------------------------------------------------+
void DetectEntrySwings()
  {
//--- Scan for a fresh entry-TF pivot, else bail out
   bool isHigh, isLow; double hi, lo; datetime t;
   if(!ScanPivot(InpEntryTimeframe, InpMssSwingLookback, isHigh, isLow, hi, lo, t)) return;
//--- Handle a fresh swing high
   if(isHigh && t != g_lastSwingHighTime)
     {
      //--- Label it H, HH or LH against the previous high
      string label; color clr = InpSwingHighColor;
      if(g_lastSwingHigh <= 0)      label = "H";
      else if(hi > g_lastSwingHigh) label = "HH";
      else                        { label = "LH"; clr = InpSwingLowColor; }
      //--- Draw the swing-high marker
      DrawSwingMarker(true, t, hi, label, clr);
      //--- Store the new swing high
      g_lastSwingHigh = hi; g_lastSwingHighTime = t;
     }
//--- Handle a fresh swing low
   if(isLow && t != g_lastSwingLowTime)
     {
      //--- Label it L, LL or HL against the previous low
      string label; color clr = InpSwingLowColor;
      if(g_lastSwingLow <= 0)      label = "L";
      else if(lo < g_lastSwingLow)  label = "LL";
      else                       { label = "HL"; clr = InpSwingHighColor; }
      //--- Draw the swing-low marker
      DrawSwingMarker(false, t, lo, label, clr);
      //--- Store the new swing low
      g_lastSwingLow = lo; g_lastSwingLowTime = t;
     }
  }

Мы определяем функцию UpdateBias для задания дневной направленности. Мы вызываем ее один раз на бар таймфрейма, выбранного для определения направленности, и с помощью ScanPivot обновляем последние свинговые максимум и минимум. Закрытие бара выше последнего свингового максимума задает бычью направленность, а ниже последнего свингового минимума — медвежью. Мы реагируем только на смену направленности: записываем изменение в лог и отмечаем пробой структуры на пробитом уровне. Именно этот фильтр определяет, в какую сторону можно торговать после снятия ликвидности. Затем мы определяем функцию DetectEntrySwings для отслеживания структуры на младшем таймфрейме. Каждую новую опорную точку классифицируем как H/HH/LH или L/LL/HL и сохраняем, поскольку после активации сетапа сохраненные уровни используются для срабатывания триггера смены рыночной структуры.

Управление жизненным циклом сессии

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

//+------------------------------------------------------------------+
//| Handle the day rollover and active-session lifecycle             |
//+------------------------------------------------------------------+
void UpdateSessionState()
  {
//--- Resolve which kill zone is active now
   string active = GetActiveSession();
//--- Track whether a new session just opened this bar
   bool justStarted = false;
//--- React only when the active session changes
   if(active != g_activeSession)
     {
      //--- Initialize state when entering a new session
      if(active != "")
        {
         //--- Stamp the session start time
         g_sessionStart      = TimeCurrent();
         //--- Seed the range with the first session bar
         g_sessionHigh       = iHigh(_Symbol, InpEntryTimeframe, 0);
         g_sessionLow        = iLow(_Symbol, InpEntryTimeframe, 0);
         //--- Reset the session counters and flags
         g_sessionBarCount   = 0;
         g_sessionTraded     = false;
         //--- Clear entry swings for a fresh session
         g_lastSwingHigh     = 0.0; g_lastSwingHighTime = 0;
         g_lastSwingLow      = 0.0; g_lastSwingLowTime  = 0;
         //--- Flag the fresh start and log it
         justStarted         = true;
         Log("Entered kill zone: " + active);
        }
      //--- Disarm any pending setup when the session ends
      else if(g_armed) Disarm("kill zone ended");
      //--- Store the new active session name
      g_activeSession = active;
     }
//--- Extend the range with the just-closed in-session bar
   if(g_activeSession != "" && !justStarted)
     {
      //--- Read the last closed bar range
      double hi = iHigh(_Symbol, InpEntryTimeframe, 1);
      double lo = iLow(_Symbol, InpEntryTimeframe, 1);
      //--- Push the session high up when exceeded
      if(hi > g_sessionHigh) g_sessionHigh = hi;
      //--- Push the session low down when exceeded
      if(lo < g_sessionLow)  g_sessionLow  = lo;
     }
  }

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

Подключение обработчиков событий

До сих пор мы создавали отдельные блоки; теперь объединим их в обработчиках событий, вызываемых терминалом. Мы добавляем код инициализации в обработчик события OnInit, код очистки — в обработчик события OnDeinit, а последовательность действий для каждого бара — в обработчик события OnTick.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Cache the symbol digits and point size
   SymDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   SymPoint  = _Point;
//--- Configure the trade object magic number and slippage
   Trade.SetExpertMagicNumber(InpMagicNumber);
   Trade.SetDeviationInPoints(20);
//--- Parse the kill-zone windows once to avoid per-bar work
   g_londonStart = ParseHHMM(InpLondonStart);  g_londonEnd = ParseHHMM(InpLondonEnd);
   g_nyStart     = ParseHHMM(InpNewYorkStart); g_nyEnd     = ParseHHMM(InpNewYorkEnd);
   g_asiaStart   = ParseHHMM(InpAsiaStart);    g_asiaEnd   = ParseHHMM(InpAsiaEnd);
//--- Force a DST offset recompute on first use
   g_nyOffsetDay = -1;
//--- Reset the trade records and setup state
   ArrayResize(g_trades, 0);
   g_armed         = false;
   g_activeSession = "";
//--- Adopt any of our positions already open
   SyncTradeRecords();
//--- Seed the bar-time guards
   g_lastEntryBar = iTime(_Symbol, InpEntryTimeframe, 0);
   g_lastBiasBar  = 0;
//--- Log a ready banner with the key settings
   Log("Bread & Butter Engine EA v" + EA_VERSION + " ready on " + _Symbol +
       " | entry " + EnumToString(InpEntryTimeframe) + " | bias " + EnumToString(InpBiasTimeframe) +
       " | Magic " + IntegerToString(InpMagicNumber));
//--- Report successful initialization
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete our chart objects on a real removal or chart close
   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE)
      ObjectsDeleteAll(0, "BB_");
//--- Clear any chart comment
   Comment("");
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new entry-TF bar
   if(IsNewEntryBar())
     {
      //--- Update the kill-zone session lifecycle
      UpdateSessionState();
      //--- Update the higher-timeframe bias (self-gates to bias bars)
      UpdateBias();
      //--- Do setup work only inside an active kill zone
      if(g_activeSession != "")
        {
         //--- Count another in-session bar
         g_sessionBarCount++;
         //--- Detect the latest entry-TF swings
         DetectEntrySwings();
        }
     }
  }

Начнем с обработчика события OnInit, в котором подготовим все, что нужно программе перед первым тиком. Мы кэшируем количество знаков после запятой в цене символа и размер пункта, чтобы избежать повторных запросов. Затем настраиваем торговый объект: задаем магическое число и небольшое допустимое отклонение цены. Мы один раз разбираем временные границы каждой килл-зоны с помощью функции ParseHHMM, чтобы не обрабатывать эти строки во время работы стратегии. Кроме того, сбрасываем сохраненное значение дня, чтобы при первом обращении принудительно пересчитать смещение с учетом летнего времени. Мы очищаем записи о сделках, сбрасываем состояние сетапа, берем под управление уже открытые позиции нашей программы и инициализируем переменные для проверки времени баров, чтобы первый бар распознавался корректно. Затем выводим сообщение о готовности с основными настройками и возвращаем INIT_SUCCEEDED, подтверждая, что программа готова к работе.

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

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

INITIAL SESSION STATE

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

Активация сетапа

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

//+------------------------------------------------------------------+
//| Try to arm a setup inside the active kill zone                   |
//+------------------------------------------------------------------+
void TryArm()
  {
//--- Require an active session, a bias, and no existing armed setup
   if(g_activeSession == "" || g_bias == 0 || g_armed || g_sessionTraded) return;
//--- Respect the single-trade limit unless multiples are allowed
   if(!InpAllowMultiple && CountOurPositions() > 0) return;
//--- Require enough session bars for a valid range
   if(g_sessionBarCount < InpMinSessionBars) return;
//--- Require a non-degenerate session range
   if(g_sessionHigh <= g_sessionLow) return;
//--- Compute the equilibrium and read the current bid
   double eq    = (g_sessionHigh + g_sessionLow) / 2.0;
   double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
//--- Bearish flow: fade a push into premium after a high sweep
   if(g_bias < 0 && IsDirectionAllowed(false))
     {
      //--- Arm only above equilibrium with a completed sweep
      if(price >= eq && SweepDone(true))
        {
         //--- Latch the armed bearish setup, MSS on a break below the last swing low
         g_armed        = true;
         g_setupDir     = -1;
         g_setupExtreme = g_sessionHigh;
         g_mssLevel     = g_lastSwingLow;
         g_armBarTime   = iTime(_Symbol, InpEntryTimeframe, 0);
         //--- Log the armed bearish setup
         Log("Bearish setup ARMED in " + g_activeSession + " | premium sweep done, waiting for MSS below " + DoubleToString(g_mssLevel, SymDigits));
         //--- Mark the swept high
         DrawMarker("BB_Sweep_" + IntegerToString((int)g_armBarTime), g_armBarTime, g_setupExtreme, false, InpSweepColor, ANCHOR_LOWER);
         //--- Label the swept high
         DrawText("BB_SweepTxt_" + IntegerToString((int)g_armBarTime), g_armBarTime, g_setupExtreme, " SWEEP", InpSweepColor, ANCHOR_LOWER);
         //--- Draw the pending MSS trigger line
         DrawMssLine();
        }
     }
//--- Bullish flow: fade a push into discount after a low sweep
   else if(g_bias > 0 && IsDirectionAllowed(true))
     {
      //--- Arm only below equilibrium with a completed sweep
      if(price <= eq && SweepDone(false))
        {
         //--- Latch the armed bullish setup, MSS on a break above the last swing high
         g_armed        = true;
         g_setupDir     = 1;
         g_setupExtreme = g_sessionLow;
         g_mssLevel     = g_lastSwingHigh;
         g_armBarTime   = iTime(_Symbol, InpEntryTimeframe, 0);
         //--- Log the armed bullish setup
         Log("Bullish setup ARMED in " + g_activeSession + " | discount sweep done, waiting for MSS above " + DoubleToString(g_mssLevel, SymDigits));
         //--- Mark the swept low
         DrawMarker("BB_Sweep_" + IntegerToString((int)g_armBarTime), g_armBarTime, g_setupExtreme, true, InpSweepColor, ANCHOR_UPPER);
         //--- Label the swept low
         DrawText("BB_SweepTxt_" + IntegerToString((int)g_armBarTime), g_armBarTime, g_setupExtreme, " SWEEP", InpSweepColor, ANCHOR_UPPER);
         //--- Draw the pending MSS trigger line
         DrawMssLine();
        }
     }
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new entry-TF bar
   if(IsNewEntryBar())
     {
      //--- Update the kill-zone session lifecycle
      UpdateSessionState();
      //--- Update the higher-timeframe bias (self-gates to bias bars)
      UpdateBias();
      //--- Do setup work only inside an active kill zone
      if(g_activeSession != "")
        {
         //--- Count another in-session bar
         g_sessionBarCount++;
         //--- Detect the latest entry-TF swings
         DetectEntrySwings();
         //--- Try to arm a setup on a sweep
         TryArm();
        }
      //--- Reconcile records only when positions may exist
      if(ArraySize(g_trades) > 0 || PositionsTotal() > 0)
        {
         //--- Drop closed positions then adopt untracked ones
         PruneTradeRecords();
         SyncTradeRecords();
        }
      //--- Draw only when visuals are shown
      if(VisualsAllowed())
        {
         //--- Draw the bias structure levels
         DrawBiasStructure();
         //--- Draw the session box while a session is active
         if(g_activeSession != "") DrawSessionBox();
         //--- Flush the chart updates
         ChartRedraw(0);
        }
     }
  }

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

ARMED SETUP SAMPLE

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

Подтверждение смены рыночной структуры и открытие позиции

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

//+------------------------------------------------------------------+
//| Size, build SL and TP, and open the trade                        |
//+------------------------------------------------------------------+
void OpenTrade(bool isBull)
  {
//--- Respect the single-trade limit unless multiples are allowed
   if(!InpAllowMultiple && CountOurPositions() > 0) return;
//--- Enter at the market on the correct side
   double entry = isBull ? NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), SymDigits)
                         : NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), SymDigits);
//--- Build the stop loss by the selected mode
   double stop;
   if(InpStopLossMode == SL_MANUAL)
     {
      //--- Manual: fixed point distance from entry
      double slDist = InpManualStopPoints * SymPoint;
      stop = isBull ? entry - slDist : entry + slDist;
     }
   else
     {
      //--- Auto: beyond the swept extreme by the buffer
      double buffer = InpStopBufferPoints * SymPoint;
      stop = isBull ? g_setupExtreme - buffer : g_setupExtreme + buffer;
     }
//--- Normalize the stop and measure the risk distance
   stop = NormalizeDouble(stop, SymDigits);
   double riskDistance = MathAbs(entry - stop);
//--- Abort on an invalid risk distance
   if(riskDistance <= 0) { Disarm("invalid risk distance"); return; }
//--- Set the take profit at the reward-to-risk multiple of the stop
   double takeProfit = NormalizeDouble(isBull ? entry + InpRewardRiskRatio * riskDistance
                                              : entry - InpRewardRiskRatio * riskDistance, SymDigits);
//--- Resolve the lot size for this trade
   double lots = ResolveLots(entry, stop);
//--- Abort on a lot sizing error
   if(lots <= 0) { Disarm("lot calc error"); return; }
//--- Send the market order on the correct side
   bool ok = isBull ? Trade.Buy(lots, _Symbol, entry, stop, takeProfit, InpOrderComment)
                    : Trade.Sell(lots, _Symbol, entry, stop, takeProfit, InpOrderComment);
//--- Record and annotate a successful fill
   if(ok)
     {
      //--- Track the new position for later management
      ulong ticket = Trade.ResultOrder();
      AddTradeRecord(ticket, isBull, entry, stop);
      //--- Lock the session to one trade
      g_sessionTraded = true;
      //--- Draw the entry, stop and target levels
      datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
      DrawEntryLevels(isBull, now, entry, stop, takeProfit);
      //--- Log the fill with its resulting reward-to-risk
      double rr = MathAbs(takeProfit - entry) / riskDistance;
      Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, SymDigits) +
          "  SL=" + DoubleToString(stop, SymDigits) + "  TP=" + DoubleToString(takeProfit, SymDigits) +
          "  lots=" + DoubleToString(lots, 2) + "  (R:R " + DoubleToString(rr, 2) + ")");
     }
   else
      //--- Log the failure reason
      Log("Open failed: " + Trade.ResultRetcodeDescription());
//--- Clear the armed flag after the attempt
   g_armed = false;
  }

//+------------------------------------------------------------------+
//| While armed, update the trigger and check for the MSS entry      |
//+------------------------------------------------------------------+
void CheckArmedForEntry()
  {
//--- Do nothing when no setup is armed
   if(!g_armed) return;
//--- Disarm if the bearish bias no longer holds
   if(g_setupDir < 0 && g_bias >= 0) { Disarm("bias no longer bearish"); return; }
//--- Disarm if the bullish bias no longer holds
   if(g_setupDir > 0 && g_bias <= 0) { Disarm("bias no longer bullish"); return; }
//--- Disarm once the kill zone has ended
   if(g_activeSession == "")         { Disarm("kill zone ended"); return; }
//--- Disarm if the setup waited too long without a trigger
   int barsElapsed = iBarShift(_Symbol, InpEntryTimeframe, g_armBarTime);
   if(barsElapsed > InpMaxWaitBars) { Disarm("no MSS in time"); return; }
//--- Read the last closed entry-TF close
   double priorClose = iClose(_Symbol, InpEntryTimeframe, 1);
//--- Handle the armed bearish setup
   if(g_setupDir < 0)
     {
      //--- Track the swept high and refresh the MSS level
      g_setupExtreme = MathMax(g_setupExtreme, iHigh(_Symbol, InpEntryTimeframe, 1));
      if(g_lastSwingLow > 0) g_mssLevel = g_lastSwingLow;
      //--- Extend the MSS line
      DrawMssLine();
      //--- Enter short on a close below the structure level
      if(g_mssLevel > 0 && priorClose < g_mssLevel) OpenTrade(false);
     }
   else
     {
      //--- Track the swept low and refresh the MSS level
      g_setupExtreme = MathMin(g_setupExtreme, iLow(_Symbol, InpEntryTimeframe, 1));
      if(g_lastSwingHigh > 0) g_mssLevel = g_lastSwingHigh;
      //--- Extend the MSS line
      DrawMssLine();
      //--- Enter long on a close above the structure level
      if(g_mssLevel > 0 && priorClose > g_mssLevel) OpenTrade(true);
     }
  }

Мы определяем функцию CheckArmedForEntry для контроля активированного сетапа до тех пор, пока он не сработает или не станет недействительным. Мы деактивируем сетап, если направленность меняется на противоположную сетапу, килл-зона заканчивается или проходит слишком много баров. В остальных случаях обновляем экстремум, за которым была снята ликвидность, и уровень триггера. Триггер срабатывает, когда последний завершенный бар закрывается за этим уровнем структуры, — это и есть смена рыночной структуры. Затем мы определяем функцию OpenTrade для открытия позиции по рынку. В ней задаем стоп либо на фиксированном расстоянии, либо за экстремумом, за которым была снята ликвидность, с дополнительным отступом; устанавливаем тейк-профит согласно заданному соотношению прибыли и риска и рассчитываем объем в лотах. При недопустимом значении риска или объема отменяем открытие позиции. После исполнения ордера мы сохраняем запись о позиции, запрещаем новые сделки до конца сессии, отрисовываем уровни и записываем результат в лог.

TRIGGERED SETUP

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

Управление открытой позицией

После открытия позиции мы управляем ею с помощью двух опциональных инструментов: частичного закрытия при достижении первой цели и подтягивания стопа по мере движения цены. Мы определяем функцию ManageOneTrade для работы с одной позицией и функцию ManageOpenTrades для последовательного обхода всех позиций.

//+------------------------------------------------------------------+
//| Manage one open trade with optional partial and trailing         |
//+------------------------------------------------------------------+
void ManageOneTrade(ulong ticket)
  {
//--- Select the position and confirm it is ours on this symbol
   if(!PositionSelectByTicket(ticket)) return;
   if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) return;
   if(PositionGetString(POSITION_SYMBOL) != _Symbol) return;
//--- Read the core position fields
   bool   isBull      = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
   double entry       = PositionGetDouble(POSITION_PRICE_OPEN);
   double currentStop = PositionGetDouble(POSITION_SL);
   double currentTP   = PositionGetDouble(POSITION_TP);
   double volume      = PositionGetDouble(POSITION_VOLUME);
//--- Recover the original risk distance from the record when available
   int    recIdx       = FindTradeRecord(ticket);
   double riskDistance = (recIdx >= 0 && g_trades[recIdx].riskDistance > 0)
                          ? g_trades[recIdx].riskDistance : MathAbs(entry - currentStop);
//--- Abort on a zero risk distance
   if(riskDistance <= 0) return;
//--- Read the current bid and ask
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
//--- Express open profit in R multiples
   double profitR = isBull ? (bid - entry) / riskDistance : (entry - ask) / riskDistance;
//--- Bank a one-time partial once the first R target is reached
   if(InpUsePartialClose && recIdx >= 0 && !g_trades[recIdx].partialTaken && profitR >= InpPartialAtRR)
     {
      //--- Read the volume constraints for the partial
      double volMin   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
      double volStep  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
      //--- Compute the closable volume snapped to the step
      double closeVol = MathFloor((volume * InpPartialPercent / 100.0) / volStep) * volStep;
      closeVol = NormalizeDouble(closeVol, 2);
      //--- Close the partial only if both sides stay above the minimum
      if(closeVol >= volMin && (volume - closeVol) >= volMin)
         if(Trade.PositionClosePartial(ticket, closeVol))
            Log("Partial close " + DoubleToString(closeVol, 2) + " @ " + DoubleToString(profitR, 2) + "R");
      //--- Mark the partial as taken regardless of outcome
      g_trades[recIdx].partialTaken = true;
     }
//--- Trail the stop once past the minimum profit plus trail distance
   if(InpUseTrailingStop)
     {
      //--- Measure open profit in points
      double profitPoints = isBull ? (bid - entry) / SymPoint : (entry - ask) / SymPoint;
      //--- Activate trailing past the activation threshold
      if(profitPoints >= InpMinProfitPoints + InpTrailPoints)
        {
         //--- Compute the trailed stop behind price
         double newStop = isBull ? bid - InpTrailPoints * SymPoint : ask + InpTrailPoints * SymPoint;
         newStop = NormalizeDouble(newStop, SymDigits);
         //--- Move the stop only when it improves protection
         bool improves = isBull ? (newStop > currentStop) : (currentStop == 0 || newStop < currentStop);
         if(improves) Trade.PositionModify(ticket, newStop, currentTP);
        }
     }
  }

//+------------------------------------------------------------------+
//| Manage every tracked open trade                                  |
//+------------------------------------------------------------------+
void ManageOpenTrades()
  {
//--- Walk the records backward and manage each position
   for(int i = ArraySize(g_trades) - 1; i >= 0; i--)
      ManageOneTrade(g_trades[i].ticket);
  }

Мы определяем функцию ManageOneTrade для управления позицией после ее открытия. Мы берем исходное расстояние от цены входа до стопа из сохраненной записи, а не рассчитываем его по текущему положению стопа — это особенно важно после начала трейлинга. Текущую прибыль выражаем в единицах исходного риска R. Если частичное закрытие включено и прибыль достигает целевого уровня в единицах R, мы один раз закрываем заданный процент объема позиции. Закрываемый объем приводим к установленному шагу; операцию выполняем только в том случае, если и закрываемый, и остающийся объем превышают допустимый минимум. Затем, когда прибыль превышает порог активации трейлинга, мы подтягиваем стоп вслед за ценой, но только если новое положение усиливает защиту позиции. Выше мы определяем функцию ManageOpenTrades как простой цикл по сохраненным записям, благодаря чему этот механизм работает и при разрешенном открытии нескольких позиций. Осталось провести тестирование, о котором пойдет речь далее.


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

Мы компилируем программу и запускаем ее в тестере стратегий MetaTrader 5 в визуальном режиме. Это позволяет последовательно, бар за баром, наблюдать за формированием сессий и активацией сетапов. Результат приведен ниже в формате Graphics Interchange Format (GIF).

BACKTEST GIF

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

График тестирования на исторических данных:

GRAPH

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

REPORT


Заключение

Итак, мы разработали на языке MQL5 программу, реализующую модель Bread and Butter Judas Swing. Она торгует на разворотах в рамках торговых сессий, открывая позиции против движения, которое снимает ликвидность с выходом цены в зону премии или дисконта, но только в соответствии с направленностью старшего таймфрейма. Мы определяем дневную направленность по рыночной структуре и торгуем только в килл-зонах, заданных по нью-йоркскому времени, с автоматическим учетом летнего времени. После снятия ликвидности с выходом цены в нужную зону активируем сетап и ждем смены рыночной структуры, подтверждающего вход. Для сделки используем фиксированный объем или объем, рассчитанный по риску, стоп, заданный по структуре или вручную, а также опциональное частичное закрытие и выход по трейлинг-стопу. Вместо того чтобы гнаться за Judas Swing, мы ждем, пока ловушка будет расставлена и рыночная структура развернется, и только после этого открываем позицию.

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

После прочтения этой статьи вы сможете:

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Khrys Barcelona
Khrys Barcelona | 28 сент. 2026 в 00:11
Отличная работа!
Возможность для нас, розничных продавцов
Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend
В статье реализован автономный советник Adaptive SuperTrend, который выполняет внутренние расчеты на выбранном таймфрейме без использования внешних буферов и файлов индикаторов. В нем предусмотрены расчет размера позиции в лотах на основе риска, стоп-лоссы на основе ATR, пошаговый трейлинг по соотношению прибыли и риска (RR), дополнительное подтверждение для защиты от перерисовки, которое можно включить, и управление торговлей по сессиям. На практике эту структуру можно использовать повторно для единообразной обработки сигналов при появлении нового бара и проверки ордеров на соответствие требованиям брокера.
Трейдинг с экономическим календарем MQL5 (часть 12): Хранение данных в SQLite и предотвращение повторных сделок Трейдинг с экономическим календарем MQL5 (часть 12): Хранение данных в SQLite и предотвращение повторных сделок
В этой статье мы заменяем встроенный CSV-снимок слоем SQLite, который сохраняет события календаря и идентификаторы сработавших сделок между перезапусками. База данных хранится в общей папке терминала и используется как графиками в реальном времени, так и тестером стратегий, поэтому оба режима читают одни и те же данные без перекомпиляции. Загрузчик по запросу с индикатором выполнения на холсте получает историю из API календаря и сохраняет ее для повторного использования в автономном режиме.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Альтернатива временным барам (Часть 4): Эффективность разных типов баров в трендовых стратегиях и стратегиях возврата к среднему Альтернатива временным барам (Часть 4): Эффективность разных типов баров в трендовых стратегиях и стратегиях возврата к среднему
Обеспечивает ли улучшение статистических свойств доходностей прирост эффективности стратегии? Мы фиксируем число временных баров, тиковых баров, тиковых баров дисбаланса и тиковых баров серий на 60,5 млн тиков EURUSD, затем выполняем метаразметку точек входа по RSI, полосам Боллинджера и ADX/DI и оцениваем результаты с помощью перекрестной проверки с очисткой. Ни одно семейство баров не дает стабильного преимущества; эффективность меняется в узких пределах, а лучший вариант не проходит перестановочный тест. Читатели узнают, как контролировать перекрытие, утечку данных и множественную проверку гипотез в исследованиях баров.