Automating Trading Strategies in MQL5 (Part 51): The Bread and Butter Judas Swing Model with Premium and Discount
Introduction
Traders who focus on session opens are often trapped by the first move after a kill zone begins. Price pushes out of the opening range, sweeps the session high or low, and then reverses sharply. This early false push (the Judas swing) is designed to take out stops from traders who chase the initial move. Without a structured way to read direction, judge whether price is expensive or cheap relative to the session range, and confirm a sweep before structure turns, these reversals are hard to trade consistently. This article is for MetaQuotes Language 5 (MQL5) developers and algorithmic traders who want to automate a session-based reversal approach grounded in smart money concepts.
In our previous article (Part 50), we automated a Turtle Soup liquidity sweep strategy. In this article, we build the Bread and Butter Judas Swing program. It combines a higher-timeframe daily bias with kill zones defined in New York time and premium and discount zones derived from each session's range. It arms a setup only after a sweep of the session extreme into the correct zone, then waits for a market structure shift before entering in the direction of the bias. We will cover the following topics:
Understanding the Bread and Butter Judas Swing Model
The Bread and Butter Judas Swing model rests on one idea: the first aggressive move after a session opens is often a trap, not a trend. The stops resting beyond the recent session high or low are the liquidity large participants need to fill orders, so price is driven into them and then reverses. We fade that false push rather than follow it, but only with a directional filter behind us. That filter is the daily bias read from a higher timeframe, where a confirmed close beyond the last swing high or low sets the bias bullish or bearish and decides which side of the reversal we are allowed to take.
Timing and location complete the setup. We restrict activity to defined kill zones (London, New York, and Asia), using New York time. Within each kill zone, we build a range from the running high and low and split it at the midpoint into premium (above) and discount (below). Premium favors selling; discount favors buying. A bearish bias then asks for a sweep of the session high into premium, and a bullish bias for a sweep of the session low into discount. The sweep is preparation, not the entry. We wait for a market structure shift: a close back through the last minor swing level in the bias direction. Then we enter, place the stop beyond the swept extreme, and target a fixed reward-to-risk. In a nutshell, here is a representation of our objectives.

Implementation in MQL5
We begin by setting the foundation: the version banner, the trade library, the enumerations that expose our options to the user, the full input set, and the global state the program carries between bars.
//+------------------------------------------------------------------+ //| 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
We establish the foundation here. We define the "EA_VERSION" banner macro and include "Trade.mqh" for order execution, then declare three enumerations — "TradeDirection", "LotSizingMode", and "StopLossMode" — that turn our configuration into readable dropdowns. We expose the inputs in groups: the general settings, stop-loss, reward-to-risk ratio, trailing, optional partial close, daily bias, kill zones in New York time, sweep setup, entry trigger, logging, and chart visuals. We declare the "TradeRecord" structure to store each position's ticket, direction, entry, initial stop, and original risk distance, so we always measure trailing and partials against the risk taken at entry rather than a stop that may have moved. Finally, we hold the global state between bars — the bias, the active session and its range, the confirmed swings, and the armed setup — with the "g_trades" array holding one record per open position.
Time and Session Foundations
Before any setup logic can run, the program needs to know when a new bar has opened and which kill zone is active. We build a small group of time helpers that handle new-bar detection, parse the session windows, resolve New York daylight saving time (DST), and name the active session.
//+------------------------------------------------------------------+ //| 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 ""; }
We build time helpers that run the strategy once per bar and resolve the active kill zone. With "IsNewEntryBar", we report the first tick of each entry-timeframe bar so the heavy logic runs once per bar. With "ParseHHMM", we convert each session input into minutes since midnight, run once at startup so no string parsing touches the live path. We handle New York time across daylight saving in two steps: "NthSundayUtc" and "NyGmtOffset" let us bound the DST window (second Sunday of March to first Sunday of November), where we return an offset of minus four inside it and minus five outside, and with "NyMinutesNow" we report the current New York minute of day, recomputing that offset only when the day changes. Finally, with "InWindow" we test a start-to-end window including wraps past midnight, and with "GetActiveSession" we return the first enabled kill zone we fall inside, or an empty string when none is active.
Pivots, Sizing, and Trade Bookkeeping
With the timing in place, the program needs a set of workers that the setup and entry logic will lean on: a swing pivot detector, direction and lot-sizing helpers, the bookkeeping that keeps our trade records in step with live positions, and the sweep test that confirms liquidity was taken.
//+------------------------------------------------------------------+ //| 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 + "."); }
We build the workers that back the setup and entry logic. With "ScanPivot", we detect the most recent confirmed swing on a timeframe by centering on a candidate bar with the lookback number of closed bars on each side and rejecting it if any neighbor is equally or more extreme — the fractal pivot both the bias and the entry structure depend on. With "IsDirectionAllowed", we enforce the allowed-direction setting, and with "CalcLotsByRisk" and "ResolveLots", we size the position so the stop equals the risk money, snapped to the broker volume step and clamped to its limits. We keep our picture aligned with the terminal through a group of record helpers: with "CountOurPositions" we enforce the single-trade limit, with "AddTradeRecord" we store a fresh fill's original risk, with "PruneTradeRecords" we drop closed positions, and with "SyncTradeRecords" we adopt any position lacking a record, marking it as already partialed so a restart never triggers a surprise scale-out. Finally, with "SweepDone" we confirm a fresh liquidity grab of the prior N-bar extreme, and with "Disarm" we reset the armed state with a logged reason.
Drawing a Horizontal Level
Before the setup and bias logic can show anything on the chart, we need a reliable way to draw a horizontal line between two times at a fixed price. We define the "DrawHLevel" function as that building block, used for every level line in the program.
//+------------------------------------------------------------------+ //| 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); }
We define the "DrawHLevel" function to draw or refresh a horizontal trend-line level from a name, two anchor times, a price, and a color, style, and width. We create the object on first use and simply move both anchors on later calls, so we redraw the same level every bar without piling up duplicates, keeping it a segment rather than a ray and non-interactive. This reuse-or-create pattern is the building block used for every level line, and we follow the same approach for the remaining box, label, and marker helpers.
Drawing the Session, Bias, and Setup Visuals
Here, we build the functions that render the whole setup on the chart: the kill-zone box with its premium and discount zones, the higher-timeframe bias levels, the pending trigger line, the entry and target levels, and the swing markers.
//+------------------------------------------------------------------+ //| 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); }
We render the whole setup on the chart here, routing everything through "DrawHLevel" and its sibling box, text, and marker helpers. With "DrawSessionBox", we fill the premium half above equilibrium and the discount half below it, outline the range, and label the zones — this is where we make the abstract idea of expensive and cheap price visible. With "DrawBiasStructure", we draw the bias high and low whose breaks flip order flow; with "DrawMssLine", we extend the dash-dot trigger line while a setup is armed; with "DrawEntryLevels", we lay down the entry, stop, and target lines with a direction arrow; and with "DrawSwingMarker", we label each confirmed pivot as H/HH/LH or L/LL/HL. Together, these give us a chart where the bias, session zones, pending trigger, and trade are all legible at a glance, as shown below.

Establishing the Bias and Tracking Structure
Now we reach the logic that gives the program its direction. We define two functions here: one that reads the higher-timeframe bias from market structure, and one that tracks the entry-timeframe swings that feed the trigger.
//+------------------------------------------------------------------+ //| 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; } }
We define the "UpdateBias" function to set the daily direction. We run it once per bias-timeframe bar, refresh the latest swing high and low through "ScanPivot", and turn the bias bullish on a close above the last swing high or bearish on a close below the last swing low. We act only when the bias flips, logging the change and marking the break of structure on the broken level — this is the filter that decides which side of a sweep we are willing to trade. We then define the "DetectEntrySwings" function to maintain the lower-timeframe picture, classifying each fresh pivot as an H/HH/LH or L/LL/HL and storing it, since these stored levels become the market structure shift trigger once a setup is armed.
Managing the Session Lifecycle
The setup logic only makes sense inside a live kill zone, so we define the "UpdateSessionState" function to manage that lifecycle: opening a fresh session, closing an old one, and growing the range as the session unfolds.
//+------------------------------------------------------------------+ //| 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; } }
We define the "UpdateSessionState" function to manage the kill-zone lifecycle. When we enter a session, we stamp the start time, seed the range with the current bar, and reset the counters, traded flag, and entry swings for a clean slate; when we leave a session, we disarm any pending setup, since a trigger that never fired is invalid once the window closes. While the session stays active, we extend the range with each just-closed bar — pushing the high up or the low down — which keeps the premium and discount zones and their equilibrium current, and we skip only the opening bar we already seeded.
Wiring the Event Handlers
Everything so far has been building blocks; now we place them inside the event handlers that the terminal calls for us. We add our startup work to the OnInit event handler, our cleanup to the OnDeinit event handler, and our per-bar sequencing to the OnTick event handler.
//+------------------------------------------------------------------+ //| 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(); } } }
We begin with the OnInit event handler, where we prepare everything the program needs before the first tick. We cache the symbol digits and point size to avoid repeated lookups, then configure the trade object with our magic number and a small deviation allowance. We parse each kill-zone window once with the "ParseHHMM" function so those strings never touch the live path, and we force a daylight saving recompute on first use by invalidating the cached day. We clear the trade records and setup state, adopt any of our positions that are already open, and seed the bar-time guards so the first bar is recognized correctly. A ready banner then prints the key settings, and we return INIT_SUCCEEDED to confirm the program is good to run.
We add the cleanup to the OnDeinit event handler, which the terminal calls when the program is removed or the chart closes. We delete only our own chart objects by their shared name prefix, and only on a genuine removal or chart close rather than a routine recompile, so a parameter change does not wipe the visuals unnecessarily. Clearing the chart comment leaves the chart clean behind us.
Finally, we add our per-bar sequence to the OnTick event handler. The whole block is gated by the "IsNewEntryBar" function so the logic runs once per bar rather than on every tick. On a fresh bar, we update the session lifecycle, refresh the higher-timeframe bias, and then, only inside an active kill zone, count the in-session bar and detect the latest entry-timeframe swings. When the program runs, we see the following.

We can see that the program initialized and built the sessions, confirming the partial wiring works. Next, we add the arming, entry, and trade-management calls, beginning with the arming logic.
Arming a Setup
This is where the model's conditions come together. We define the "TryArm" function to watch the active session and latch a pending setup once a sweep into premium or discount lines up with the bias.
//+------------------------------------------------------------------+ //| 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(); } } }
We define the "TryArm" function to decide whether the pieces are in place to prepare a trade. We first clear our gates — an active session, a resolved bias, no setup already armed, no trade taken this session, the single-trade limit respected, enough session bars for a valid range, and a range where the high sits above the low — then compute equilibrium and read the current bid. In bearish flow, when price is at or above equilibrium with a completed sweep of the recent high, we latch a bearish setup: we anchor the stop on the swept high and set the trigger at the last swing low. We mirror this in bullish flow below equilibrium with a swept low. We then call it in the tick handler as below.
//+------------------------------------------------------------------+ //| 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); } } }
After compiling, we get the following result.

The key point is that arming is not entering: we have only recognized that liquidity was taken while price sits on the correct side of equilibrium for our bias. We now wait for structure to shift, and that trigger check is what we build next.
Confirming the Shift and Opening the Trade
With a setup armed, we now build the two functions that turn it into a live position: one that watches for the market structure shift and one that sizes, protects, and sends the order.
//+------------------------------------------------------------------+ //| 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); } }
We define the "CheckArmedForEntry" function to supervise an armed setup until it triggers or is invalidated. We disarm when the bias flips against the setup, the kill zone ends, or too many bars pass; otherwise we keep the swept extreme and trigger level current and fire when the last closed bar closes through that structure level — the market structure shift. We then define the "OpenTrade" function to execute: we enter at market, build the stop either at a fixed distance or beyond the swept extreme by a buffer, set the take-profit at the reward-to-risk multiple, resolve the lot size, and abort cleanly on an invalid risk or lot. On a fill, we record the position, lock the session to one trade, draw the levels, and log the result.

With the setup armed, confirmed, and triggered, what remains is managing the positions, and here is the logic we use to achieve that.
Managing the Open Position
Once a trade is live, we manage it with two optional tools: banking a partial at the first target and trailing the stop as the price runs. We define the "ManageOneTrade" function to handle a single position and the "ManageOpenTrades" function to walk them all.
//+------------------------------------------------------------------+ //| 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); }
We define the "ManageOneTrade" function to handle a position after it opens. We recover the original risk distance from our stored record rather than the live stop — critical once trailing moves the stop — and express open profit as a multiple of R. When it is enabled and profit reaches the R target, we bank a one-time partial of our chosen percentage, snapped to the volume step and closed only if both slices stay above the minimum. We then advance the trailing stop behind price once profit clears its activation threshold, applying it only when it improves protection. Above this, we define the "ManageOpenTrades" function as a thin loop over our records, which lets the scheme scale when multiple positions are allowed. What remains is testing, covered next.
Backtesting
We compile the program and run it in the MetaTrader 5 strategy tester in visual mode, which lets us watch each session build and each setup arm bar by bar. The result is shown below as a Graphics Interchange Format (GIF).

The test confirmed each stage worked as we designed it: the bias filter kept our trades in the higher-timeframe direction, setups armed only after a sweep of the session extreme into premium or discount, entries fired only on a close back through structure, and the optional trailing stop advanced once profit cleared its activation threshold.
Backtest graph:

Backtest report:

Conclusion
In conclusion, we built the Bread and Butter Judas Swing program in MQL5: a session-based reversal model that fades liquidity sweeps into premium and discount, but only in the direction of a higher-timeframe bias. We read the daily bias from structure, restrict activity to New York kill zones with daylight saving resolved automatically, arm a setup after a sweep into the correct zone, wait for a market structure shift to confirm, and then manage the trade with fixed or risk-based sizing, a structural or manual stop, and optional partial and trailing exits. Instead of chasing the Judas swing, we wait for the trap to be set and structure to turn before committing.
Disclaimer: This article is for educational purposes only. Trading carries significant financial risks, and past performance during backtesting does not guarantee future results. Thorough backtesting and careful risk management are essential before deploying this program in live markets.
After reading this article, you will be able to:
- Arm a session reversal only when a higher-timeframe bias, a kill-zone window, and a sweep into premium or discount all align.
- Confirm entries on a market structure shift and manage them with structural stops and optional partial and trailing exits.
We kept the model to one trade per session, but the same structure extends to multi-session tracking and setup grading.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Building a Modular Fair Value Gap (FVG) Detection Engine in MQL5
MCMC Sampling Methods — The Metropolis-Hastings Algorithm
Mathematical Models in Grid Strategies
Neural Networks in Trading: Effective Feature Extraction for Accurate Classification (Final Part)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use