In this project we bring the well-known Turtle Soup reversal strategy to life on MetaTrader 5. The premise is simple but powerful. Markets constantly produce false breakouts, where price lunges past a recent high or low, grabs the resting liquidity, and then reverses hard. Rather than chase that breakout, we fade it. The robot keeps a running watch on the highest high and lowest low over a configurable lookback window, twenty bars by default in honour of the original Turtle system, treats those extremes as buy-side and sell-side liquidity, and waits patiently for price to sweep one of them and close back inside the range.
By the end of this build you will have a finished Expert Advisor on your chart. Everything is written out in full, in the order you type it, so you can follow along from an empty file to a compiling program without needing to guess what goes where.
Part 1 — The strategy
Why breakouts fail
Every obvious level on a chart is obvious to everybody, and that is the whole point of it.
When a market has spent twenty bars refusing to trade above a particular high, that high stops being just a price. It becomes a shelf where orders pile up. Traders who are short have their protective buy stops sitting above it. Traders waiting to buy the breakout have their entry orders above it. Anyone who wants confirmation that the trend has resumed is waiting for exactly the same tick.
All of that sits inside one narrow band, and it is the easiest fuel supply in the neighbourhood. So price goes and gets it. It reaches up through the shelf, fills everything resting there in one motion, and then discovers there is nothing left to push it higher, because the orders that would have done the pushing were just consumed. The candle closes back below the level, and everyone who bought that breakout is now holding the worst position on the chart.
The mirror image happens beneath a twenty-bar low, where protective sells and breakout shorts are stacked together.
Turtle Soup is the decision to trade the failure rather than the breakout. We sell the failed break of the high, we buy the failed break of the low, and we only act once the market has shown its hand by closing back inside the range.

The four tests
Price touches levels constantly. If our robot fired on every wick that peeked over a high it would spend its life catching knives, so every candidate setup has to survive four separate questions before it earns an order.
Did the sweep go anywhere? One tick past the level has collected nothing worth collecting. We measure penetration two ways at once: an absolute distance in points, and a percentage of the height of the lookback range. Both must clear their thresholds. The absolute test keeps things sane on quiet instruments, the percentage test keeps things sane when volatility expands and thirty points stops meaning what it used to.
Is the level old enough to hold anything? A high formed two bars ago has had no time to gather orders behind it. Age is our stand-in for accumulation, so the swept extreme must sit a minimum number of bars back before we treat it as liquidity at all.
Has price actually rejected? This is the test to be strict about. We never act on the wick, we act on the close. One or more consecutive bars must close back inside the level, and optionally the confirming bar must close with its body in our trade direction. Trading the wick is a prediction. Trading the close is a response to something that already happened.
Where is the idea wrong? Beyond the extreme the sweep reached. If price climbs back through that exact point, the breakout was genuine and we were standing in front of it. Putting the stop there means a real continuation removes us quickly and cheaply.
The plan
Before writing a line, here is what the finished program has to do:
- Track the highest high and lowest low over a lookback window and treat them as liquidity levels
- Watch a small window of recent bars for a sweep beyond either level
- Confirm the rejection with closes back inside, optionally with a reversal candle body
- Reject sweeps that are too shallow, and levels that are too young
- Place a stop beyond the sweep extreme, and a target either at a reward multiple or at the opposite side of the range
- Respect the broker's minimum stop distance so orders are never rejected
- Draw everything on the chart so its reasoning is visible
- Run detection once per closed bar, never trade the same signal twice, and optionally trail open positions
We build it in nine steps, in that order. Here is what the finished system looks like on a chart:

Part 2 — Building the robot
Nothing works until the program knows what it is allowed to do, so the first thing we write is every setting the trader can reach. This block is the whole configuration layer, and it goes at the very top of a fresh file.
//+------------------------------------------------------------------+
//| TBS Turtle Body Soup.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"
#property strict
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Enums |
//+------------------------------------------------------------------+
enum ENUM_SLTP_METHOD
{
SLTP_Dynamic = 0, // Dynamic (SL beyond sweep extreme)
SLTP_Static = 1 // Static (fixed points)
};
enum ENUM_TRAILING_TYPE
{
Trail_None = 0, // None
Trail_Points = 1 // By Points
};
enum ENUM_TP_MODE
{
TP_RiskReward = 0, // Fixed Risk:Reward multiple
TP_OppositeRange = 1 // Opposite side of the lookback range
};
//+------------------------------------------------------------------+
//| Inputs |
//+------------------------------------------------------------------+
input group "Setup Detection"
input ENUM_TIMEFRAMES InpSignalTimeframe = PERIOD_CURRENT; // Signal Timeframe
input int InpLookbackBars = 20; // Lookback Bars for N-Bar High/Low (Turtle = 20)
input int InpMinExtremeAgeBars = 4; // Min Bars Between Prior Extreme and Sweep
input int InpSweepWindowBars = 1; // Bars Allowed to Contain the Sweep (1 = Classic)
input int InpConfirmCloseBars = 1; // Consecutive Closes Back Inside to Confirm
input bool InpRequireBearBullBody = true; // Require Reversal Candle Body in Trade Direction
input group "Sweep Depth Filter"
input bool InpUseSweepDepthFilter = true; // Enable Sweep Depth Filter
input double InpMinSweepDepthPoints = 30.0; // Min Sweep Beyond Level (Points)
input double InpMinSweepDepthPctRange = 2.0; // Min Sweep as % of Lookback Range
input group "Risk / Orders"
input double InpLotSize = 0.01; // Trade Volume (Lots)
input ENUM_SLTP_METHOD InpSLTPMethod = SLTP_Dynamic; // SL Calculation Method
input ENUM_TP_MODE InpTPMode = TP_RiskReward; // Take-Profit Mode
input double InpRiskRewardRatio = 1.0; // Risk:Reward Ratio (RR Target Mode)
input int InpStaticSLPoints = 5000; // SL Points (Static Method)
input double InpSLBufferPoints = 3000.0; // Extra SL Buffer in Points (Both Methods)
input int InpMaxPositionsPerDirection = 1; // Max Open Positions Per Direction
input int InpMagicNumber = 1107; // Magic Number
input group "Trailing"
input ENUM_TRAILING_TYPE InpTrailingType = Trail_None; // Trailing Stop Type
input double InpTrailingStopPoints = 300.0; // Trailing Stop Distance (Points)
input double InpMinProfitToActivateTrail = 1000.0; // Min Profit to Start Trailing (Points)
input group "Visuals"
input bool InpShowVisuals = true; // Draw Chart Objects
input color InpNBarHighColor = clrOrange; // N-Bar High (Buy-Side Liquidity) Color
input color InpNBarLowColor = clrDodgerBlue; // N-Bar Low (Sell-Side Liquidity) Color
input color InpSellSetupColor = clrCrimson; // Sell Setup Color
input color InpBuySetupColor = C'0,150,0'; // Buy Setup Color
//+------------------------------------------------------------------+
//| Global Variables |
//+------------------------------------------------------------------+
CTrade g_trade; // Trade execution object
datetime g_lastBarTime = 0; // Last processed signal-bar open time
datetime g_lastSellSignalTime = 0; // Last sell signal bar time (deduplication guard)
datetime g_lastBuySignalTime = 0; // Last buy signal bar time (deduplication guard)
double g_cachedRefHigh = 0.0; // Most recent lookback reference high price
double g_cachedRefLow = 0.0; // Most recent lookback reference low price
string OBJ_PREFIX = "TSOUP_"; // Chart object name prefix for bulk cleanupThe "#property" lines are metadata the terminal displays. "#property strict" is the one that matters to us while building, because it makes the compiler treat sloppy code as an error instead of waving it through. The include below them pulls in MetaTrader's trading library, which gives us the "CTrade" class so we never have to assemble a raw order request by hand.
The three enumerations turn numbers into words in the settings window. "ENUM_SLTP_METHOD" decides where the stop goes: "SLTP_Dynamic" anchors it beyond the point the sweep actually reached, "SLTP_Static" uses a fixed distance from entry. "ENUM_TRAILING_TYPE" is the trailing on-off switch. "ENUM_TP_MODE" chooses between "TP_RiskReward", a target set as a multiple of the risk, and "TP_OppositeRange", a target at the far side of the box.
The inputs then map one-for-one onto the plan above. In Setup Detection, "InpLookbackBars" is the Turtle number defining our levels, "InpMinExtremeAgeBars" is the age test, "InpSweepWindowBars" says how many bars the raid may span, "InpConfirmCloseBars" is how much proof of rejection we demand, and "InpRequireBearBullBody" adds the reversal-candle requirement. In Sweep Depth Filter, "InpMinSweepDepthPoints" and "InpMinSweepDepthPctRange" are the two halves of the depth test. Risk and Orders holds the money settings, including "InpSLBufferPoints" for breathing room beyond the stop and "InpMagicNumber", the stamp that lets the robot recognise its own trades and ignore everybody else's. Trailing and Visuals cover management and colours.
The globals at the bottom are the robot's memory between ticks. "g_trade" is the object every order flows through. "g_lastBarTime" is how we notice a new bar has opened. "g_lastSellSignalTime" and "g_lastBuySignalTime" remember which bar already produced a trade, so one setup can never fire twice. "OBJ_PREFIX" is stamped into the name of every chart object we create, which is what lets us wipe them all off in one call later.
Nothing in this step does anything yet. It is the workbench being laid out.
Step 2: Two questions the robot must answer
Detection needs two facts it cannot work out on its own: how much exposure is already open in a direction, and whether price genuinely closed back inside the level it swept. We write both now so the detection logic has them waiting.
//+------------------------------------------------------------------+
//| Count active positions filtered by symbol, magic, and direction |
//+------------------------------------------------------------------+
int CountActivePositions(ENUM_POSITION_TYPE positionType)
{
int count = 0;
//--- Scan all open positions in reverse order
for(int pos = PositionsTotal() - 1; pos >= 0; pos--)
{
//--- Retrieve ticket for this position slot
ulong ticket = PositionGetTicket(pos);
//--- Skip invalid or unselectable tickets
if(ticket == 0) continue;
//--- Count only positions matching this symbol, magic number, and direction
if(PositionGetString(POSITION_SYMBOL) == _Symbol
&& PositionGetInteger(POSITION_MAGIC) == InpMagicNumber
&& PositionGetInteger(POSITION_TYPE) == positionType)
count++;
}
//--- Return total matched positions
return count;
}
//+------------------------------------------------------------------+
//| Check that N consecutive closed bars are inside the level |
//+------------------------------------------------------------------+
bool AllConfirmClosesInsideLevel(bool checkBelowLevel, double referenceLevel, int barsToConfirm)
{
//--- Test each confirmation bar from the most recent closed bar outward
for(int barIdx = 1; barIdx <= barsToConfirm; barIdx++)
{
//--- Fetch closing price of this confirmation bar
double closePrice = iClose(_Symbol, InpSignalTimeframe, barIdx);
//--- Sell confirmation: close must be below the swept high
if(checkBelowLevel)
{
//--- Fail immediately if any close is still above the level
if(!(closePrice < referenceLevel)) return false;
}
else
{
//--- Buy confirmation: close must be above the swept low
//--- Fail immediately if any close is still below the level
if(!(closePrice > referenceLevel)) return false;
}
}
//--- All confirmation bars closed on the correct side
return true;
}"CountActivePositions" walks the open positions backwards and counts only those matching three things at once: our symbol, our magic number, and the direction asked for. That triple filter is what keeps the robot honest on an account where other programs, or you by hand, are also trading. Without it another trade could block a valid setup, or ours could stack exposure it believes it does not have.
"AllConfirmClosesInsideLevel" is the rejection test from Part 1 turned into code. We pass it a flag for which side we are checking, the level, and how many bars must agree. It reads back that many closed bars using "iClose" and checks each close against the level: below it when we are preparing to sell the failed high, above it when we are preparing to buy the failed low.
Notice it returns false the instant one bar closes on the wrong side. A rejection that gets interrupted halfway is not a rejection, and checking the remaining bars would tell us nothing.
Step 3: The drawing kit
The robot should show its reasoning on the chart, not hide it in the journal. These five helpers are the materials everything visual is built from, so they come before anything that uses them.
//+------------------------------------------------------------------+
//| Draw a bounded horizontal price segment as an entry reference |
//+------------------------------------------------------------------+
void DrawHorizontalSegment(string objName, datetime startTime, double price, datetime endTime,
color lineColor, ENUM_LINE_STYLE lineStyle, int lineWidth)
{
//--- Remove any prior object sharing this name
ObjectDelete(ChartID(), objName);
//--- Create a horizontal trend line anchored between startTime and endTime
ObjectCreate(ChartID(), objName, OBJ_TREND, 0, startTime, price, endTime, price);
//--- Apply visual properties
ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, lineColor);
ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, lineStyle);
ObjectSetInteger(ChartID(), objName, OBJPROP_WIDTH, lineWidth);
//--- Disable rightward ray so segment stays bounded
ObjectSetInteger(ChartID(), objName, OBJPROP_RAY_RIGHT, false);
//--- Lock the object from accidental user interaction
ObjectSetInteger(ChartID(), objName, OBJPROP_SELECTABLE, false);
}
//+------------------------------------------------------------------+
//| Draw a filled risk or reward rectangle behind the candles |
//+------------------------------------------------------------------+
void DrawFilledTradeBox(string objName, datetime startTime, double priceA,
datetime endTime, double priceB, color fillColor)
{
//--- Remove any prior object sharing this name
ObjectDelete(ChartID(), objName);
//--- Create rectangle spanning entry price to SL or TP
ObjectCreate(ChartID(), objName, OBJ_RECTANGLE, 0, startTime, priceA, endTime, priceB);
//--- Apply solid fill and visual properties
ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, fillColor);
ObjectSetInteger(ChartID(), objName, OBJPROP_FILL, true);
//--- Render behind candlesticks so price action stays readable
ObjectSetInteger(ChartID(), objName, OBJPROP_BACK, true);
ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(ChartID(), objName, OBJPROP_WIDTH, 1);
//--- Lock the object from accidental user interaction
ObjectSetInteger(ChartID(), objName, OBJPROP_SELECTABLE, false);
}
//+------------------------------------------------------------------+
//| Draw a Spectra-style vertical signal arrow with rotated label |
//+------------------------------------------------------------------+
void DrawSignalArrowAndLabel(string baseName, datetime signalTime, double anchorPrice,
bool isBuy, string labelText, color signalColor)
{
//--- Select Wingdings glyph: 217 = up arrow for buy, 218 = down arrow for sell
int arrowCode = isBuy ? 217 : 218;
//--- Rotate label -90 degrees (upward) for buy, +90 degrees (downward) for sell
double labelAngle = isBuy ? -90.0 : 90.0;
//--- Build unique object names for the arrow and text components
string arrowObjName = baseName + "_ARW";
string textObjName = baseName + "_TXT";
//--- Remove any prior arrow object
ObjectDelete(ChartID(), arrowObjName);
//--- Create the directional arrow at the signal bar price
ObjectCreate(ChartID(), arrowObjName, OBJ_ARROW, 0, signalTime, anchorPrice);
ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_ARROWCODE, arrowCode);
ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_COLOR, signalColor);
ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_WIDTH, 2);
//--- Render in foreground so arrow sits above candles
ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_BACK, false);
//--- Anchor top of arrow below candle for buy; anchor bottom above candle for sell
ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM);
ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_SELECTABLE, false);
//--- Remove any prior text label object
ObjectDelete(ChartID(), textObjName);
//--- Create the rotated label at the same coordinate as the arrow
ObjectCreate(ChartID(), textObjName, OBJ_TEXT, 0, signalTime, anchorPrice);
//--- Four leading spaces visually clear the arrow glyph before the label text
ObjectSetString(ChartID(), textObjName, OBJPROP_TEXT, " " + labelText);
ObjectSetString(ChartID(), textObjName, OBJPROP_FONT, "Calibri Bold");
ObjectSetInteger(ChartID(), textObjName, OBJPROP_FONTSIZE, 11);
ObjectSetInteger(ChartID(), textObjName, OBJPROP_COLOR, signalColor);
//--- Apply vertical rotation to render the label upright beside the arrow
ObjectSetDouble(ChartID(), textObjName, OBJPROP_ANGLE, labelAngle);
ObjectSetInteger(ChartID(), textObjName, OBJPROP_ANCHOR, ANCHOR_LEFT);
ObjectSetInteger(ChartID(), textObjName, OBJPROP_SELECTABLE, false);
}
//+------------------------------------------------------------------+
//| Render or update a dotted horizontal price level line |
//+------------------------------------------------------------------+
void RenderHorizontalLevel(string objName, double levelPrice, color levelColor, string tooltipText)
{
//--- Create the horizontal line only on the first call
if(ObjectFind(ChartID(), objName) < 0)
{
ObjectCreate(ChartID(), objName, OBJ_HLINE, 0, 0, levelPrice);
ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT);
ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, tooltipText);
}
//--- Update price and color on every subsequent call
ObjectSetDouble(ChartID(), objName, OBJPROP_PRICE, levelPrice);
ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, levelColor);
ChartRedraw(ChartID());
}
//+------------------------------------------------------------------+
//| Create or replace a text label at a chart time/price coordinate |
//+------------------------------------------------------------------+
void RenderPriceLabel(string objName, datetime labelTime, double labelPrice,
string labelText, color textColor, int anchorType)
{
//--- Always delete and recreate to guarantee position accuracy
ObjectDelete(ChartID(), objName);
ObjectCreate(ChartID(), objName, OBJ_TEXT, 0, labelTime, labelPrice);
ObjectSetString(ChartID(), objName, OBJPROP_TEXT, labelText);
ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, textColor);
ObjectSetInteger(ChartID(), objName, OBJPROP_ANCHOR, anchorType);
ObjectSetInteger(ChartID(), objName, OBJPROP_FONTSIZE, 10);
ChartRedraw(ChartID());
}"DrawHorizontalSegment" draws a flat line pinned between two times at one price. It deletes any object already using that name, creates a trend line, and switches off "OBJPROP_RAY_RIGHT" so the line stops where we told it instead of running to the edge of the chart. "DrawFilledTradeBox" is the same idea as a filled rectangle, with "OBJPROP_BACK" set true so it renders behind the candles and price stays readable through the fill. Both delete before creating, which makes them safe to call repeatedly.
"DrawSignalArrowAndLabel" marks the bar that triggered a trade. Wingdings glyph 217 points up for a buy, 218 points down for a sell, and the anchor flips so the arrow sits below the candle for a buy and above it for a sell. The text label is placed at the same coordinate and rotated ninety degrees through "OBJPROP_ANGLE", so it stands upright beside the arrow instead of running across the price action. The four spaces in front of the label text are a small trick to push the words clear of the arrow without any coordinate maths.
"RenderHorizontalLevel" behaves differently from the rest on purpose. The liquidity levels are permanent fixtures that move as the lookback window slides, so it uses "ObjectFind" to create the line only on the first call and then just updates price and colour every time after. The level glides instead of blinking.
"RenderPriceLabel" does the opposite and rebuilds every time. Text anchors need to be exact, and deleting and recreating is the cheapest way to guarantee that.
Step 4: Drawing a complete setup
Now we combine those five helpers into one function that draws an entire trade: the sweep zone, the coloured risk and reward blocks, the entry line, the arrow, and the price readouts.
//+------------------------------------------------------------------+
//| Draw sweep box, filled R/R boxes, signal arrow, and price labels |
//+------------------------------------------------------------------+
void DrawTradeSetupVisuals(bool isBuy, double sweptLevel, double sweepExtreme, double entryPrice,
double stopLoss, double takeProfit, datetime signalBarTime)
{
//--- Build a unique object name suffix from the signal bar timestamp
string timeSuffix = IntegerToString((long)signalBarTime);
//--- Choose colors based on trade direction
color setupColor = isBuy ? InpBuySetupColor : InpSellSetupColor;
//--- Clamp sweep window to at least one bar
int sweepWindowSize = MathMax(InpSweepWindowBars, 1);
//--- Resolve the start time of the sweep window and the current bar time
datetime sweepStartTime = iTime(_Symbol, InpSignalTimeframe, sweepWindowSize);
datetime currentBarTime = iTime(_Symbol, InpSignalTimeframe, 0);
//--- Project the trade annotation box eight bars into the future for readability
datetime tradeBoxEndTime = currentBarTime + (datetime)(PeriodSeconds(InpSignalTimeframe) * 8);
//--- Define annotation colors
color slBorderColor = clrCrimson; // Stop loss label color
color tpBorderColor = C'0,140,0'; // Take profit label color (dark green)
color entryLineColor = C'40,40,40'; // Entry line and label color (near-black)
color riskBoxFill = C'255,200,200'; // Risk zone fill (light red)
color rewardBoxFill = C'200,235,200'; // Reward zone fill (light green)
color buyArrowColor = C'0,150,0'; // Buy signal arrow color
color sellArrowColor = clrCrimson; // Sell signal arrow color
//--- Compute sweep rectangle boundary prices
double rectTop = MathMax(sweptLevel, sweepExtreme);
double rectBot = MathMin(sweptLevel, sweepExtreme);
//--- Draw the sweep zone rectangle from sweep window start to current bar
string sweepRectName = OBJ_PREFIX + "Sweep_" + timeSuffix;
ObjectCreate(ChartID(), sweepRectName, OBJ_RECTANGLE, 0, sweepStartTime, rectTop, currentBarTime, rectBot);
ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_COLOR, setupColor);
ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_FILL, false);
ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_BACK, true);
ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_WIDTH, 2);
//--- Place the "Turtle Soup (sweep)" label centered vertically on the left edge of the box
double sweepBoxMidPrice = (rectTop + rectBot) * 0.5;
RenderPriceLabel(OBJ_PREFIX + "SwTxt_" + timeSuffix, sweepStartTime, sweepBoxMidPrice,
"Turtle Soup (sweep) ", setupColor, ANCHOR_RIGHT);
//--- Draw the filled risk box (entry to SL) and reward box (entry to TP)
DrawFilledTradeBox(OBJ_PREFIX + "RiskBox_" + timeSuffix,
signalBarTime, entryPrice, tradeBoxEndTime, stopLoss, riskBoxFill);
DrawFilledTradeBox(OBJ_PREFIX + "RewBox_" + timeSuffix,
signalBarTime, entryPrice, tradeBoxEndTime, takeProfit, rewardBoxFill);
//--- Draw the entry reference line spanning the width of the trade box
DrawHorizontalSegment(OBJ_PREFIX + "EnLine_" + timeSuffix,
signalBarTime, entryPrice, tradeBoxEndTime, entryLineColor, STYLE_SOLID, 2);
//--- Anchor the signal arrow to bar low for buy, bar high for sell
double signalArrowPrice = isBuy
? iLow (_Symbol, InpSignalTimeframe, 1)
: iHigh(_Symbol, InpSignalTimeframe, 1);
DrawSignalArrowAndLabel(OBJ_PREFIX + "Sig_" + timeSuffix,
signalBarTime, signalArrowPrice, isBuy,
isBuy ? " Buy" : " Sell",
isBuy ? buyArrowColor : sellArrowColor);
//--- Compute SL distance in points and achieved R multiple for the labels
double slDistancePoints = MathAbs(entryPrice - stopLoss) / _Point;
double achievedRMultiple = (MathAbs(entryPrice - stopLoss) > 0.0)
? MathAbs(takeProfit - entryPrice) / MathAbs(entryPrice - stopLoss)
: 0.0;
//--- For buy: SL label anchors below, TP label anchors above; mirror for sell
int slLabelAnchor = isBuy ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER;
int tpLabelAnchor = isBuy ? ANCHOR_RIGHT_LOWER : ANCHOR_RIGHT_UPPER;
//--- Render entry, SL, and TP price labels at the right edge of the trade box
RenderPriceLabel(OBJ_PREFIX + "EnTxt_" + timeSuffix, tradeBoxEndTime, entryPrice,
"Entry " + DoubleToString(entryPrice, _Digits) + " ",
entryLineColor, ANCHOR_RIGHT);
RenderPriceLabel(OBJ_PREFIX + "SlTxt_" + timeSuffix, tradeBoxEndTime, stopLoss,
"SL " + DoubleToString(stopLoss, _Digits)
+ " (" + DoubleToString(slDistancePoints, 0) + " pts) ",
slBorderColor, slLabelAnchor);
RenderPriceLabel(OBJ_PREFIX + "TpTxt_" + timeSuffix, tradeBoxEndTime, takeProfit,
"TP " + DoubleToString(takeProfit, _Digits)
+ " (" + DoubleToString(achievedRMultiple, 2) + "R) ",
tpBorderColor, tpLabelAnchor);
ChartRedraw(ChartID());
}"DrawTradeSetupVisuals" receives the direction, the two prices describing the sweep, the three prices describing the trade, and the timestamp of the confirming bar. That timestamp becomes "timeSuffix" and is appended to every object name in the function, which is why last week's annotations never collide with today's.
It draws in layers. First the sweep zone: a dotted rectangle running from the swept level to the furthest point price reached beyond it, spanning the sweep window, captioned on its left edge. Anyone looking at the chart can see immediately how deep the raid went.
Then the trade. A light red box from entry to stop, a light green box from entry to target, both extended to "tradeBoxEndTime" eight bars into the future so they stay readable as new candles print over them, with a solid entry line across the same width. The arrow anchors to the confirming bar's low for a buy and its high for a sell, using "iLow" and "iHigh", so it always sits clear of the candle.
Finally the numbers. "slDistancePoints" is the entry-to-stop distance divided by "_Point", and "achievedRMultiple" is reward divided by risk with a guard against dividing by zero. Three labels then sit at the right edge carrying the entry price, the stop with its point distance, and the target with its value in R. The anchors flip with direction so no label ever sits on top of the line it describes.
Step 5: Turning a signal into an order
This is where a validated setup becomes a live position. The function works out the stop and target, makes both survive contact with the broker, sends the order, logs it, and calls the drawing routine we just wrote.
//+------------------------------------------------------------------+
//| Calculate SL/TP, place the market order, and log the result |
//+------------------------------------------------------------------+
bool OpenTurtleSoupTrade(bool isBuy, double sweptLevel, double sweepExtreme,
double oppositeRangeLevel, double lookbackRange,
int extremeAgeBars, double sweepDepth, datetime signalBarTime)
{
//--- Gather current market prices and symbol properties
double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double pointSize = _Point;
int priceDigits = (int)_Digits;
long brokerStopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
//--- Convert broker stops level from points to price distance
double minStopDistance = (double)brokerStopsLevel * pointSize;
double entryPrice, stopLoss, takeProfit, riskDistance;
if(isBuy)
{
//--- Enter long at the current ask price
entryPrice = askPrice;
//--- Place SL below the sweep low with optional extra buffer (dynamic), or at a fixed distance (static)
if(InpSLTPMethod == SLTP_Dynamic)
stopLoss = sweepExtreme - InpSLBufferPoints * pointSize;
else
stopLoss = entryPrice - (InpStaticSLPoints + InpSLBufferPoints) * pointSize;
//--- Calculate raw risk distance from entry to SL
riskDistance = entryPrice - stopLoss;
//--- Clamp risk distance to the broker minimum to avoid rejection
if(riskDistance < minStopDistance || riskDistance <= 0.0)
{
riskDistance = minStopDistance;
stopLoss = entryPrice - riskDistance;
}
//--- Target the opposite side of the lookback range if that mode is active
if(InpTPMode == TP_OppositeRange)
{
takeProfit = oppositeRangeLevel;
//--- Fall back to RR ratio if the opposite level is too close to entry
if(takeProfit - entryPrice < minStopDistance)
takeProfit = entryPrice + riskDistance * InpRiskRewardRatio;
}
else
{
//--- Target a fixed RR multiple of the risk distance
takeProfit = entryPrice + riskDistance * InpRiskRewardRatio;
}
//--- Enforce broker minimum stop distance on both SL and TP
if(entryPrice - stopLoss < minStopDistance) stopLoss = entryPrice - minStopDistance;
if(takeProfit - entryPrice < minStopDistance) takeProfit = entryPrice + minStopDistance;
//--- Round SL and TP to the symbol's decimal precision
stopLoss = NormalizeDouble(stopLoss, priceDigits);
takeProfit = NormalizeDouble(takeProfit, priceDigits);
//--- Send the buy market order to the server
if(!g_trade.Buy(InpLotSize, _Symbol, entryPrice, stopLoss, takeProfit, "Turtle Soup Buy"))
{
Print("Buy failed: ", g_trade.ResultRetcodeDescription());
return false;
}
//--- Abort if the server did not confirm the fill
if(g_trade.ResultRetcode() != TRADE_RETCODE_DONE) return false;
//--- Log all relevant trade details to the journal
Print("BUY (Turtle Soup): swept ", DoubleToString(InpLookbackBars), "-bar low ",
DoubleToString(sweptLevel, priceDigits), " by ",
DoubleToString(sweepDepth / pointSize, 0), " pts (",
DoubleToString(sweepDepth / lookbackRange * 100.0, 1), "% of range). Low was ",
extremeAgeBars, " bars old. Entry ", DoubleToString(entryPrice, priceDigits),
" SL ", DoubleToString(stopLoss, priceDigits),
" TP ", DoubleToString(takeProfit, priceDigits));
}
else
{
//--- Enter short at the current bid price
entryPrice = bidPrice;
//--- Place SL above the sweep high with optional extra buffer (dynamic), or at a fixed distance (static)
if(InpSLTPMethod == SLTP_Dynamic)
stopLoss = sweepExtreme + InpSLBufferPoints * pointSize;
else
stopLoss = entryPrice + (InpStaticSLPoints + InpSLBufferPoints) * pointSize;
//--- Calculate raw risk distance from entry to SL
riskDistance = stopLoss - entryPrice;
//--- Clamp risk distance to the broker minimum to avoid rejection
if(riskDistance < minStopDistance || riskDistance <= 0.0)
{
riskDistance = minStopDistance;
stopLoss = entryPrice + riskDistance;
}
//--- Target the opposite side of the lookback range if that mode is active
if(InpTPMode == TP_OppositeRange)
{
takeProfit = oppositeRangeLevel;
//--- Fall back to RR ratio if the opposite level is too close to entry
if(entryPrice - takeProfit < minStopDistance)
takeProfit = entryPrice - riskDistance * InpRiskRewardRatio;
}
else
{
//--- Target a fixed RR multiple of the risk distance
takeProfit = entryPrice - riskDistance * InpRiskRewardRatio;
}
//--- Enforce broker minimum stop distance on both SL and TP
if(stopLoss - entryPrice < minStopDistance) stopLoss = entryPrice + minStopDistance;
if(entryPrice - takeProfit < minStopDistance) takeProfit = entryPrice - minStopDistance;
//--- Round SL and TP to the symbol's decimal precision
stopLoss = NormalizeDouble(stopLoss, priceDigits);
takeProfit = NormalizeDouble(takeProfit, priceDigits);
//--- Send the sell market order to the server
if(!g_trade.Sell(InpLotSize, _Symbol, entryPrice, stopLoss, takeProfit, "Turtle Soup Sell"))
{
Print("Sell failed: ", g_trade.ResultRetcodeDescription());
return false;
}
//--- Abort if the server did not confirm the fill
if(g_trade.ResultRetcode() != TRADE_RETCODE_DONE) return false;
//--- Log all relevant trade details to the journal
Print("SELL (Turtle Soup): swept ", DoubleToString(InpLookbackBars), "-bar high ",
DoubleToString(sweptLevel, priceDigits), " by ",
DoubleToString(sweepDepth / pointSize, 0), " pts (",
DoubleToString(sweepDepth / lookbackRange * 100.0, 1), "% of range). High was ",
extremeAgeBars, " bars old. Entry ", DoubleToString(entryPrice, priceDigits),
" SL ", DoubleToString(stopLoss, priceDigits),
" TP ", DoubleToString(takeProfit, priceDigits));
}
//--- Draw the visual trade setup annotations on the chart
if(InpShowVisuals)
DrawTradeSetupVisuals(isBuy, sweptLevel, sweepExtreme,
entryPrice, stopLoss, takeProfit, signalBarTime);
return true;
}Everything begins with the broker's terms rather than our preferences. We read ask and bid, then read "SYMBOL_TRADE_STOPS_LEVEL" and convert it into "minStopDistance", a real price distance. That number is the floor beneath every level we calculate. Ignoring it is the single most common reason a sensible order comes back rejected.
Follow the long branch. Entry is the ask. Under "SLTP_Dynamic" the stop sits "InpSLBufferPoints" below the sweep extreme, the price at which the failed breakdown would stop being failed. Under the static method it sits a fixed distance below entry. We measure "riskDistance", and if it comes out thinner than the broker accepts, or zero or negative, we widen it to the floor and move the stop with it rather than submitting something guaranteed to bounce.
The target branches on mode. In "TP_OppositeRange" we aim at the far side of the box, but if that level sits closer to entry than the minimum allows we fall back to the risk-reward projection instead of sending a nonsense target. In risk-reward mode we project "riskDistance" multiplied by "InpRiskRewardRatio" beyond entry. Both results are re-checked against the minimum a second time, then passed through "NormalizeDouble" so the prices carry exactly the symbol's precision.
The order goes out through the trade object's "Buy" method. We abandon on a failed send, and again if the return code is anything other than "TRADE_RETCODE_DONE", because a call that returns quietly is not the same as a filled order. On success the journal line records what a post-mortem would want: which level was swept, by how many points, what share of the range that was, how old the level was, and the three prices.
The sell branch is the same thing mirrored — entry at the bid, stop above the sweep high, target projected downward, "Sell" instead of "Buy". Either way the function finishes by calling "DrawTradeSetupVisuals" when visuals are on.
Step 6: The detection engine
This is the strategy itself, and it is the longest function in the program. Once per closed bar it finds the liquidity levels, measures what price did around them, runs all four tests, and fires whichever direction qualifies.
//+------------------------------------------------------------------+
//| Scan for Turtle Soup setups on the most recently closed bar |
//+------------------------------------------------------------------+
void ScanForTurtleSoupSetups()
{
//--- Clamp windowing parameters to their minimum legal values
int sweepWindow = MathMax(InpSweepWindowBars, 1);
int confirmBars = MathMax(InpConfirmCloseBars, 1);
//--- Calculate the minimum number of bars required to run the scan
int minBarsNeeded = InpLookbackBars + sweepWindow + 2;
//--- Abort silently until enough bar history is available
if(Bars(_Symbol, InpSignalTimeframe) < minBarsNeeded) return;
//--- The reference extreme is the highest/lowest of InpLookbackBars bars before the sweep window
int refWindowStartShift = 1 + sweepWindow;
//--- Locate the bar indices of the reference high and low within the lookback range
int refHighBarShift = iHighest(_Symbol, InpSignalTimeframe, MODE_HIGH, InpLookbackBars, refWindowStartShift);
int refLowBarShift = iLowest (_Symbol, InpSignalTimeframe, MODE_LOW, InpLookbackBars, refWindowStartShift);
//--- Abort if either extreme lookup returned an invalid index
if(refHighBarShift < 0 || refLowBarShift < 0) return;
//--- Read the reference high and low prices
double refHighPrice = iHigh(_Symbol, InpSignalTimeframe, refHighBarShift);
double refLowPrice = iLow (_Symbol, InpSignalTimeframe, refLowBarShift);
//--- Compute the full height of the lookback range
double lookbackRange = refHighPrice - refLowPrice;
//--- Abort if the range is degenerate (zero or negative)
if(lookbackRange <= 0.0) return;
//--- Cache the reference extremes for potential external access
g_cachedRefHigh = refHighPrice;
g_cachedRefLow = refLowPrice;
//--- Render the N-bar high and low liquidity level lines and labels
if(InpShowVisuals)
{
RenderHorizontalLevel(OBJ_PREFIX + "HighLevel", refHighPrice, InpNBarHighColor,
"N-Bar High (BSL - buy-side liquidity)");
RenderHorizontalLevel(OBJ_PREFIX + "LowLevel", refLowPrice, InpNBarLowColor,
"N-Bar Low (SSL - sell-side liquidity)");
//--- Fetch current bar time for label placement
datetime currentTime = iTime(_Symbol, InpSignalTimeframe, 0);
RenderPriceLabel(OBJ_PREFIX + "HighTxt", currentTime, refHighPrice,
IntegerToString(InpLookbackBars) + "-Bar High (BSL)", InpNBarHighColor, ANCHOR_RIGHT_LOWER);
RenderPriceLabel(OBJ_PREFIX + "LowTxt", currentTime, refLowPrice,
IntegerToString(InpLookbackBars) + "-Bar Low (SSL)", InpNBarLowColor, ANCHOR_RIGHT_UPPER);
}
//--- Find the highest high and lowest low inside the sweep window [shift 1 .. sweepWindow]
int sweepHighBarShift = iHighest(_Symbol, InpSignalTimeframe, MODE_HIGH, sweepWindow, 1);
int sweepLowBarShift = iLowest (_Symbol, InpSignalTimeframe, MODE_LOW, sweepWindow, 1);
double sweepWindowHigh = iHigh(_Symbol, InpSignalTimeframe, sweepHighBarShift);
double sweepWindowLow = iLow (_Symbol, InpSignalTimeframe, sweepLowBarShift);
//--- Compute the age of each reference extreme in bars (relative to the most recently closed bar)
int refHighAgeBars = refHighBarShift - 1;
int refLowAgeBars = refLowBarShift - 1;
//--- Record the open time of the signal/confirmation bar
datetime signalBarTime = iTime(_Symbol, InpSignalTimeframe, 1);
//--- Evaluate sell setup: fade a false breakout above the N-bar high
bool sellHighSwept = (sweepWindowHigh > refHighPrice);
//--- Verify all confirmation closes are back below the reference high
bool sellClosedBelow = AllConfirmClosesInsideLevel(true, refHighPrice, confirmBars);
//--- Optionally require the confirmation bar to close with a bearish body
bool sellBearishBody = (!InpRequireBearBullBody)
|| (iClose(_Symbol, InpSignalTimeframe, 1) < iOpen(_Symbol, InpSignalTimeframe, 1));
//--- Require the reference high to be old enough to represent a meaningful prior extreme
bool sellExtremeOldEnough = (refHighAgeBars >= InpMinExtremeAgeBars);
//--- Measure how far price swept beyond the reference high
double sellSweepDepth = sweepWindowHigh - refHighPrice;
//--- Apply the optional sweep depth filter (absolute points and range percentage)
bool sellDepthPasses = (!InpUseSweepDepthFilter)
|| (sellSweepDepth >= InpMinSweepDepthPoints * _Point
&& (sellSweepDepth / lookbackRange * 100.0) >= InpMinSweepDepthPctRange);
//--- Trigger a sell trade only when all conditions pass, the signal is not a duplicate, and the position limit allows
if(sellHighSwept && sellClosedBelow && sellBearishBody && sellExtremeOldEnough && sellDepthPasses
&& g_lastSellSignalTime != signalBarTime
&& CountActivePositions(POSITION_TYPE_SELL) < InpMaxPositionsPerDirection)
{
//--- Attempt to open the sell and mark the signal bar as processed on success
if(OpenTurtleSoupTrade(false, refHighPrice, sweepWindowHigh, refLowPrice,
lookbackRange, refHighAgeBars, sellSweepDepth, signalBarTime))
g_lastSellSignalTime = signalBarTime;
}
//--- Evaluate buy setup: fade a false breakout below the N-bar low
bool buyLowSwept = (sweepWindowLow < refLowPrice);
//--- Verify all confirmation closes are back above the reference low
bool buyClosedAbove = AllConfirmClosesInsideLevel(false, refLowPrice, confirmBars);
//--- Optionally require the confirmation bar to close with a bullish body
bool buyBullishBody = (!InpRequireBearBullBody)
|| (iClose(_Symbol, InpSignalTimeframe, 1) > iOpen(_Symbol, InpSignalTimeframe, 1));
//--- Require the reference low to be old enough to represent a meaningful prior extreme
bool buyExtremeOldEnough = (refLowAgeBars >= InpMinExtremeAgeBars);
//--- Measure how far price swept below the reference low
double buySweepDepth = refLowPrice - sweepWindowLow;
//--- Apply the optional sweep depth filter (absolute points and range percentage)
bool buyDepthPasses = (!InpUseSweepDepthFilter)
|| (buySweepDepth >= InpMinSweepDepthPoints * _Point
&& (buySweepDepth / lookbackRange * 100.0) >= InpMinSweepDepthPctRange);
//--- Trigger a buy trade only when all conditions pass, the signal is not a duplicate, and the position limit allows
if(buyLowSwept && buyClosedAbove && buyBullishBody && buyExtremeOldEnough && buyDepthPasses
&& g_lastBuySignalTime != signalBarTime
&& CountActivePositions(POSITION_TYPE_BUY) < InpMaxPositionsPerDirection)
{
//--- Attempt to open the buy and mark the signal bar as processed on success
if(OpenTurtleSoupTrade(true, refLowPrice, sweepWindowLow, refHighPrice,
lookbackRange, refLowAgeBars, buySweepDepth, signalBarTime))
g_lastBuySignalTime = signalBarTime;
}
}It opens defensively. The window inputs are clamped to legal minimums with "MathMax", and if "Bars" reports less history than "minBarsNeeded" we return silently. During the tester's warm-up that happens on almost every tick, and silence is correct.
Then comes the one piece of indexing the whole design rests on. "refWindowStartShift" is set to one plus the sweep window, so the search for the reference extreme starts only after stepping over the bars where the raid may have happened. Read that line twice, because it is easy to get wrong when writing this from scratch: if the lookback included the sweep bars, the highest high of the recent past would frequently be the very spike we are trying to fade. The comparison would be measuring a bar against itself and the setup would never trigger. "iHighest" and "iLowest" return the bar indices, "iHigh" and "iLow" give the prices, and their difference becomes "lookbackRange", which we reject outright if it has collapsed to nothing.
With the levels known we cache them and, when visuals are on, paint them as dotted lines tagged buy-side and sell-side liquidity. Keeping them on screen permanently means the robot's reasoning stays visible even in the long stretches when nothing trades.
Next we measure the raid. "iHighest" and "iLowest" run a second time, now across the sweep window only, producing "sweepWindowHigh" and "sweepWindowLow", the furthest price reached in each direction. Subtracting one from each reference index gives the age of each level in bars. We also grab "signalBarTime", which does double duty as the deduplication key and as the object-name suffix.
The sell case is then written out as five separate booleans, one per test, which makes it easy to read and easy to debug. "sellHighSwept" asks whether the sweep window reached above the reference high at all. "sellClosedBelow" hands the level to "AllConfirmClosesInsideLevel". "sellBearishBody" checks the confirming candle closed lower than it opened, short-circuiting to true when the requirement is switched off. "sellExtremeOldEnough" compares the level's age against "InpMinExtremeAgeBars". "sellDepthPasses" measures the sweep against both the points threshold and the percentage threshold.
All five must hold. On top of that, the signal bar must not already have been traded, and "CountActivePositions" must show room under the cap. Only then does "OpenTurtleSoupTrade" get called.
Watch the ordering on the last line of the block: "g_lastSellSignalTime" is stamped only after the trade reports success. Stamping first would throw away a perfectly good setup any time a send failed for a transient reason.
The buy case below is the same logic mirrored across the range.
Step 7: Knowing when a bar has closed
Detection must run on closed bars only, never on a candle still forming, so we need a cheap way to notice a new one.
//+------------------------------------------------------------------+
//| Detect and record the opening of a new signal-timeframe bar |
//+------------------------------------------------------------------+
bool IsNewSignalBarOpen()
{
//--- Read the open time of the current (forming) bar
datetime currentBarTime = iTime(_Symbol, InpSignalTimeframe, 0);
//--- Return false if the symbol/timeframe has no data yet
if(currentBarTime == 0) return false;
//--- Compare with the last recorded bar time to detect a new bar
if(currentBarTime != g_lastBarTime)
{
//--- Store the new bar time and signal detection
g_lastBarTime = currentBarTime;
return true;
}
//--- Same bar is still forming
return false;
}"IsNewSignalBarOpen" reads the open time of the currently forming bar with "iTime" and compares it against "g_lastBarTime". If the timestamp has moved on it stores the new one and reports a fresh bar, otherwise it reports that nothing changed. The check for a zero timestamp covers the case where the symbol has no data loaded yet, which would otherwise fire a false trigger on the very first tick.
Step 8: Trailing a winner
Management is optional but worth having. This pulls the stop along behind a position that is already working.
//+------------------------------------------------------------------+
//| Advance the trailing stop for all qualifying open positions |
//+------------------------------------------------------------------+
void ApplyPointsBasedTrailingStop()
{
double pointSize = _Point;
//--- Iterate all open positions in reverse order to allow safe modification
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
//--- Fetch the position ticket at this index
ulong ticket = PositionGetTicket(i);
//--- Skip unselectable or invalid tickets
if(ticket == 0) continue;
//--- Skip positions that belong to a different symbol or magic number
if(PositionGetString(POSITION_SYMBOL) != _Symbol
|| PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
//--- Read current SL, TP, and open price for this position
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
//--- Read current bid to measure floating profit
double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
//--- Propose a new SL one trailing distance below the current bid
double newTrailingSL = NormalizeDouble(currentBid - InpTrailingStopPoints * pointSize, _Digits);
//--- Move SL upward only if the new level is higher and minimum profit threshold is met
if(newTrailingSL > currentSL
&& (currentBid - openPrice) > InpMinProfitToActivateTrail * pointSize)
g_trade.PositionModify(ticket, newTrailingSL, currentTP);
}
else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
{
//--- Read current ask to measure floating profit
double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
//--- Propose a new SL one trailing distance above the current ask
double newTrailingSL = NormalizeDouble(currentAsk + InpTrailingStopPoints * pointSize, _Digits);
//--- Move SL downward only if the new level is lower (or SL is zero) and minimum profit threshold is met
if((newTrailingSL < currentSL || currentSL == 0.0)
&& (openPrice - currentAsk) > InpMinProfitToActivateTrail * pointSize)
g_trade.PositionModify(ticket, newTrailingSL, currentTP);
}
}
}"ApplyPointsBasedTrailingStop" walks the open positions in reverse, which is the safe direction whenever the loop body may modify what it is iterating over, and skips anything not carrying our symbol and magic number.
For a long it proposes a stop one "InpTrailingStopPoints" below the bid; for a short, one trail distance above the ask. Two conditions decide whether the proposal is applied. The new level must improve on the existing stop, which stops the trail ratcheting backwards and handing profit away. And floating profit must have cleared "InpMinProfitToActivateTrail", which stops the trail tightening around a position that has not yet earned room to breathe. When both hold, "PositionModify" moves the stop and leaves the target alone.
The short branch also accepts a current stop of zero, covering a position somehow opened without one.

Step 9: Startup, shutdown, and the tick cycle
The last block wires everything into MetaTrader's three event handlers and completes the program.
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Reject initialization if the lookback period is too short to form a range
if(InpLookbackBars < 2)
{
Print("ERROR: InpLookbackBars must be >= 2");
return(INIT_PARAMETERS_INCORRECT);
}
//--- Reject initialization if the sweep window is zero or negative
if(InpSweepWindowBars < 1)
{
Print("ERROR: InpSweepWindowBars must be >= 1");
return(INIT_PARAMETERS_INCORRECT);
}
//--- Reject initialization if the confirmation bar count is zero or negative
if(InpConfirmCloseBars < 1)
{
Print("ERROR: InpConfirmCloseBars must be >= 1");
return(INIT_PARAMETERS_INCORRECT);
}
//--- Reject initialization if RR ratio or lot size would produce invalid orders
if(InpRiskRewardRatio <= 0.0 || InpLotSize <= 0.0)
{
Print("ERROR: InpRiskRewardRatio and InpLotSize must be > 0");
return(INIT_PARAMETERS_INCORRECT);
}
//--- Configure the trade object with the EA's magic number and execution settings
g_trade.SetExpertMagicNumber(InpMagicNumber);
g_trade.SetMarginMode();
g_trade.SetTypeFillingBySymbol(_Symbol);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//--- Remove every chart object this EA created using the shared prefix
ObjectsDeleteAll(ChartID(), OBJ_PREFIX);
ChartRedraw(ChartID());
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Apply trailing stop logic on every tick when trailing is enabled and positions are open
if(InpTrailingType == Trail_Points && PositionsTotal() > 0)
ApplyPointsBasedTrailingStop();
//--- Execute setup detection logic only once per new signal-timeframe bar
if(!IsNewSignalBarOpen()) return;
ScanForTurtleSoupSetups();
}"OnInit" refuses to start on settings that cannot work. A lookback under two bars cannot form a range, a sweep or confirmation window under one is meaningless, and a non-positive lot size or reward ratio guarantees rejected orders. Each returns "INIT_PARAMETERS_INCORRECT" with a message naming the offending input, which is far kinder than a program that loads happily and then does nothing for an hour while you wonder why. Once the settings pass, the trade object gets our magic number, the account's margin mode, and a fill policy suited to the symbol.
"OnDeinit" cleans up. Because every object we ever created carries "OBJ_PREFIX", one call to "ObjectsDeleteAll" removes all of them however many trades were drawn.
"OnTick" is short because it expresses one idea: the program runs on two clocks. Position management is a tick-rate job, so trailing runs first and runs on every tick. Detection is a bar-close job, so it sits behind "IsNewSignalBarOpen" and only reaches "ScanForTurtleSoupSetups" when a fresh bar has actually opened. Open trades get responsive handling and the heavy scan still runs exactly once per closed bar.
That is the complete program. Compile it, and it is ready to attach.
Part 3 — Running it
First run
Save the file into "MQL5/Experts", compile, and drag it onto any chart. Leave the defaults alone the first time: twenty bars lookback, a one-bar sweep window, a single confirming close. That is the classic configuration and it is the right baseline to judge everything else against.
You should immediately see two dotted lines on the chart, orange above and blue below, labelled as buy-side and sell-side liquidity. They move as the lookback window slides. If they are not there, visuals are off or the chart has less history than the robot needs.
When a setup qualifies you get the full annotation: the sweep zone showing how far the raid went, red and green blocks for risk and reward, an arrow on the confirming bar, and price labels carrying the stop distance in points and the target in R. You can read the entire trade off the chart without opening a dialog.
Testing it
Run it in the Strategy Tester in visual mode the first few times rather than jumping straight to a summary table. This is a strategy worth watching. Seeing a shallow poke get rejected by the depth filter in real time tells you more about whether your thresholds are right than any statistic will.

Once you have watched a run through, the summary numbers mean more.


Tuning it
A few things are worth knowing before you optimise.
The lookback length is the personality dial. Twenty bars is tradition, but shorter windows produce many more setups of lower individual quality, and longer windows produce rarer setups against levels that genuinely matter. If you widen it, raise the minimum extreme age with it, or you end up fading levels that are old on paper and irrelevant in practice.
The two depth thresholds should be tuned together and per instrument. Thirty points means something completely different on a five-digit currency pair than on an index CFD, and the percentage threshold exists precisely so you are not re-guessing that number every time you change symbol. If the robot trades too often, tighten the percentage first.
Requiring the reversal body is the cheapest quality filter available. Switching it off will roughly increase your setup count and roughly decrease your win rate, and whether that trade is worth making depends on your target mode. It pairs badly with wide reward multiples.
The sweep window is worth experimenting with on higher timeframes. The default single-bar setting expects the raid and the rejection in quick succession, which is realistic intraday. On a daily chart a sweep often takes two or three sessions, and widening the window catches setups the strict version discards.
What it does not do
It is a reversal system, so it will give money back during strong trends. When a market is genuinely breaking out and running, the sweep-and-fail pattern becomes rare and the ones that appear tend to fail. The filters reduce that exposure, they do not remove it. Adding a higher-timeframe trend filter and only fading in the direction of the larger structure is the obvious next extension, and the code leaves room for it.
It also trades a fixed lot. There is no percentage-risk sizing built in, deliberately, because sizing is the part of any system most worth writing yourself rather than inheriting from someone else's defaults.
Conclusion
We started with an observation any trader can make — that obvious levels get swept and breakouts fail more often than they should — and turned it into a program that acts on it mechanically. Along the way the interesting problems were not the trading ones. They were making the lookback window skip over the sweep bars so a spike cannot nominate itself as its own reference level, respecting the broker's minimum stop distance so orders are not rejected, stamping the signal bar only after a confirmed fill, and separating tick-rate management from bar-close detection.
The complete source is attached. Every behaviour described above is exposed as an input, so nothing needs editing to be tested.