Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта
Введение
Трейдеры, торгующие на открытии сессий, часто попадают в ловушку первого движения цены после начала килл-зоны. Цена выходит за пределы диапазона открытия, снимает ликвидность за максимумом или минимумом сессии, а затем резко разворачивается. Этот начальный ложный импульс (Judas Swing) призван выбить стопы трейдеров, которые пытаются догнать первое движение цены. Чтобы последовательно торговать такие развороты, нужен четкий порядок действий: определить направление, оценить, является ли текущая цена дорогой или дешевой относительно диапазона сессии, и подтвердить снятие ликвидности до разворота рыночной структуры. Эта статья предназначена для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, которые хотят автоматизировать торговлю на разворотах в рамках торговых сессий на основе концепций Smart Money.
В предыдущей статье (часть 50) мы автоматизировали стратегию Turtle Soup на основе снятия ликвидности. В этой статье мы создадим программу, реализующую модель Bread and Butter Judas Swing. Программа сочетает дневную направленность, определяемую по данным старшего таймфрейма, килл-зоны, заданные по нью-йоркскому времени, и зоны премии и дисконта, рассчитанные по диапазону каждой сессии. Программа активирует сетап только после снятия ликвидности за экстремумом сессии с выходом цены в нужную зону. Затем она ждет смены рыночной структуры и открывает позицию в соответствии с дневной направленностью. Мы рассмотрим следующие темы:
- Принцип работы модели Bread and Butter Judas Swing
- Реализация на MQL5
- Тестирование на исторических данных
- Заключение
Принцип работы модели Bread and Butter Judas Swing
Модель Bread and Butter Judas Swing основана на одной идее: первое агрессивное движение цены после открытия сессии часто является ловушкой, а не началом тренда. Стопы, расположенные за недавним максимумом или минимумом сессии, представляют собой ликвидность, необходимую крупным участникам для исполнения ордеров, поэтому цену направляют к ним, после чего она разворачивается. Мы торгуем против этого ложного импульса, а не следуем за ним, но только если такую сделку допускает фильтр направления. Таким фильтром служит дневная направленность, определяемая на старшем таймфрейме: подтвержденное закрытие бара за последним свинговым максимумом или минимумом задает бычью или медвежью направленность и определяет, в какую сторону разрешено открывать позиции при развороте.
Время и положение цены дополняют условия сетапа. Мы торгуем только в заданных килл-зонах (Лондон, Нью-Йорк и Азия), ориентируясь на нью-йоркское время. В течение каждой килл-зоны мы строим диапазон по текущим максимуму и минимуму и делим его пополам: выше середины находится зона премии, ниже — зона дисконта. В зоне премии предпочтительны продажи, в зоне дисконта — покупки. При медвежьей направленности требуется снятие ликвидности над максимумом сессии с выходом цены в зону премии, а при бычьей — под минимумом сессии с выходом цены в зону дисконта. Снятие ликвидности — это подготовка, а не вход в рынок. Мы ждем смены рыночной структуры: цена должна вернуться за уровень последнего малого свинга в сторону дневной направленности, а бар — закрыться за этим уровнем. Затем мы открываем позицию, размещаем стоп за экстремумом, за которым была снята ликвидность, и задаем цель с фиксированным соотношением прибыли и риска. Ниже кратко представлены наши цели.

Реализация на 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. В результате на графике с первого взгляда видны направленность, зоны сессии, еще не сработавший триггер и сделка, как показано ниже.

Определение направленности и отслеживание структуры
Теперь мы переходим к логике, которая определяет направление торговли. Здесь мы определяем две функции: первая определяет направленность по рыночной структуре старшего таймфрейма, а вторая отслеживает свинги на таймфрейме входа, по которым задается уровень триггера.
//+------------------------------------------------------------------+ //| 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, поэтому логика выполняется один раз на бар, а не на каждом тике. При появлении нового бара мы обновляем состояние сессии и направленность по старшему таймфрейму. Затем, только во время активной килл-зоны, увеличиваем счетчик баров сессии и определяем последние свинги на таймфрейме входа. При запуске программы мы видим следующее.

Мы видим, что программа инициализировалась и сформировала сессии, что подтверждает корректную работу уже подключенной части логики. Далее добавим вызовы функций активации сетапа, входа и управления сделками, начиная с логики активации.
Активация сетапа
Именно здесь мы объединяем условия модели. Мы определяем функцию 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); } } }
После компиляции мы получаем следующий результат.

Важно, что активация сетапа — это еще не вход: мы лишь установили, что ликвидность снята, а цена находится по нужную сторону уровня равновесия с учетом нашей направленности. Теперь мы ждем смены рыночной структуры. Далее реализуем проверку срабатывания этого триггера.
Подтверждение смены рыночной структуры и открытие позиции
Теперь, когда сетап активирован, мы создаем две функции для открытия позиции: первая отслеживает смену рыночной структуры, а вторая рассчитывает объем, задает защитные уровни и отправляет ордер.
//+------------------------------------------------------------------+ //| 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 для открытия позиции по рынку. В ней задаем стоп либо на фиксированном расстоянии, либо за экстремумом, за которым была снята ликвидность, с дополнительным отступом; устанавливаем тейк-профит согласно заданному соотношению прибыли и риска и рассчитываем объем в лотах. При недопустимом значении риска или объема отменяем открытие позиции. После исполнения ордера мы сохраняем запись о позиции, запрещаем новые сделки до конца сессии, отрисовываем уровни и записываем результат в лог.

После активации и подтверждения сетапа и срабатывания триггера остается управлять позициями. Далее рассмотрим логику управления.
Управление открытой позицией
После открытия позиции мы управляем ею с помощью двух опциональных инструментов: частичного закрытия при достижении первой цели и подтягивания стопа по мере движения цены. Мы определяем функцию 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).

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

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

Заключение
Итак, мы разработали на языке MQL5 программу, реализующую модель Bread and Butter Judas Swing. Она торгует на разворотах в рамках торговых сессий, открывая позиции против движения, которое снимает ликвидность с выходом цены в зону премии или дисконта, но только в соответствии с направленностью старшего таймфрейма. Мы определяем дневную направленность по рыночной структуре и торгуем только в килл-зонах, заданных по нью-йоркскому времени, с автоматическим учетом летнего времени. После снятия ликвидности с выходом цены в нужную зону активируем сетап и ждем смены рыночной структуры, подтверждающего вход. Для сделки используем фиксированный объем или объем, рассчитанный по риску, стоп, заданный по структуре или вручную, а также опциональное частичное закрытие и выход по трейлинг-стопу. Вместо того чтобы гнаться за Judas Swing, мы ждем, пока ловушка будет расставлена и рыночная структура развернется, и только после этого открываем позицию.
Отказ от ответственности: данная статья предназначена исключительно для образовательных целей. Торговля сопряжена со значительными финансовыми рисками, а результаты, полученные при тестировании на исторических данных, не гарантируют будущих результатов. Перед запуском этой программы на реальном рынке критически важны тщательное тестирование на исторических данных и строгое управление рисками.
После прочтения этой статьи вы сможете:
- активировать сетап разворота в рамках сессии только при согласованном выполнении всех условий — направленности старшего таймфрейма, активной килл-зоны и снятия ликвидности с выходом цены в зону премии или дисконта;
- подтверждать входы по смене рыночной структуры и управлять открытыми позициями с помощью стопов, заданных по структуре, а также опционального частичного закрытия и выхода по трейлинг-стопу.
Мы ограничили модель одной сделкой на сессию, но эту же структуру можно расширить, добавив отслеживание нескольких сессий и оценку качества сетапов.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23523
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend
Трейдинг с экономическим календарем MQL5 (часть 12): Хранение данных в SQLite и предотвращение повторных сделок
Альтернатива временным барам (Часть 4): Эффективность разных типов баров в трендовых стратегиях и стратегиях возврата к среднему
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования