How to Code BOS and CHoCH in MQL5
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
| Fixture | Expected result |
|---|---|
| First pivot not yet confirmed | No pivot, no BOS/CHoCH |
| Leg unchanged | No new pivot |
| Wick above the high, close below | No bullish break under close confirmation |
| Close exactly at the level | No break under the strict > rule |
| Two breaks of the same pivot | Only the first event is valid |
| Close back below the previous level | No crossover, no event |
| Trend down + close above high | CHoCH, not BOS |
| Trend up + close above high | BOS, not CHoCH |
| Forming bar | Preview 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.
Next: Internal vs Swing StructurePillar guideSMC Code Map
Educational code. Test on fixed fixtures and closed bars before using it in an EA.