Market structure

How to Code BOS and CHoCH in MQL5

AlgoSpecial8 November 202616 min read

BOS and CHoCH are often drawn as if they are just two labels on a line. The implementation is more specific: a confirmed pivot is broken by a closed bar, and the previous trend determines whether that break is a continuation or a character change.

1. Define state before writing the loop

The engine needs the last two confirmed swing highs, the last two confirmed swing lows, a trend value and the last event bar. If you keep only one high and one low, you cannot determine HH/HL/LH/LL and you will misclassify the break.

struct Pivot
{
   datetime time;
   double   price;
   int      bar;
   bool     isHigh;
   bool     crossed;
};

struct Structure
{
   Pivot lastHigh, prevHigh;
   Pivot lastLow,  prevLow;
   int   trend;                 // 1 = bullish, -1 = bearish, 0 = mixed
   int   lastEventBar;
   double previousHighLevel;
   double previousLowLevel;
};

Keep the bar index even when you also store the time. The bar index is used for ordering and for the “one event per bar” rule. The time is used for drawing.

2. Confirm the pivot from the leg change

The reference engine does not scan for a two-sided fractal. It updates a leg from the break of the N-bar extreme. The pivot is the bar that created the leg change, and it becomes known only after the N bars to its right have closed. Canonical defaults are 50 for swing, 5 for internal and 3 for equal highs/lows.

#define LEG_NONE 0
#define LEG_BULL 1
#define LEG_BEAR -1

bool UpdateLegPivot(const MqlRates &r[], int current, int size,
                    int &leg, int &previousLeg, Pivot &out)
{
   if(current < size) return false;

   int pivot = current - size;
   double highest = r[current].high;
   double lowest  = r[current].low;

   for(int k = pivot + 1; k < current; k++)
   {
      if(r[k].high > highest) highest = r[k].high;
      if(r[k].low  < lowest)  lowest  = r[k].low;
   }

   int next = leg;
   if(r[pivot].high > highest)    next = LEG_BEAR;
   else if(r[pivot].low < lowest) next = LEG_BULL;

   if(next == leg) return false;

   int previous = previousLeg;
   leg = next;
   previousLeg = next;
   if(previous == LEG_NONE) return false;

   out.time    = r[pivot].time;
   out.price   = (next == LEG_BULL) ? r[pivot].low : r[pivot].high;
   out.bar     = pivot;
   out.isHigh  = (next == LEG_BEAR);
   out.crossed = false;
   return true;
}

The array is oldest-to-newest, as returned by CopyRates. Walk it from the first possible pivot at index size toward the newest bar. Initialise both leg and previousLeg to LEG_NONE. A new high is a bearish leg; a new low is a bullish leg. The first leg only initialises state; a pivot is emitted only when the leg changes.

Use the leg-change pivot, not the highest high in a window. A highest-high scan can select an old extreme that has already been broken, which changes the meaning of the next BOS.

3. A break is a close beyond the last pivot

Define exactly which price closes beyond the level. Using the live bid/ask in a “closed-bar” indicator introduces intrabar state. For a non-repainting structure event, use the last closed candle:

bool BullishBreak(double closeNow, double closePrev, double level,
                  double &previousLevel, Pivot &swingHigh)
{
   bool cross = previousLevel > 0.0 &&
                closeNow > level && closePrev <= previousLevel;
   previousLevel = level;
   return cross && !swingHigh.crossed;
}

bool BearishBreak(double closeNow, double closePrev, double level,
                  double &previousLevel, Pivot &swingLow)
{
   bool cross = previousLevel > 0.0 &&
                closeNow < level && closePrev >= previousLevel;
   previousLevel = level;
   return cross && !swingLow.crossed;
}

The second comparison uses the previous pivot level, not the current level. That is the definition of ta.crossover and ta.crossunder: today's close crosses the level that was active on the previous bar. Update the stored previous level on every bar, even when no event fires.

If you add a buffer, apply it consistently to BOS, CHoCH and alerts. A 2-point buffer on one event and zero on another makes the historical labels inconsistent.

Do not reuse the same pivot for multiple events. Set the crossed flag on the pivot after the event. A new event is allowed only when a newer pivot becomes the active level or the trend changes through a different swing.

4. BOS and CHoCH come from the previous trend

Use one event function with the direction and the previous trend. This prevents two different break functions from drifting apart.

int StructureEvent(Structure &s, int direction, int bar)
{
   bool wasBull = (s.trend == 1);
   bool wasBear = (s.trend == -1);

   bool choch = (direction == 1 && wasBear) ||
                (direction == -1 && wasBull);

   if(s.lastEventBar == bar) return 0; // once per bar
   s.lastEventBar = bar;
    s.trend = direction;
    return choch ? 2 : 1; // 2 = CHoCH, 1 = BOS
}

Route a confirmed break through the event function and lock the pivot:

if(BullishBreak(closeNow, closePrev, s.lastHigh.price,
                s.previousHighLevel, s.lastHigh))
{
   s.lastHigh.crossed = true;
   int event = StructureEvent(s, 1, bar); // 1 BOS, 2 CHoCH
}

Examples:

  • Trend down; close above the last swing high → bullish CHoCH.
  • Trend up; close above the last swing high → bullish BOS.
  • Trend down; close below the last swing low → bearish BOS.
  • Trend up; close below the last swing low → bearish CHoCH.

A common bug is to set the trend from the breakout candle itself. A strong bullish candle below the last swing high does not change structure.

5. Draw once and lock the event

Create a line from the pivot time to the event bar and a label at the midpoint. Store the object name using the pivot time and event type so a recalculation updates the same object instead of creating duplicates.

string name = StringFormat("SMC_BOS_%I64d_%d",
                            (long)swing.time, direction);
if(ObjectFind(0, name) < 0)
   ObjectCreate(0, name, OBJ_TREND, 0,
                swing.time, swing.price,
                iTime(_Symbol, _Period, 0), swing.price);

In real-time mode, draw the forming preview separately. When the bar closes, commit the event once and never move the historical line.

6. Test cases that expose broken logic

FixtureExpected result
First pivot not yet confirmedNo pivot, no BOS/CHoCH
Leg unchangedNo new pivot
Wick above the high, close belowNo bullish break under close confirmation
Close exactly at the levelNo break under the strict > rule
Two breaks of the same pivotOnly the first event is valid
Close back below the previous levelNo crossover, no event
Trend down + close above highCHoCH, not BOS
Trend up + close above highBOS, not CHoCH
Forming barPreview only; no committed object

Use the SMC Code Map to see where this module sits, and the Object Parity Checker when comparing two implementations.

7. FAQ

Should BOS use body close or wick?

Choose one rule and use it consistently. Closed-body confirmation is the usual non-repainting default; wick-based structure is a different definition.

How many bars should confirm a swing?

The reference uses leg lengths of 50 for swing, 5 for internal and 3 for equal highs/lows. A pivot becomes known after that many bars on its right have closed; do not substitute a two-sided fractal without changing the definition.

Can I use the same pivots for internal and swing structure?

No. They use different lookbacks and must keep independent state. See the next article.

Educational code. Test on fixed fixtures and closed bars before using it in an EA.

🚀 Invite friends — Earn $5 You both get $5 credit on Go Ad · opencode.ai AI-Powered Coding Agent — Try Free Build apps, fix bugs & ship faster with opencode. Get $5 free credit when you join. × Ad · quo.com QuoPhone — $20 Visa Gift Card Free Sign up to Quo, subscribe 3 months, get a $20 Visa gift card. Atif's referral gift for you. ×