MQL5 Smart Money Concepts: Structure, OB and FVG
Most MQL5 Smart Money Concepts indicators fail for one of three reasons: they detect a pattern before the bar is closed, they treat a broken swing as a new trend without checking the structure that created it, or they draw objects without a bounded object model. This guide builds the complete engine in the correct order.
The goal is not to reproduce one chart visually. The goal is a deterministic engine: the same closed bars must always produce the same swings, BOS, CHoCH, order blocks, gaps and alerts. Once that is true, the indicator can be tested, exposed to an Expert Advisor and made non-repainting.
1. The data model comes before the pattern
A market-structure engine needs four separate states: the price series, the confirmed pivots, the active structure leg and the drawing objects. Do not mix them. A pivot is a fact about a closed bar; a BOS is a relationship between a close and a pivot; an order block is a stored range derived from a break.
struct SwingPoint
{
datetime time;
double price;
int bar; // oldest-to-newest index
bool isHigh;
bool crossed;
};
struct StructureState
{
SwingPoint lastHigh, prevHigh;
SwingPoint lastLow, prevLow;
int trend; // 1 up, -1 down, 0 mixed
int lastEventBar;
bool valid;
};
Store the bar index as well as the time. Time is needed for drawing; the index is needed for array comparisons. Mixing them is the source of many off-by-one bugs.
2. Confirmed swings: use the leg-change rule
The reference does not use a two-sided fractal. It compares an older bar with the extreme of the N newer bars and flips the active leg when that older bar is a new extreme. The pivot becomes known only after those N bars 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, SwingPoint &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 from index size toward the newest bar. A new high creates a bearish leg; a new low creates a bullish leg. The first leg only initialises state; a pivot is emitted only when the leg changes. Equal highs are excluded because both comparisons are strict.
3. BOS and CHoCH are not the same event
BOS means the current structure continues in the same direction. CHoCH means the structure changes direction. The difference is the previous trend state, not the shape of the breakout bar.
- Bullish BOS: previous trend was up and a closed bar closes above the last confirmed swing high.
- Bullish CHoCH: previous trend was down and a closed bar closes above the last confirmed swing high.
- Bearish BOS: previous trend was up? No — previous trend was down and a closed bar closes below the last swing low.
- Bearish CHoCH: previous trend was up and a closed bar closes below the last swing low.
The clean implementation is one function with an event type:
void OnStructureBreak(StructureState &s, int direction, int bar)
{
if(s.lastEventBar == bar) return;
bool wasBullish = (s.trend == 1);
bool wasBearish = (s.trend == -1);
bool isChoch = (direction == 1 && wasBearish) ||
(direction == -1 && wasBullish);
string label = isChoch ? "CHoCH" : "BOS";
s.lastEventBar = bar;
s.trend = direction;
// draw the event and set the broken pivot's crossed flag
}
Do not set the trend from the breakout candle’s direction. The trend changes only when a confirmed swing is broken. In a downtrend, a strong bullish candle that closes below the last swing high is not a CHoCH.
4. Order blocks: store a range, not a rectangle
An order block is a stored price range derived from the last opposite candle before a structure break. The engine should store the high, low, time and bias. The rectangle is only a rendering of that state.
struct OrderBlock
{
datetime time;
double high;
double low;
int bias; // 1 bullish, -1 bearish
};
void StoreOrderBlock(OrderBlock &list[], datetime t, double hi,
double lo, int bias, int maxCount)
{
OrderBlock ob = {t, hi, lo, bias};
ArrayResize(list, ArraySize(list) + 1);
for(int i = ArraySize(list) - 1; i > 0; i--) list[i] = list[i - 1];
list[0] = ob;
if(ArraySize(list) > maxCount)
ArrayResize(list, maxCount);
}
The order-block filter changes the stored range, not the entry logic. With the canonical ATR filter, a high-volatility bar reverses the parsed high and low. ATR is Wilder-smoothed true range with period 200, seeded by the first 200 true ranges; the range test is inclusive.
void FilteredRange(double high, double low, double atr,
double &parsedHigh, double &parsedLow)
{
bool highVolatility = (atr > 0.0) && ((high - low) >= 2.0 * atr);
parsedHigh = highVolatility ? low : high;
parsedLow = highVolatility ? high : low;
}
With the cumulative mean range option, replace atr with the expanding mean of true range since the first computed bar. Whichever you use, document it and test the first valid bar and the transition bar.
Mitigation is also a state operation: remove or mark the block when the chosen mitigation source crosses the correct side. The reference default is high/low mitigation; close-based mitigation is a separate mode and produces different histories.
5. Fair value gaps: three candles and an auto threshold
A bullish fair value gap exists when the current candle’s low is above the high from two candles earlier and the middle candle closes above that high. A bearish gap is the inverse. The reference also requires the middle candle body to beat an auto threshold, and it checks only on a new higher-timeframe bar.
double CumulativeBodyThreshold(const MqlRates &r[], int i)
{
if(i <= 0) return 0.0;
double sum = 0.0;
for(int k = 0; k < i; k++)
if(r[k].open != 0.0)
sum += MathAbs((r[k].close - r[k].open) / r[k].open);
return (sum / i) * 2.0;
}
bool IsBullishFVG(const MqlRates &r[], int i,
bool autoThreshold, double fixedThreshold)
{
if(i < 2) return false;
double body = (r[i - 1].open != 0.0) ?
(r[i - 1].close - r[i - 1].open) / r[i - 1].open : 0.0;
double threshold = autoThreshold ? CumulativeBodyThreshold(r, i)
: fixedThreshold;
if(body <= threshold) return false;
return (r[i].low > r[i - 2].high &&
r[i - 1].close > r[i - 2].high);
}
bool IsBearishFVG(const MqlRates &r[], int i,
bool autoThreshold, double fixedThreshold)
{
if(i < 2) return false;
double body = (r[i - 1].open != 0.0) ?
(r[i - 1].close - r[i - 1].open) / r[i - 1].open : 0.0;
double threshold = autoThreshold ? CumulativeBodyThreshold(r, i)
: fixedThreshold;
if(-body <= threshold) return false;
return (r[i].high < r[i - 2].low &&
r[i - 1].close < r[i - 2].low);
}
The array is oldest-to-newest, so i is the current closed bar, i - 1 is the middle candle and i - 2 is the older candle. The gap boundaries are current low → older high for a bullish gap, and older low → current high for a bearish gap. The body comparison is strict: equality fails.
For a higher-timeframe FVG, do not use the current chart’s bars as if they were the HTF bars. Load the HTF series, aggregate the completed HTF candle, and use the HTF open/close/high/low in the same three-candle test. Evaluate the pattern only when the HTF timeframe changes. On historical data this is a completed-bar operation; in real time the forming HTF candle must use only the portion already known to the chart.
6. Premium, discount and MTF levels
A dealing range needs a swing high and a swing low. In the reference, premium occupies the top 5%, discount the bottom 5%, and equilibrium is the central 5% band around the midpoint. Store the four boundaries, not only a midpoint.
double premiumTop = swingHigh;
double premiumBottom = 0.95 * swingHigh + 0.05 * swingLow;
double equilibriumTop = 0.525 * swingHigh + 0.475 * swingLow;
double equilibriumBottom = 0.525 * swingLow + 0.475 * swingHigh;
double discountTop = 0.95 * swingLow + 0.05 * swingHigh;
double discountBottom = swingLow;
The exact percentages are configuration, not universal truth; store the chosen bands and apply them consistently. The reference defaults are 0.05/0.95 for premium/discount and 0.475/0.525 for equilibrium.
Daily, weekly and monthly levels use the previous completed period, not the current one. If the chart timeframe is D1 or higher, the level engine must avoid using the same candle as both the source and the display. Load the higher-timeframe series and verify that the required period is ready before drawing.
7. Object limits and transparent boxes
Chart objects are finite. A PIL-style limit of 500 labels, 500 lines and 500 boxes is common in charting ports. Keep separate collections, delete the oldest non-persistent object first, and never scan every chart object on every tick.
MT5 rectangles have no alpha channel. To produce a transparent block, render it through a canvas with alpha blending or use a semi-transparent colour approximation. The important part is that the colour conversion is deterministic and documented.
- Keep persistent objects in a separate list.
- Delete oldest first when the collection exceeds the cap.
- Cache object hashes; do not redraw identical objects.
- Rebuild only the forming-bar preview, not the committed history.
8. Alerts and EA buffers
An alert must fire once per event, not once per tick. The reference clears the current-bar alert array at the start of every bar, sets the event flags during that bar, and emits once after the bar is processed. Store the bar index of the last alert for each event type. If alertOnBarClose is enabled, evaluate only the last closed bar.
bool currentAlerts[EVENT_COUNT];
int lastAlertBar[EVENT_COUNT];
for(int e = 0; e < EVENT_COUNT; e++) currentAlerts[e] = false;
// ... structure and pattern updates set currentAlerts[e] = true ...
if(currentAlerts[EVENT_BULLISH_CHOCH] &&
lastAlertBar[EVENT_BULLISH_CHOCH] != bar)
{
lastAlertBar[EVENT_BULLISH_CHOCH] = bar;
Alert(_Symbol, " Bullish CHoCH");
}
For an EA, expose a small, explicit buffer contract: trend, last swing high, last swing low, last BOS, last CHoCH. Do not make the EA read chart objects. A buffer contract is testable and survives object cleanup.
9. Tests and edge cases
At minimum, test these cases before calling an SMC implementation complete:
| Test | Expected behaviour |
|---|---|
| First bar | No pivot, no BOS, no FVG before enough bars exist. |
| Leg unchanged | No new pivot and no structure event. |
| Equal FVG body threshold | Rejected because the comparison is strict. |
| Later bars after an FVG | Must not change the auto threshold or the earlier result. |
| Forming bar | Computed on a copy and discarded at the next tick. |
| Missing HTF bars | No HTF object until the series is ready. |
| Duplicate timestamps | Reject or de-duplicate before structure detection. |
| Object overflow | Oldest non-persistent object removed first. |
| Weekend gap | No synthetic bar; HTF levels use completed periods. |
| Alert repeat | One alert per bar/event, not per tick. |
Reproduce these with fixed CSV fixtures, not with a live chart. The same input must produce the same output. If the output changes, the state or the bar boundary is wrong.
10. FAQ
Should I calculate on every tick or only on a new bar?
Calculate structure on a new bar. A forming-bar preview can run on ticks if it uses a throwaway state and is not committed.
Is a BOS the same as a breakout?
No. A BOS is a structural event relative to a confirmed swing. A breakout is a price crossing; it needs context.
Can I use this code for an EA?
Yes, but expose indicator buffers or a separate signal engine. Do not make the EA depend on chart object names.
What is the most common repainting bug?
Confirming a pivot before the required bars on the right have closed, or committing the forming-bar state.
Market Structure clusterSMC Lab toolsSMC Code Map
Code is educational. Test in MetaEditor and Strategy Tester before using it with real capital.