You can read it in half a second. Price has been stacking higher highs and higher lows all session, then one candle closes below the last higher low and the whole picture inverts — the pullbacks you were buying are now the rallies you should be fading.

Your Expert Advisor sees none of that. It sees an array of doubles.

That gap is where most ICT builds stall. Not at the strategy — you already know what a shift in structure means — but at the moment you have to write down, in code, which bar counts as a swing point, what "the trend" is at any given instant, and why this break is a reversal while the last one was continuation. Get it slightly wrong and the detector either repaints (labels that appear, then vanish as the bar finishes) or fires on every wick that pokes through a level.

What follows is that detection layer on its own: how a shift becomes deterministic state your EA can branch on. The code is illustrative MQL5 — the shape of the logic, not a drop-in product.

Key Takeaways
  • A market structure shift only becomes tradeable logic when three things exist in code: a confirmed swing level, a stored bias variable, and a break test evaluated on closed bars.
  • BOS and CHoCH are the same price event - a close beyond a swing level. The bias in force before the break is the only thing that separates continuation from reversal.
  • Every non-repainting detector pays a fixed confirmation lag, because a pivot is defined partly by the bars that come after it. That lag is the feature, not the bug.
  • Running the same detector at two lookbacks (internal and external) turns most false reversals into what they actually are: pullbacks inside an intact leg.
Table of Contents (24 min read)Contents

What a Market Structure Shift Signals for an EA

To you, a market structure shift is a read. To an EA it has to be an event: a typed record carrying a direction, the price level that was broken, and the timestamp of the bar that confirmed it — produced once, never revised.

That record is worth building because it is the only thing on the chart that answers two different questions with one object:

  • Which way am I allowed to trade? The event sets a directional bias every other module can query before it does anything.
  • Has something just changed, and where was it invalidated? The event is a timestamped trigger, and the level it broke is a natural invalidation reference for risk.

Here is the part that makes it awkward to code. A break of structure (BOS) and a change of character (CHoCH) are the same price event — a close beyond a prior swing level. What separates them is entirely external to the break: the bias the market was in a moment earlier. Break upward while bullish and you have continuation. Break downward while bullish and you have a character change. Identical arithmetic, opposite meaning.

So a detector that inspects only the breaking bar can never classify anything. It needs memory — and that memory has to survive restarts, be identical in the tester and live, and update in exactly one place.

The illustrative path below is what that looks like from the EA's side. Nothing on this chart is real market data; it is a constructed sequence so each label has one unambiguous cause.

Illustrative 4H path: an uptrend flips bias, then confirms EUR/USD 4H
Constructed data, not a real market: the break that flips the bias (CHoCH) always comes before the break that confirms the new direction (BOS).

Walk it as the code would. The uptrend is not "an uptrend" to the EA — it is a stored high at 1.0880 and a stored low at 1.0840. When a bar closes above 1.0880, the break agrees with a bullish bias, so it is a BOS; the stored levels re-anchor to 1.0940 and 1.0890. Then a bar closes below 1.0890. That break disagrees with the stored bias, so the same comparison that produced "BOS" three legs ago now produces "CHoCH", and the bias variable flips bearish. The next break down — through 1.0854 — agrees with the new bias and is a BOS again.

One event type changed nothing about the price action. It changed what the EA is allowed to do next.

The Non-Repainting Swing Points Every Detector Needs

Everything above rests on those stored levels, so the swing detector is the piece to get right first. Everything downstream inherits its mistakes.

A swing high is not "the highest price recently". It is a bar whose high is greater than the highs of a fixed number of bars on both sides — the classic fractal rule. The left side costs you nothing: those bars are already history. The right side is the whole problem, because it does not exist yet.

That asymmetry produces the single most important property of a usable detector: a swing point can only be confirmed N bars after it printed, where N is your right-hand lookback. A detector that labels the forming bar is not faster — it is repainting, and every backtest it produces is fiction, because in the test the future bars are already there.

mql5 swing_detector.mqh
input int InpLeft  = 3;   // bars that must be lower to the LEFT
input int InpRight = 3;   // bars that must be lower to the RIGHT

// Series-indexed array: shift 0 is the forming bar, 1 the last closed bar.
// A candidate is only testable once InpRight bars have closed to its right,
// which is why nothing is ever labelled on bar 0.
bool IsSwingHigh(const double &high[], const int shift)
  {
   const double pivot = high[shift];

   for(int i = 1; i <= InpLeft; i++)          // older bars
      if(high[shift + i] >= pivot) return(false);

   for(int i = 1; i <= InpRight; i++)         // newer bars, already closed
      if(high[shift - i] >= pivot) return(false);

   return(true);
  }

// The one bar worth testing on each new bar: it has just finished
// collecting its right-hand confirmation.
int SwingCandidateShift() { return(InpRight + 1); }
The pivot is confirmed InpRight bars after it printed. That lag is the price of a detector that never revises its own history.

Four decisions hide inside those few lines, and each one changes the output:

  1. Lookback size. A three-bar right side confirms quickly and gives you many small pivots; fifteen bars gives you few, structurally significant ones. Neither is correct in the abstract — they are answering different questions, which is exactly the distinction the internal/external section below is built on.
  2. Ties. Use >= when rejecting neighbours, not >. Two bars with identical highs will otherwise both qualify, and your "most recent swing high" flickers between them on symbols with rounded pricing.
  3. Which candidate to test. On each new bar there is exactly one bar that has just finished collecting its right-hand confirmation — shift InpRight + 1. Re-scanning the whole history every bar is wasted work and invites accidental repaints.
  4. What you store. Keep the level and its bar time. The time is what later lets you check whether the level is stale, and whether the break came after the pivot rather than before it.

MT5 ships a built-in fractal indicator, and it is a perfectly reasonable starting point — but its window is fixed, which means you cannot run the same logic at two different structural scales. Writing the pivot check yourself is a dozen lines and buys you the parameter you will actually want to tune.

Tracking Bias So the EA Knows Continuation From Reversal

Bias is state, not a calculation. That distinction is easy to skim past and expensive to ignore: if you recompute "the trend" from scratch on every bar with some sliding-window rule, your classification of an identical break can change depending on how far back you happened to look. Stored state cannot drift like that.

The minimum viable model is three states — undefined, bullish, bearish — and exactly one transition rule: bias changes only when a confirmed break occurs, and it changes to the direction of that break.

stateDiagram-v2
    state "Bias undefined" as Undefined
    state "Bullish bias" as Bull
    state "Bearish bias" as Bear

    [*] --> Undefined
    Undefined --> Bull: first break up (logged BOS)
    Undefined --> Bear: first break down (logged BOS)
    Bull --> Bull: close above stored high (BOS)
    Bull --> Bear: close below stored low (CHoCH)
    Bear --> Bear: close below stored low (BOS)
    Bear --> Bull: close above stored high (CHoCH)
    
Only four transitions exist, and just two of them change the state — the self-transitions are continuation breaks that re-anchor levels without touching the bias.

You will see two ways to express the same thing. One labels every pivot in sequence (HH, HL, LH, LL) and reads the trend off the labels. The other keeps a single bias variable and updates it on breaks. They agree in the normal case, and the labels are genuinely useful on a chart for a human — but the variable is what your code should branch on. Label sequences get ambiguous the moment price prints a higher high and a lower low in the same leg, and "ambiguous" is not a state an if statement handles gracefully.

Two practical details decide whether this survives contact with a live terminal:

  • The cold start. Before any break has happened there is no bias, and the honest answer is to say so. Treat the first confirmed break as a BOS — a CHoCH against nothing is meaningless — and only classify properly from the second break onward.
  • The restart. An EA that initialises its bias to undefined mid-session behaves differently from the same EA that has been running since Monday, which also means it behaves differently in the Strategy Tester than on your live chart. Rebuild the state in OnInit() by replaying enough history bars through the same swing-and-break logic, rather than starting blank.

Is It a CHoCH or a BOS? Classifying the Break

With a confirmed level and a stored bias, the classification rule collapses to one comparison: does the break direction agree with the bias you were in before the break? Agreement is a BOS. Disagreement is a CHoCH. There is no third case, and no threshold to tune.

Break of Structure vs Change of Character

Break of Structure (BOS)

  • Price closes beyond a confirmed swing level in the SAME direction as the stored bias.
  • The bias variable is unchanged; the EA re-anchors its levels and keeps its current permissions.
  • Reads as continuation — the existing leg simply extended.
  • The first break after a cold start is logged here, because there is no prior bias to contradict.

Confirms the direction the EA is already allowed to trade.

Change of Character (CHoCH)

  • Price closes beyond a confirmed swing level AGAINST the stored bias.
  • The bias variable flips, and every downstream module's permission set flips with it.
  • Reads as the first evidence that the prior leg is finished.
  • Is always followed by a BOS, since the next break in the new direction agrees with the new bias.

Changes what the EA is allowed to do next.

Identical arithmetic on the breaking bar; the stored bias is the only thing that separates them.

What actually breaks in practice is not the rule — it is the bookkeeping around it.

  • Compare against the right level. The candidate for a bullish break is the most recent confirmed swing high, not the highest high on the chart and not the extreme of the current leg. Using the leg extreme means an in-progress rally is constantly "breaking" a level that has not been validated as structure.
  • Re-anchor immediately, or you will emit an event per bar. Once a level is broken it is spent. If the stored high stays at 1.0880 while price trades at 1.0920, every subsequent bar close satisfies the break test and your EA fires the same signal twenty times. Mark it consumed and move the anchor to the newest confirmed pivot.
  • A CHoCH is always followed by a BOS. After the bias flips, the next break in the new direction agrees with the new bias, so it classifies as continuation by definition. That is not a bug — it is the sequence traders describe as "confirmation", falling out of the state machine for free.
  • A failed CHoCH is just a CHoCH back. There is no separate "invalidation" event to model. If the market flips bias and then immediately flips again, you get two CHoCHs in a row, which is itself a useful thing for an EA to notice.

Confirming on Closed Bars, Not Every Tick

Evaluate structure on every tick and you get events that can un-happen. Price pokes through the level at 14:03, your EA classifies a CHoCH, flips bias, and by the bar close price is back inside the range with the level intact. On a live chart that is a bad trade. In the Strategy Tester at anything below every-tick modelling it may not even reproduce, which is the worst possible combination: a live behaviour your test cannot show you.

The fix is a bar gate, and it is about six lines.

mql5 bar_gate.mq5
datetime g_last_bar_time = 0;

bool IsNewBar()
  {
   const datetime t = iTime(_Symbol, _Period, 0);
   if(t == g_last_bar_time) return(false);
   g_last_bar_time = t;
   return(true);
  }

void OnTick()
  {
   ManageOpenPositions();     // per-tick jobs: trailing stops, kill switch

   if(!IsNewBar()) return;    // structure work runs once per closed bar
   UpdateStructure();         // swings, bias, break classification
  }
Per-tick trade management runs before the gate; anything that classifies structure runs after it, on closed bars only.

Note the ordering. Anything genuinely per-tick — trailing a stop, a kill switch — runs before the gate. Structure work runs after it, and reads bar shift 1, the last closed bar. Shift 0 is still forming and has no final close; touching it inside your detection logic is how repainting sneaks back in after you removed it from the pivot check.

Then there is the choice you have to make explicitly, because both answers are defensible:

  • Close beyond the level. Fewer events, later. A wick that overshoots and closes back inside is treated as no break at all — a rejection, not a shift. This is the confirmation candle approach, and it is the stricter of the two.
  • Any trade beyond the level. More events, earlier, and considerably more of them are noise. Every stop-run that reverses within the bar registers as a structure break.

Neither one is free. Requiring the close costs you the part of the move that happens between the touch and the close; accepting the wick costs you precision. Pick one, write it as an input parameter, and keep it constant across every test you run — flipping it between runs makes two backtests uncomparable.

One lag question deserves a direct answer, because it worries people who have just discovered the fractal delay: the right-hand confirmation lag does not delay your break signal. By the time price is breaking a level, that level was confirmed bars ago. What the lag actually costs you is different — a violent V-shaped reversal may break a high that has not yet been validated as a swing, so the EA simply has nothing to compare against and stays silent. That is the trade you accept in exchange for a detector that never revises its own history.

From Detected Shift to a Tradeable Signal

A detector that draws on the chart is a drawing tool. What makes it tradeable is that the rest of the EA can ask it a question and get a deterministic answer, on any tick, without knowing anything about fractals.

mql5 structure_module.mqh
enum ENUM_BIAS  { BIAS_NONE, BIAS_BULL, BIAS_BEAR };
enum ENUM_SHIFT { SHIFT_NONE, SHIFT_BOS, SHIFT_CHOCH };

struct StructureEvent
  {
   ENUM_SHIFT  type;        // BOS or CHoCH
   ENUM_BIAS   direction;   // the bias in force AFTER the event
   double      level;       // the swing level that was broken
   datetime    time;        // close time of the confirming bar
   bool        consumed;    // set by whichever module acts on it
  };

ENUM_BIAS      g_bias      = BIAS_NONE;
double         g_last_high = 0.0;   // most recent CONFIRMED swing high
double         g_last_low  = 0.0;   // most recent CONFIRMED swing low
StructureEvent g_event;

ENUM_SHIFT ClassifyBreak(const bool broke_up)
  {
   if(g_bias == BIAS_NONE)               return(SHIFT_BOS);   // cold start
   if(broke_up  && g_bias == BIAS_BULL)  return(SHIFT_BOS);
   if(!broke_up && g_bias == BIAS_BEAR)  return(SHIFT_BOS);
   return(SHIFT_CHOCH);                                       // break against bias
  }

void UpdateStructure()
  {
   const double close1 = iClose(_Symbol, _Period, 1);   // last CLOSED bar

   const bool broke_up   = (g_last_high > 0.0 && close1 > g_last_high);
   const bool broke_down = (g_last_low  > 0.0 && close1 < g_last_low);

   if(broke_up || broke_down)
     {
      g_event.type      = ClassifyBreak(broke_up);
      g_event.level     = broke_up ? g_last_high : g_last_low;
      g_event.time      = iTime(_Symbol, _Period, 1);
      g_bias            = broke_up ? BIAS_BULL : BIAS_BEAR;
      g_event.direction = g_bias;
      g_event.consumed  = false;
     }

   RefreshConfirmedSwings(g_last_high, g_last_low);   // re-anchor: a broken level is spent
  }

// The only thing the rest of the EA needs to call. Reads state, never writes it.
bool StructureShiftFresh(const ENUM_SHIFT want, const int max_age_bars)
  {
   if(g_event.type != want || g_event.consumed) return(false);
   const int age = iBarShift(_Symbol, _Period, g_event.time);
   return(age >= 0 && age <= max_age_bars);
  }
The EA never asks whether there is a shift on the chart. It asks whether a CHoCH is fresh, which way it points, and which level it broke.

The struct is the whole interface. Everything else in the EA — entry module, filter, risk sizing — consumes it in one of two shapes:

  • As a trigger condition: act when a CHoCH is fresh. Freshness matters more than most builders expect. An event forty bars old is context; it is not a reason to enter now. An age check in bars, exposed as an input, is the difference between a signal and a permanent state.
  • As a trade filter: never trigger on the shift at all, and instead let it veto. Your order-block or sweep module proposes a long; the structure module answers "bias is bearish" and the trade never gets placed. This is the quieter, usually more robust use, and it composes cleanly with every other ICT building block you add later.

Three implementation details make the difference between an interface and a leak:

  1. Fire once. Mark the event consumed when a module acts on it. Without that, an event that stays fresh for ten bars can open ten positions.
  2. Return the broken level, not just the direction. That level is the structural invalidation point — the price at which the read that produced the trade is simply wrong. Whatever your risk module does with it, it needs the number.
  3. Keep the query pure. StructureShiftFresh() should read state, never modify it. The moment a query has side effects, calling it twice in one tick from two modules gives two different answers.

Internal vs External Structure: Why Timeframe Changes the Read

Here is the refinement that most single-indicator implementations skip, and it changes the signal profile more than any other tuning you can do.

Structure exists at every scale simultaneously. Run your detector with a three-bar lookback and you get internal structure — the small pivots inside a leg, sensitive and frequent. Run the same code with a fifteen-bar lookback (or on a higher timeframe) and you get external structure — the swings that define the leg itself. Same logic, two instances, two independent bias variables.

A diagram of one rising price path traced by a bold outer zigzag labelled external structure and a thin inner zigzag labelled internal structure, with an internal change-of-character marked inside the pullback while the external bias stays bullish.
One price path, two lookbacks: an internal change of character inside an intact external uptrend is a pullback, not a reversal.

Once you have both, the classification you already built gains a second dimension:

  • External BOS with internal BOS — the leg is extending and the small structure agrees. Continuation, cleanest case.
  • Internal CHoCH inside an intact external uptrend — the pullback is beginning, or ending. This is not a reversal signal, and treating it as one is the single most common source of false signals in a naive detector. For a continuation trader it is the opposite: it is the setup.
  • Internal CHoCH followed by external CHoCH — the small structure turned first and the large structure has now agreed. This is the high-conviction turn, and it is also the slowest to arrive.

This is multi-timeframe confirmation applied to structure rather than to an indicator, and the cost is exactly what you would expect: far fewer events pass. A detector that requires agreement on both scales will sit idle through entire sessions. Whether that is a feature depends on whether your EA is a scalper or a swing system — but you should choose it deliberately, not discover it after a month of live trading.

A practical note on the two-instance approach: prefer two lookbacks on one timeframe over two timeframes where you can. Two lookbacks share one bar clock, so both instances update on the same closed bar. Pulling higher-timeframe bars introduces a second, slower bar boundary, and the synchronisation bugs that come with it are tedious to find in the tester.

Where a Structure-Shift Detector Alone Falls Short

A single finished glass and metal precision component resting beside three empty machined sockets waiting for parts that have not been built.
The detection layer can be finished and correct and still not be a trading system.

Be clear-eyed about what you have built at this point. A structure module knows one thing: that the market's character changed, in which direction, and at what level. That is a genuinely useful input and it is nowhere near a trading system.

What it does not know, and cannot be made to know:

  • Whether the shift is worth trading. Structure repeats fractally, so a one-minute chart produces dozens of perfectly valid shifts a day, most of them inside the spread of the instrument you are trading. Validity and significance are different properties.
  • Where to enter, and with how much. The detector hands you a level and a direction. Entry logic, position sizing, and a risk-per-trade rule are separate modules, and the stop-loss placement it implies still has to survive the instrument's normal noise.
  • When to stand aside. A CHoCH printed thirty seconds before a scheduled release, or in the dead hours between sessions, is technically identical to one printed in a liquid window. The detector cannot tell them apart. Timing and event filters are their own modules.
  • Whether the logic holds up outside the window you looked at. That is what the Strategy Tester and a period of forward testing are for — and a structure detector is unusually easy to over-tune, because every parameter you add (lookback, close-vs-wick, freshness window) multiplies the number of configurations you can fit to the past.

None of that is a reason to skip the module. It is a reason to stop treating "the EA detected a CHoCH" as a complete thought. Automated trading carries real risk of loss regardless of how clean the detection layer is — the risk warning applies to a well-built detector exactly as much as to a bad one.

Fitting This Into the Full ICT-to-EA Build

Built this way, the structure module ends up being the easiest part of the system to reuse, because it does not know anything about the rest of the EA. It reads bars, produces typed events, and answers questions. Everything else — the module that finds a zone to enter against, the one that decides which session windows are allowed, the one that recognises the daily accumulation-manipulation-distribution cycle and wants a structure shift to confirm the turn — subscribes to it.

That is also why it is worth building first. Almost every other ICT concept you will automate eventually asks the same question — "which way is structure pointing right now?" — and if the answer lives in one module with one bias variable, they all get a consistent reply. If three modules each work it out for themselves, they will disagree, and you will spend an evening finding out why your EA went long and short on the same bar.

The remaining pieces — turning this into an EA that actually places orders, with entries, risk, permissions and deployment — belong to the full ICT-to-EA automation workflow, which orchestrates modules like this one rather than re-implementing them.

You came in with “a shift you can read in half a second and an EA that only sees an array of doubles” and you leave with a typed event carrying a direction, a level and a timestamp any module can query.

Structure is the module worth building first

A confirmed pivot, a stored bias and a closed-bar break test are the whole detector — and once they live in one module with one bias variable, every other ICT concept you automate gets a consistent answer to the same question. What is missing now is not detection. It is the timing, the event handling and the safety layer that decide whether a detected shift is worth acting on at all.

Continue your research Trading session filter The timing gate a structure signal usually needs next News filter Why a valid shift printed into a release is still a bad trade Kill switch The per-tick safety layer that runs before the bar gate

FAQ

How many bars should the swing lookback be?

There is no universal number, and any source that gives you one is guessing on your behalf. The lookback is a scale selector: a small right-hand window (two to three bars) tracks momentum inside a leg, a large one (ten to fifteen or more) tracks the legs themselves. Choose it from the job you want the module to do, then confirm on your instrument and timeframe in the tester. If you find yourself tuning it to maximise a backtest result, you have stopped selecting a scale and started curve-fitting.

Does a wick through the level count as a break?

Only if you decide it does. Requiring a bar to close beyond the level filters out stop-runs that immediately reverse, at the cost of entering later. Accepting any trade beyond the level catches the move earlier and accepts considerably more noise. Both are legitimate; what is not legitimate is changing the answer between test runs, because the two variants produce different trade sets and their results cannot be compared.

Can an EA detect a structure shift without the confirmation delay?

Not honestly. A pivot is defined partly by the bars that come after it, so confirming one before those bars exist means guessing — and a guess that gets revised is precisely what repainting is. The delay is not a limitation of MQL5 or of your code; it is a property of the definition. What you can do is reduce the right-hand lookback so confirmation arrives sooner, accepting more marginal pivots in exchange.

What happens to the bias state when the EA restarts?

Whatever you tell it to. A freshly initialised EA has no bias, so it will classify its first break as a BOS regardless of what the chart shows — which means a restart can silently change your EA's behaviour for the next several hours. Rebuild the state in OnInit() by replaying a fixed number of historical bars through the same swing and break logic. Do it once, and live behaviour matches tester behaviour.

Is a CHoCH on its own enough to enter a trade?

As a complete entry rule, no — it gives you a direction and an invalidation level, not a price to enter at, a size, or a reason to believe this particular shift is significant. Most working builds use it as a bias filter or as an arming condition, then hand the actual entry to a module with a tighter definition of location. Treating the shift as the whole trade is what produces an EA that trades constantly and holds nothing worth holding.

How is this different from a moving-average trend filter?

A moving average answers "where has price been on average" — it is continuous, it lags smoothly, and it has no concept of a specific level being taken out. A structure module answers "which prior swing was broken, when, and did that agree with the previous bias" — it is discrete, event-driven, and every state change points at a specific price you can measure risk against. The two can coexist, but they are not substitutes: only one of them hands your risk module a level.

Sources & Further Reading

Want to go deeper? These independent, authoritative sources shaped this guide — each one is worth reading in full:

Signalbots Cross-Market Desk

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.

More from this desk

Discussions 0

Leave a comment