The manual for most "OTE expert advisors" contains one line that quietly cancels the product: draw a Fibonacci retracement on the chart and rename it to OTE. Everything after that line really is automated — the tool watches the levels you drew, waits for price, sizes the position, sends the order. Everything before it is still you, with a mouse, on a chart you have to be sitting in front of.
If you went looking for a way to stop dragging that tool at 3am, you already found the same thing: the marketplace listings that need a hand-named Fibonacci object, and the free code that asks you to type in the 50%, 61% and 100% prices as inputs. That is not automation. It is a drawing tool with a trade button attached.
This page closes that gap. It assumes you already know what the optimal trade entry is — the 61.8% to 79% retracement of a confirmed leg — and why order blocks and fair value gaps belong in the filter. What it adds is the part that has to become code: which bar counts as a swing, which close counts as a break, what the expert advisor does when price touches the band and simply keeps going.
Key Takeaways
The automation lives in the anchors, not the execution. Most "OTE EAs" automate the order and leave the swing selection to you — which is the only judgment call in the setup.
Two rules make the zone codeable: a swing counts only once wing bars have closed either side of it, and the leg counts only once a bar has closed beyond the prior high.
A band alone fires on every retracement. Confluence inside the band plus a reaction rule instead of a first-touch entry are what turn a location into a trigger.
Anything that reads a hand-drawn chart object cannot run in the Strategy Tester — so it cannot be measured, which is the practical case for deriving every level in code.
Table of Contents (21 min read)Contents
What automating the OTE actually requires
Strip the marketing off and there are only three postures on the market.
Manual-anchor tools. You draw the retracement; the tool executes against it. The zone-finding — the only judgment in the whole setup — never left your hands.
Single-threshold code. The EA checks one level, usually 61.8%, and buys when price crosses it. No band, no sweet spot, no confluence, no confirmation. It fires on every retracement deep enough to touch a number.
A coded zone. The EA finds the leg, validates it, derives the levels, checks what is inside the band and decides whether the reaction is good enough to act on.
Only the third one runs while you sleep. The test is blunt: if a human has to touch the chart between the setup forming and the order going out, it is not automated. By that test, the level math is the easy half. Six jobs have to move into code, and only one of them is arithmetic:
Find a candidate impulse leg in raw price data.
Prove it is a real leg — a swing pair confirmed by a break of structure.
Derive the 0.618 / 0.705 / 0.79 prices from those two anchors.
Decide whether anything inside that band justifies a trade.
Decide what counts as price reacting rather than merely arriving.
Attach a stop, a target and a lot size, and know when to give up on the leg.
The execution half was never the hard part - the anchoring half is what still has a handle on it.
Building the zone automatically
The whole problem is two prices. Give the code a swing high and a swing low it can defend, and the Fibonacci band is one subtraction away. Get the anchors wrong and every level below them is wrong in the same direction, silently, on every trade.
Detecting the swing and the break of structure
A swing high is a bar whose high is above the wing bars on either side of it. The important word is either — the bars to the right have to have closed before the swing is real. If your code evaluates the currently-forming bar, a swing appears, disappears and moves as the bar ticks, which is repainting at the source: the EA's own memory of the past changes underneath it. Reading history from index 1 instead of index 0 costs you wing bars of lag and buys back the ability to test anything.
That lag is not a bug to optimise away. It is the price of a swing that means the same thing in a backtest as it does on Monday morning.
The break of structure is the second anchor test, and here it is a precondition, not a project. This EA only needs one boolean: did some bar in the leg close beyond the high that capped price before the swing low? A wick through it does not count — wick-based structure is what turns one clean leg into four overlapping ones. The full detection logic for market structure shifts, including change-of-character handling, is a subject of its own; the zone builder just needs the yes or no.
Same leg, two ways to anchor it: one needs a trader on the chart, the other needs three closed bars and a break of structure.
Anchoring the Fibonacci retracement to that leg
With both anchors held as numbers in memory, the levels are arithmetic.
OTE level from a confirmed bullish legfx
Pr = H − (H − L) × r
wherewhere H = the confirmed swing high (the 0.0 anchor), L = the confirmed swing low (the 1.0 anchor), and r = the retracement ratio. The band runs from r = 0.618 to 0.79, with 0.705 as its midpoint. Example: H = 1.0920, L = 1.0820, so r = 0.705 gives 1.0920 − 0.0100 × 0.705 = 1.08495.
Three numbers and one subtraction — the part everyone draws by hand is the cheapest line in the EA.
Six ratios matter to the logic, and only three of them are the band: 0.5 is equilibrium (a retracement that stalls above it never reached the zone), 0.618 is the band top, 0.705 is the midpoint you fill toward, 0.79 is the band base, 1.0 is the invalidation anchor, and the -0.27 and -0.62 extensions beyond the leg are where targets usually sit. If you want to sanity-check a set of levels by hand before you trust the code, run the leg through a Fibonacci retracement calculator and compare it against what the EA printed.
Notice what is absent from the code below: OBJ_FIBO, ObjectGetDouble, and every other call that reads a graphical object. A zone that lives in a struct can be logged, unit-tested and replayed. A zone that lives in a chart object exists only on the chart where somebody drew it.
mql5ote_zone.mqh
// Build the OTE band from confirmed structure. No chart objects, no drawing.
struct OteZone
{
bool valid;
double anchor_hi, anchor_lo; // the 0.0 and 1.0 prices
double top, sweet, base; // 0.618, 0.705, 0.79
datetime confirmed_at;
};
// A swing is only a swing once `wing` bars have CLOSED on both sides of it.
bool IsSwingHigh(const int i,const int wing,const double &hi[])
{
for(int k=1; k<=wing; k++)
if(hi[i]<=hi[i-k] || hi[i]<=hi[i+k])
return false;
return true;
}
// IsSwingLow is the same function with the comparisons reversed.
bool BuildBullishZone(const string sym,const ENUM_TIMEFRAMES tf,
const int lookback,const int wing,OteZone &z)
{
MqlRates r[];
ArraySetAsSeries(r,true);
if(CopyRates(sym,tf,1,lookback,r)<lookback) // start at 1: bar 0 is still forming
return false;
double hi[],lo[];
ArrayResize(hi,lookback); ArrayResize(lo,lookback);
for(int i=0;i<lookback;i++) { hi[i]=r[i].high; lo[i]=r[i].low; }
int hi_idx=-1, lo_idx=-1;
for(int i=wing;i<lookback-wing && hi_idx<0;i++) // newest confirmed high
if(IsSwingHigh(i,wing,hi)) hi_idx=i;
if(hi_idx<0) return false;
for(int i=hi_idx+1;i<lookback-wing && lo_idx<0;i++) // the low that starts the leg
if(IsSwingLow(i,wing,lo)) lo_idx=i;
if(lo_idx<0) return false;
// Break of structure: some bar in the leg CLOSED above the high that capped
// price before the swing low. A wick through it does not count.
double prior_high=0.0;
for(int i=lo_idx+1;i<=lo_idx+wing*3 && i<lookback;i++)
prior_high=MathMax(prior_high,hi[i]);
bool bos=false;
for(int i=hi_idx;i<=lo_idx && !bos;i++)
if(r[i].close>prior_high) bos=true;
if(!bos) return false;
double H=hi[hi_idx], L=lo[lo_idx], range=H-L;
if(range<=0) return false;
z.valid=true;
z.anchor_hi=H; z.anchor_lo=L;
z.top = H-range*0.618;
z.sweet = H-range*0.705;
z.base = H-range*0.790;
z.confirmed_at=r[hi_idx].time;
return true;
}
Every price the EA acts on is derived, never read from something a human placed on the chart.
The direction flip is mechanical: for a bearish leg you anchor 0.0 at the swing low, 1.0 at the swing high, and add the ratio instead of subtracting it. Most builders keep one function with a bullish flag rather than two near-identical copies — the second copy is where the sign error hides.
Filtering for confluence: order blocks and fair value gaps inside the band
Here is where a working OTE EA parts company with the single-threshold version. The band is a location, not a signal. Price retraces into 61.8%–79% constantly, in trends and in chop alike, and an EA that treats arrival as permission will trade every one of them.
The filter is the fix: fire only when the band contains a structural reason to be there — an unmitigated order block, or an unfilled fair value gap, whose price range overlaps [base, top]. Mechanically it is an intersection test plus a mitigation check, which is why it belongs in its own module with its own tests:
Overlap, not containment. An order block whose upper edge pokes into the band still counts. Requiring the whole block to sit inside the zone rejects most valid setups.
Unmitigated only. If price already traded through the block or filled the gap since it formed, it is spent. Tracking that state is the part people skip, and it is why a filter that looked perfect in code fires on stale zones live.
One timeframe unless you mean it. Pulling the confluence from a lower timeframe than the leg is a legitimate design, but it doubles your data handling and your look-ahead surface. Decide deliberately.
Keep the detector behind a narrow interface — ConfluenceInsideZone(zone) returning a bool — so the entry logic never has to know how it works. The detection algorithm itself is a separate build: the order blocks and fair value gaps that define the OTE zone deserve their own rules, their own edge cases and their own tests, and this module should only consume the answer.
Should the EA enter on the first touch of the zone?
No — and this is the single most common reason a correctly-built zone still produces a bad EA.
The band is narrow. On the worked example below it is seventeen pips wide. First touch means the top edge: the shallowest retracement, the worst fill in the zone, and zero evidence that buyers are there. Worse, price frequently trades clean through the band to the 1.0 anchor and beyond, and an EA that entered at 0.618 has now used its entire risk budget on the least informative price in the setup. That is the textbook false signal shape for this pattern.
So the trigger has to encode a reaction. Pick one rule, code it, and test it — do not stack all four:
A bullish confirmation candle that closes back above 0.705 after tagging deeper into the band.
A displacement close: a bar whose range and close both exceed the recent average, signalling the reaction is not a drift.
A lower-timeframe structure shift inside the band, if you already have that module built.
A resting buy limit at 0.705 with an invalidation stop — no confirmation at all, but a strictly better fill.
The last one deserves a straight answer, because it is the real fork. A limit at the sweet spot gets you the price you wanted and fills on setups a confirmation rule would have missed; it also fills on every setup that is about to keep falling. A market order on confirmation costs you two to four pips of fill quality and skips the ones that never react. Neither dominates. What matters is that both are fully mechanical, so you can measure the difference instead of arguing about it.
Two more trigger conditions belong in the same block. Invalidation: a close beyond the 1.0 anchor kills the leg — drop the zone and rebuild, or the EA will keep buying a level the market has already rejected. Expiry: a zone that has sat untouched for a fixed number of bars is stale structure; give it a bar budget and let it lapse.
flowchart TD
A["New closed bar"] --> B{"Confirmed swing pair<br/>plus a break of structure?"}
B -- No --> Z1["Wait: no leg to anchor"]
B -- Yes --> C["Compute 0.618 / 0.705 / 0.79<br/>from the two anchors"]
C --> D{"Last closed bar<br/>traded inside the band?"}
D -- No --> Z2["Wait: retracement too shallow"]
D -- Yes --> E{"Unmitigated order block<br/>or FVG inside the band?"}
E -- No --> Z3["Skip: a zone without a reason"]
E -- Yes --> F{"Bullish close back<br/>above 0.705?"}
F -- No --> G{"Close beyond<br/>the 1.0 anchor?"}
G -- Yes --> Z4["Invalidate and drop the leg"]
G -- No --> Z5["Keep waiting inside the zone"]
F -- Yes --> H["Size from risk, set SL and TP"]
H --> I["Send the order via CTrade"]
Most bars end in one of the five wait-or-skip terminals. Firing is the rare path, and that is the design working.
Coding the entry trigger in MQL5
OnTick fires on every tick, which is exactly what you do not want. Gate it to one evaluation per closed bar, then run the checks in cost order: the cheap price comparison first, the structural scan second, the confirmation rule last. A magic number keeps this EA's positions distinct from anything else on the account.
mql5ote_ea.mq5
#include <Trade/Trade.mqh>
CTrade trade;
input int InpMagic = 70518; // one magic number per strategy
input double InpRiskPercent = 0.5; // percent of balance risked per trade
input int InpLookback = 300; // bars of history scanned for structure
input int InpSwingWing = 3; // bars that must close either side of a swing
datetime g_last_bar = 0;
OteZone g_zone;
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
return INIT_SUCCEEDED;
}
void OnTick()
{
datetime bar_time=iTime(_Symbol,PERIOD_CURRENT,0);
if(bar_time==g_last_bar) return; // evaluate once per CLOSED bar
g_last_bar=bar_time;
if(PositionSelect(_Symbol)) return; // one OTE position at a time
if(!g_zone.valid || ClosedBeyond(g_zone.anchor_lo,1)) // invalidated leg
BuildBullishZone(_Symbol,PERIOD_CURRENT,InpLookback,InpSwingWing,g_zone);
if(!g_zone.valid) return;
double open1 = iOpen(_Symbol,PERIOD_CURRENT,1);
double close1 = iClose(_Symbol,PERIOD_CURRENT,1);
double low1 = iLow(_Symbol,PERIOD_CURRENT,1);
// Gate 1 - the last closed bar traded inside the band
if(low1>g_zone.top || close1<g_zone.base) return;
// Gate 2 - structural confluence sits inside the band
if(!ConfluenceInsideZone(g_zone)) return;
// Gate 3 - reaction, not first touch: a bullish close back above 0.705
if(close1<=open1 || close1<g_zone.sweet) return;
OpenOteLong(g_zone);
}
Three gates in cost order: the cheap price test first, the structural scan second, the confirmation rule last.
Three details in that skeleton are worth more than they look. PositionSelect stops the EA stacking four entries on one zone while price chops around 0.705. Rebuilding the zone only when it is invalid means the anchors stay stable for the life of the setup instead of drifting bar by bar. And every price read carries the index 1, never 0 — the same discipline as the swing detector, applied at the trigger.
Setting stop-loss, take-profit, and position size
An EA that identifies entries and leaves you to manage them is a signal generator with extra steps. The exits are part of the same logic, and in this setup they come from the same two anchors.
The stop-loss goes below the 1.0 anchor, plus a small structural pad — not at a fixed pip distance. That is the whole point of anchoring to structure: on a 40-pip leg the stop is tight, on a 200-pip leg it is wide, and the position size absorbs the difference.
The take-profit has three defensible homes: the impulse high itself (the 0.0 anchor), the -0.27 extension, or the -0.62 extension for a runner. Many builders split the difference with a partial take profit at the swing high and a trailing remainder, which keeps the leg's own structure as the exit map instead of a fixed pip target.
Sizing then falls out of the stop, never the reverse. Convert the stop distance to ticks, multiply by tick value, divide the cash you are willing to lose by the result, and round down to the volume step:
mql5ote_risk.mqh
double LotsForRisk(const string sym,const double sl_distance,const double risk_cash)
{
double tick_val=SymbolInfoDouble(sym,SYMBOL_TRADE_TICK_VALUE);
double tick_sz =SymbolInfoDouble(sym,SYMBOL_TRADE_TICK_SIZE);
if(tick_val<=0 || tick_sz<=0 || sl_distance<=0) return 0.0;
double lots = risk_cash/((sl_distance/tick_sz)*tick_val);
double step=SymbolInfoDouble(sym,SYMBOL_VOLUME_STEP);
double vmin=SymbolInfoDouble(sym,SYMBOL_VOLUME_MIN);
double vmax=SymbolInfoDouble(sym,SYMBOL_VOLUME_MAX);
lots=MathFloor(lots/step)*step;
return (lots<vmin) ? 0.0 : MathMin(lots,vmax);
}
void OpenOteLong(const OteZone &z)
{
double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
double range = z.anchor_hi-z.anchor_lo;
double pad = 20*SymbolInfoDouble(_Symbol,SYMBOL_POINT); // 2 pips on a 5-digit feed
double sl = z.anchor_lo-pad; // below the 1.0 anchor, not a fixed pip stop
double tp = z.anchor_hi+range*0.27; // the -0.27 extension beyond the leg
double risk_cash = AccountInfoDouble(ACCOUNT_BALANCE)*InpRiskPercent/100.0;
double lots = LotsForRisk(_Symbol,ask-sl,risk_cash);
if(lots<=0) return; // sizing failed - take no trade at all
if(!trade.Buy(lots,_Symbol,0.0,sl,tp,"OTE 0.705"))
PrintFormat("OTE entry rejected, retcode %d",trade.ResultRetcode());
}
The stop is a structural price, so the lot size falls out of it — never the other way round.
The return 0.0 path matters more than the arithmetic. When the computed size rounds below the broker's minimum volume, the honest answer is to skip the trade — an EA that quietly falls back to SYMBOL_VOLUME_MIN has silently doubled or tripled your risk per trade on exactly the widest-stop setups. If you want to see how stop distance and account size interact before you hard-code a percentage, work it through in a position size calculator first.
Whatever the code computes, the arithmetic only describes intent. Fills slip, spreads widen into the close, and a stop placed below structure is still a real loss when price gets there — which is why any expectation you build from this belongs next to the risk warning before it goes near a funded account.
A worked example: from swing leg to filled trade
Numbers make the sequence concrete. This is an illustrative EUR/USD H1 walkthrough, not a backtested result.
The EA scans back and finds a swing low at 1.0820, confirmed by three closed bars either side. Before that low, price was capped at 1.0862. Eight bars later a candle closes at 1.0866 — above 1.0862, on a close, not a wick — so the leg is confirmed. Price runs to a swing high of 1.0920, and three bars close below it without taking it out, so 1.0920 becomes the 0.0 anchor.
Range: 100 pips. The band computes to 1.0858 (0.618), 1.08495 (0.705) and 1.0841 (0.79). An unmitigated bullish order block from the impulse sits between 1.0844 and 1.0852 — inside the band, so the confluence gate passes.
Price retraces. It touches 1.0850 and the EA does nothing: first touch is not a reaction. The next bar trades down to 1.0843, still inside the band and still above the 1.0 anchor, so the zone survives. The bar after that opens at 1.0845 and closes at 1.0854 — bullish, above 0.705 — and the trigger fires at the next tick, filling around 1.0855.
Stop: 1.0815, two pips below the 1.0 anchor — 40 pips of risk. Target: the -0.27 extension at 1.0947, 92 pips away, or roughly 2.3R. On a $5,000 balance risking 0.5%, that is $25 of risk, which on EUR/USD sizes to about 0.06 lots.
EUR/USD H1 - the leg, the band and the fill the EA computed on its ownEUR/USDH1
An illustrative walkthrough, not a backtested result: 40 pips of risk to reach the -0.27 extension, with every level derived from the two anchors.
Every number in that sequence came from two prices the EA found by itself. Change the swing detection parameters and the anchors move, the levels move, and the trade either happens or does not — which is precisely why those parameters deserve more scrutiny than the ratios do.
Testing pitfalls specific to OTE automation
The generic backtesting advice — model spread, model slippage, walk forward — applies here and is a subject of its own. These four failures are specific to OTE logic, and three of them pass a naive test run cleanly.
Swing points that repaint. If any part of the zone builder reads bar 0, the Strategy Tester will show entries at prices the EA could not have known were entries. The tell is a backtest whose fills cluster suspiciously near the exact band base.
Logic that cannot run in the tester at all. This is the fate of every tool built around a hand-drawn Fibonacci object: the tester opens its own chart, with nothing drawn on it, so the object the EA depends on does not exist and the run errors out or trades nothing. If your design cannot execute unattended in the tester, you have no way to measure it — which is the strongest practical argument for the coded zone over the drawn one.
Anchor instability across parameters.wing and lookback decide which leg you anchor to. Sweep them and you will find settings where the results swing wildly. That is not an optimum to harvest; it is a warning that the anchors are fragile, and it is how OTE EAs get overfit to one leg-selection accident.
Symbol precision assumptions. Hard-coding 0.0001 as a pip breaks on JPY pairs, metals and 3-digit feeds. Derive everything from SYMBOL_POINT, SYMBOL_TRADE_TICK_SIZE and SYMBOL_DIGITS.
Once the tester run is clean, the same logic still has to survive real fills. Run it on a demo account through a full session cycle as a forward test before it sizes a live position — the confirmation-candle rule in particular behaves differently when the close you acted on was three pips away by the time the order arrived.
Where this fits in the full ICT EA build
The OTE module is one composable block, and it is deliberately narrow: it turns a confirmed leg into a validated entry, and nothing else. It consumes structure detection and order-block detection from other modules, and it hands a sized order to your risk layer. That separation is what makes each piece testable on its own — and it is why the full ICT-to-EA automation workflow, from MetaTrader 5 setup through deployment, is organised as modules rather than one monolithic OnTick.
One filter worth adding once the core works: a trading session filter restricting entries to the hours your leg selection was designed around. It removes a category of thin-liquidity fires without touching the zone logic at all.
Build it in that order — zone builder, confluence, trigger, risk — and each part can be broken and fixed in isolation. Build it as one function and the first thing you will not be able to answer is which half of it was wrong.
FAQ
Can an EA really build the OTE zone without me drawing anything?
Yes, and the code is shorter than most people expect. The EA needs two confirmed anchor prices — a swing high and a swing low validated by closed bars either side, with a break of structure between them — and the rest of the band is one subtraction per level. What makes commercial tools require a hand-drawn Fibonacci is not a technical limit; it is that anchor selection is the hard, opinionated part, and pushing it back onto the trader avoids having to defend a rule.
Which Fibonacci levels should the EA actually compute?
Six: 0.5 as an equilibrium check, 0.618 and 0.79 as the band edges, 0.705 as the fill target inside it, 1.0 as the invalidation anchor, and one extension (-0.27 or -0.62) for the target. Anything else is chart decoration that costs nothing to omit from the code. Note that 0.705 is not a classical Fibonacci ratio — it is the midpoint of the band, which is exactly why it is a sensible limit price rather than a magic level.
Why does my OTE EA behave differently in the Strategy Tester than on a live chart?
Two causes account for most of it. Either the logic reads a chart object or a manual input that exists on your live chart and not in the tester's own chart, or it evaluates the forming bar, so live it acts on a price that later gets revised while in the tester it sees only settled history. Both are fixed the same way: derive every value in code, and read history from index 1.
Buy limit at 0.705, or market order on confirmation?
Both are valid, mechanical rules, and they fail differently. The limit gives you the better price and catches setups that never produce a confirmation bar, at the cost of filling everything that keeps falling. The confirmation entry skips those but pays a few pips and misses the sharpest reversals. Code the one you can defend, measure it over a large sample, and keep the reward-to-risk ratio comparison honest by changing only that rule between runs.
How far back should the EA look for swings?
Far enough to contain a complete leg on your timeframe, and no further — an over-long lookback keeps re-anchoring to an older, larger structure and produces bands that never get tested. Treat lookback and the swing wing as a pair, and check that small changes to either do not flip your results; if they do, the problem is the anchor rule, not the parameter value.
Sources & Further Reading
Want to go deeper? These independent, authoritative sources shaped this guide — each one is worth reading in full:
The Cross-Market Desk is the SignalBots editorial team for topics that span every market — platform connectors, copy trading, partnership and IB programs, and the general mechanics of trading automation. We research and write the guides that apply no matter what you trade.
Discussions 0
Leave a comment