FreeMT5MQL5 source code

Market Structure from Pullback Swings — BOS, CHoCH, IDM

Market Structure from Pullback Swings — BOS, CHoCH, IDM

My previous CodeBase entry, SMC Pullback Indicator, answered one question: which highs and lows actually matter? It stopped there deliberately, and the description said as much — no HH/HL labels, no BoS, no CHoCH, no IDM. Those are separate steps that need a reliable set of swings first.

This indicator is that next step. It marks market structure the way Smart Money Concepts (SMC) describes it, and it takes the same pullback detector — the class is included here unchanged, so the file stands alone with no dependency to install — and builds market structure on top of it: a trend, one live high, one live low, and the four events that move them. BoS, CHoCH, IDM and the liquidity sweep marked x.

What you see on the chart

Element Appearance Meaning
Pullback zig-zag thin pale line Every swing found by the pullback rule. This is the raw material, deliberately faint — it is context, not signal.
Structure zig-zag thick gold line Only the highs and lows that the structure actually uses. A small subset of the pale one.
Structure markers arrows above / below The structure high and low themselves.
BoS dashed, green Break of Structure — the level was taken with the trend.
CHoCH dashed, red Change of Character — the level was taken against the trend. The trend flips here.
x solid, purple Liquidity sweep — the level was taken by a wick only, and price then went the other way.
IDM dotted, blue Inducement — the pullback level whose break confirms a new structure extreme.

Each event line is horizontal and spans from the bar that set the level to the bar that took it, so the length of the line tells you how long that level survived.

Layer one: the pullback swings

Briefly, because it is documented in full in the previous entry: a high becomes a swing only once price trades back through the low of the candle that made it. Nothing is decided by shape — a swing is an event, not a geometry, and that is what lets it carry a timestamp.

Two properties of that layer matter downstream:

  • The output strictly alternates high, low, high, low. Never two highs in a row.
  • A swing is confirmed later than the candle it sits on. The structure layer only ever sees a pullback once it is final, which is why the structure can never be built on a swing that is still provisional.

Both layers are driven one closed bar at a time, pullback first, structure second — so when the structure evaluates a bar, it already knows about any swing confirmed on that same bar.

Layer two: the structure

The structure holds exactly three things:

State Meaning
Trend None, Up or Down
Structure high the one high that currently matters — the level whose break is bullish
Structure low the one low that currently matters — the level whose break is bearish

It is seeded from the first pullback high and the first pullback low, with the trend set to None. From that moment on, the pullback layer never writes into the structure again — it only supplies the reference levels described further down.

The break rule: close takes, wick sweeps

This single distinction produces three of the four event types:

A level is broken only when a candle closes beyond it. A candle whose wick alone goes beyond it has swept it.

So for every closed candle, with the trend and the two levels as they stand:

Trend What the candle did Event
None closed above the high / below the low BoS — the trend becomes Up / Down
Up closed above the structure high BoS — continuation, trend stays Up
Up closed below the structure low CHoCH — trend flips to Down
Down closed below the structure low BoS — continuation, trend stays Down
Down closed above the structure high CHoCH — trend flips to Up
any wick beyond a level, close back inside sweep candidate — remembered, nothing drawn yet
any stayed inside both levels nothing

So BoS and CHoCH are the same event — a close through a level. The only thing that decides the label is whether the break agrees with the trend that is already in force. With no trend yet, the first break is a BoS by definition, because there is no character to change.

The sweep: when a wick becomes an x

A wick through a level is not an event on its own. It is recorded and left pending, because at that moment it is ambiguous — price may simply close through the level on the next candle, in which case it was an ordinary break in the making, not a sweep.

It becomes an x only when the next break happens on the opposite side. The classic sequence, in an uptrend:

  1. Price runs above the structure high with a wick, but closes back below it. Recorded as a sweep candidate. Nothing on the chart.
  2. Price turns and closes below the structure low.
  3. That resolves the ambiguity. The move up was a liquidity grab, so the indicator moves the structure high onto the swept candle — the marker and both zig-zag legs follow it — draws the solid x line, and only then records the CHoCH.

Moving the high is the point of the whole exercise. The swing that the reversal started from is the sweep candle, not the older high that it ran through, and every level you derive afterwards should be measured from there.

The x line spans from the bar that set the level to the bar whose wick took it, drawn at the old level's price — that is the liquidity that was resting there. The bearish case is the exact mirror: a wick below the structure low, then a close above the structure high.

Three rules keep this honest:

  • If several candles wick through the level, the furthest one wins.
  • If price later closes through the level in the same direction, that is an ordinary break and the sweep candidate is discarded.
  • A sweep candidate is discarded as soon as any new structure point is published. Once the structure has moved on, an older wick is stale and must not retroactively relocate a point.

IDM: confirming the new extreme

A break answers one question and opens another. When price closes above the structure high in an uptrend, you know a BoS happened — but you do not yet know where the new high is. Price is still moving. Calling the breaking candle the new high would be wrong the moment the next candle goes higher.

The inducement is the answer. At the moment of the BoS:

  1. The latest pullback low becomes the reference — the IDM.
  2. Price continues up; the indicator tracks the highest high it reaches, but publishes nothing.
  3. Each time a new pullback low forms above the current IDM, the IDM ratchets up to it. The reference is always the highest pullback low seen since the break.
  4. When price finally comes back and takes the IDM, the new structure high is confirmed: it is the highest high between the breaking candle and the candle that took the IDM.

At that moment the dotted IDM line is drawn — from the pullback that set it, across to the candle that ran through it — and the structure high marker appears. The two always come together, which makes the IDM line a useful audit of the logic: every structure high should have an IDM line ending on the bar where its marker appears.

The bearish case is the mirror: after a bearish break the IDM is the lowest pullback high, and taking it confirms the new structure low as the lowest low of the move.

While an extreme is pending, its own side of the structure holds no level, so only the opposite side can be broken. This falls out of the design rather than needing a rule: the IDM always sits at or above the structure low, so price has to take the IDM — confirming the high — before it can reach the low.

Keeping the chain a zig-zag

Structure points must alternate, exactly like the pullback layer. Two highs in a row is a broken chain, not a structure.

A bullish break is on its way to publishing a high. If the newest point in the chain is already a high, a low is missing between them, so the indicator takes the lowest low between that high and the breaking candle and publishes it first. If the newest point is already a low, the chain alternates and nothing is inserted.

That one rule covers every case: your BoS in an established trend, your CHoCH, and the very first break after the seed. The bearish case is the mirror — the highest high of the swept range.

Edge cases, stated openly

Situation How it is resolved
One candle both takes the IDM and closes through the opposite level Read in that order: the extreme is confirmed first, then the break is recorded. This is precisely the sweep-then-CHoCH case, and the order matters.
A pending extreme is never confirmed, because price breaks the other way first The tracked extreme is published where it stands, then the break is processed. Nothing is lost and the chain still alternates.
The breaking candle itself returns to the IDM Confirmation can never happen on the breaking candle. The earliest it can occur is the following bar.
No pullback exists yet to serve as the IDM The current structure level on that side is used instead. This only happens on the oldest bars of the chart.

On repainting — read this before using it

The pullback layer does not repaint, and nothing has changed about it.

The structure layer does, and it is not a defect — it is what the method is. Two things move:

  • A pending extreme moves as price extends, right up until the IDM is taken. It is published live so you can see the structure forming, instead of waiting in the dark.
  • A sweep relocates a point backwards, by design. That is the whole purpose of the x.

Once an extreme is confirmed by its IDM and no sweep is pending against it, it is settled. Events — BoS, CHoCH, x, IDM — are append-only and never move.

If you build on this, treat a structure point as provisional until its IDM line exists, and treat the events as the stable record. Only closed candles are evaluated; the forming candle is ignored entirely.

Inputs

Pullback layer

Input Default What it does
Start by looking for Looking for a swing high Which side the very first leg searches for. Only affects the oldest bars on the chart.
Pullback break confirmed by Wick The sensitivity control for the whole indicator. Wick is the literal Trading Hub rule. Close gives fewer, larger pullbacks — and therefore fewer, larger structure moves. Switch to it on M1–M15 or on noisy symbols such as gold and index CFDs.

Market structure

Input Default What it does
Extreme confirmed when reference is Wick How the IDM must be taken. Wick means touched; Close requires a close through it, which confirms extremes later but filters out shallow pokes at the inducement.
Detect liquidity sweeps true Turn off for plain structure: levels are then taken by close only, no x, and points are never relocated. Useful for comparing the two readings side by side.

There is no period, depth or sensitivity setting anywhere — the structure has no parameters of its own. If you want larger structure, raise the timeframe or switch the pullback rule to Close.

Display

Group What it controls
Pullback zig-zag The faint underlying swings — colour, style, width, or off entirely.
Structure zig-zag The gold structure line.
Structure markers Arrows on the structure high / low: Wingdings codes and pixel distance from the bar. The buffers stay filled when the arrows are off, so iCustom still sees everything.
Break lines A separate colour for BoS, CHoCH, x and IDM, plus line width and the captions. Turn the captions off on dense charts and keep the colours as the key.

Alerts

Popup and push notification on a structure event. They fire only on live updates, never while history loads, and only once per event.

Using it from an EA

Five buffers. The empty value is 0.0.

Buffer Contents
0 price of a structure high
1 price of a structure low
2 trend on every bar: 1 up, -1 down, 0 none
3 price of a confirmed pullback high
4 price of a confirmed pullback low
MQL5
int h = iCustom(_Symbol, _Period, "SMC_MarketStructure");
double trend[];
CopyBuffer(h, 2, 0, 1, trend);   // 1 = up, -1 = down, 0 = none

Buffer 2 is filled on every bar including the forming one, so reading it at shift 0 always returns the current trend. The other four are sparse by nature.

Both engines are self-contained classes with no buffer or chart dependencies — CPullbackDetector takes price arrays and reports confirmed swings, CMarketStructure takes bars plus those swings and reports a point chain and an event list. Both can be copied straight into an EA if you would rather not go through iCustom, which also gives you the events — they are not exposed through buffers.

What it deliberately does not do

It stops at structure. No order blocks, no fair value gaps, no premium/discount zones, no entries and no HH/HL/LH/LL labelling. Those all need a dependable trend and a dependable pair of levels first — which is what this provides, with every decision it makes visible on the chart as a line you can check.

Source code

Copy the code into MetaEditor, save it as SMC_MarketStructure.mq5 and compile (F7).

SMC_MarketStructure.mq5
//+-------------------------------------------------------------------+
//|                                          SMC_MarketStructure.mq5  |
//|                                Copyright 2026, Sandro Begashvili  |
//|                   https://www.mql5.com/en/users/sandrobegashvil   |
//|                                          https://t.me/rhinoquant  |
//|                                                                   |
//| Market structure - trend, BoS, CHoCH and liquidity sweeps - built |
//| on top of the pullback swing detector of SMC_Pullback.mq5.        |
//|                                                                   |
//| Two layers run one after another on every closed bar:             |
//|                                                                   |
//| 1. CPullbackDetector - unchanged from SMC_Pullback. It turns raw  |
//|    candles into an alternating chain of pullback swings. It is    |
//|    included here verbatim so this file stands alone; it is        |
//|    documented on its own at                                       |
//|    https://www.mql5.com/en/code/76451                             |
//|                                                                   |
//| 2. CMarketStructure - keeps ONE high and ONE low as the current   |
//|    structure. A close beyond one of them is a break: with the     |
//|    trend it is a BoS, against it a CHoCH. The opposite side of    |
//|    the structure is then taken from the range the break swept,    |
//|    and the new extreme on the break side is confirmed later, when |
//|    price returns to a reference pullback level.                   |
//|                                                                   |
//| Only a close counts as a break. A candle whose wick alone goes    |
//| past the level is remembered as a liquidity sweep: if the next    |
//| break happens on the OTHER side, the structure point is first     |
//| moved onto the swept candle, so the sweep - not the old level -   |
//| becomes the origin of the move.                                   |
//|                                                                   |
//| The structure repaints by design: the newest point moves while it |
//| is still being confirmed, and a sweep relocates it backwards. The |
//| pullback layer underneath never repaints.                         |
//+-------------------------------------------------------------------+
#property copyright "Copyright 2026, Sandro Begashvili"
#property link      "https://www.mql5.com/en/users/sandrobegashvil"
#property version   "1.00"
#property description "Market structure on pullback swings: trend, BoS, CHoCH, IDM and liquidity sweeps."
#property description "Smart Money Concepts. Evaluated on closed bars."
#property description "Telegram: https://t.me/rhinoquant"

#property indicator_chart_window
#property indicator_buffers 5
#property indicator_plots   5

#property indicator_label1  "MS High"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrOrangeRed
#property indicator_width1  2

#property indicator_label2  "MS Low"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrDodgerBlue
#property indicator_width2  2

#property indicator_label3  "MS Trend"
#property indicator_type3   DRAW_NONE

#property indicator_label4  "Pullback High"
#property indicator_type4   DRAW_NONE

#property indicator_label5  "Pullback Low"
#property indicator_type5   DRAW_NONE

//--- side the pullback detector is currently searching for
enum ENUM_LOOKING_FOR
  {
   LOOK_HIGH,       // Looking for a swing high
   LOOK_LOW         // Looking for a swing low
  };

//--- what counts as breaking the pullback reference level
enum ENUM_BREAK_MODE
  {
   BREAK_BY_WICK,   // Wick - the high / low of the candle
   BREAK_BY_CLOSE   // Close - the close of the candle
  };

//--- what counts as price returning to the confirmation reference
enum ENUM_TOUCH_MODE
  {
   TOUCH_BY_WICK,   // Wick - the level is touched
   TOUCH_BY_CLOSE   // Close - the level is closed through
  };

//--- market structure trend, also published through the trend buffer
enum ENUM_MS_TREND
  {
   MS_TREND_DOWN = -1,  // Down
   MS_TREND_NONE = 0,   // No trend yet
   MS_TREND_UP   = 1    // Up
  };

input group             "Pullback layer"
input ENUM_LOOKING_FOR  InpStartSide    = LOOK_HIGH;         // Start by looking for
input ENUM_BREAK_MODE   InpBreakMode    = BREAK_BY_WICK;     // Pullback break confirmed by

input group             "Market structure"
input ENUM_TOUCH_MODE   InpTouchMode    = TOUCH_BY_WICK;     // Extreme confirmed when reference is
input bool              InpUseSweeps    = true;              // Detect liquidity sweeps

input group             "Display - pullback zigzag"
input bool              InpShowPbLegs   = true;              // Draw the pullback zigzag
input color             InpPbLegColor   = clrGainsboro;      // Pullback zigzag colour
input ENUM_LINE_STYLE   InpPbLegStyle   = STYLE_SOLID;       // Pullback zigzag style
input int               InpPbLegWidth   = 1;                 // Pullback zigzag width

input group             "Display - structure zigzag"
input bool              InpShowMsLegs   = true;              // Draw the structure zigzag
input color             InpMsLegColor   = clrGoldenrod;      // Structure zigzag colour
input ENUM_LINE_STYLE   InpMsLegStyle   = STYLE_SOLID;       // Structure zigzag style
input int               InpMsLegWidth   = 2;                 // Structure zigzag width

input group             "Display - structure markers"
input bool              InpShowMarkers  = true;              // Draw the structure markers
input int               InpArrowHigh    = 217;               // Arrow code - structure high
input int               InpArrowLow     = 218;               // Arrow code - structure low
input int               InpArrowShift   = 14;                // Arrow distance from the bar (pixels)

input group             "Display - break lines"
input bool              InpShowBreaks   = true;              // Draw BoS / CHoCH / sweep lines
input color             InpBosColor     = clrMediumSeaGreen; // BoS colour
input color             InpChochColor   = clrTomato;         // CHoCH colour
input color             InpSweepColor   = clrMediumOrchid;   // Liquidity sweep colour
input color             InpIdmColor     = clrSteelBlue;      // IDM colour
input int               InpBreakWidth   = 1;                 // Break line width
input bool              InpShowLabels   = true;              // Draw the break captions
input int               InpLabelSize    = 8;                 // Break caption font size

input group             "Alerts"
input bool              InpAlertPopup   = false;             // Popup alert on a break
input bool              InpAlertPush    = false;             // Push notification on a break

#define SMC_EMPTY 0.0

double BufMsHigh[];
double BufMsLow[];
double BufTrend[];
double BufPbHigh[];
double BufPbLow[];

//+-------------------------------------------------------------------+
//| One confirmed pullback swing                                      |
//+-------------------------------------------------------------------+
struct SwingPoint
  {
   int               index;     // bar index in the OnCalculate arrays (0 = oldest)
   double            price;     // extreme price of the swing candle
   bool              is_high;   // side
  };

//+-------------------------------------------------------------------+
//| Pullback detection engine - identical to SMC_Pullback.mq5         |
//|                                                                   |
//| While looking for a HIGH it tracks the highest high; the candle   |
//| that made it also supplies the reference level - its own low. A   |
//| later candle that breaks that low confirms the tracked high as a  |
//| swing, and the search flips to the LOW side, seeded from the      |
//| breaking candle. Looking for a LOW is the mirror image.           |
//|                                                                   |
//| One candle can make a new extreme AND break the reference. It is  |
//| then the swing itself, and it seeds the opposite leg as well, so  |
//| at most one swing is ever confirmed on a single bar.              |
//+-------------------------------------------------------------------+
class CPullbackDetector
  {
private:
   ENUM_BREAK_MODE   m_break_mode;
   bool              m_looking_high;  // side being searched for
   bool              m_started;       // first leg seeded
   int               m_ext_index;     // candle holding the tracked extreme
   double            m_ext_price;     // tracked extreme: highest high or lowest low
   double            m_ref_price;     // level whose break confirms the extreme

   //--- (re)starts a leg on bar i: that candle is both the tracked
   //--- extreme and the source of the reference level
   void              StartLeg(const bool up, const int i, const double &h[], const double &l[])
     {
      m_looking_high = up;
      m_ext_index    = i;
      m_ext_price    = up ? h[i] : l[i];
      m_ref_price    = up ? l[i] : h[i];
     }

public:
   void              Configure(const ENUM_LOOKING_FOR side, const ENUM_BREAK_MODE mode)
     {
      m_break_mode   = mode;
      m_looking_high = (side == LOOK_HIGH);
      m_started      = false;
      m_ext_index    = -1;
      m_ext_price    = 0.0;
      m_ref_price    = 0.0;
     }

   bool              LookingHigh(void) const { return(m_looking_high); }
   double            Reference(void)   const { return(m_ref_price);   }

   bool              Step(const int i, const double &h[], const double &l[],
                          const double &c[], SwingPoint &out);
  };

//+-------------------------------------------------------------------+
//| Evaluates one closed bar. Returns true when a swing became final  |
//| on this bar; `out` is then that swing, and it never moves again.  |
//+-------------------------------------------------------------------+
bool CPullbackDetector::Step(const int i, const double &h[], const double &l[],
                             const double &c[], SwingPoint &out)
  {
   if(!m_started)                             // seed the very first leg
     {
      StartLeg(m_looking_high, i, h, l);
      m_started = true;
      return(false);
     }

//--- both tests use the state as it stands on entry, so extending the
//--- leg cannot hide a break of the reference that is still in force
   const bool   extreme = m_looking_high ? (h[i] > m_ext_price) : (l[i] < m_ext_price);
   const double level   = (m_break_mode == BREAK_BY_CLOSE) ? c[i]
                          : (m_looking_high ? l[i] : h[i]);
   const bool   broke   = m_looking_high ? (level < m_ref_price) : (level > m_ref_price);

   if(extreme && broke)                       // outside bar: it is the swing itself
     {
      out.index   = i;
      out.price   = m_looking_high ? h[i] : l[i];
      out.is_high = m_looking_high;
     }
   else
      if(extreme)                             // leg extends, this candle is the new reference
        {
         StartLeg(m_looking_high, i, h, l);
         return(false);
        }
      else
         if(broke)                            // the tracked extreme is confirmed
           {
            out.index   = m_ext_index;
            out.price   = m_ext_price;
            out.is_high = m_looking_high;
           }
         else
            return(false);                    // inside bar: nothing changes

   StartLeg(!m_looking_high, i, h, l);        // this candle opens the opposite leg
   return(true);
  }

//+-------------------------------------------------------------------+
//| One point of the market structure chain                           |
//+-------------------------------------------------------------------+
struct MsPoint
  {
   int               index;     // bar index of the extreme
   double            price;     // the extreme itself
   bool              is_high;   // side - the chain always alternates
  };

//--- kinds of structure event
#define MS_EVENT_BOS    0
#define MS_EVENT_CHOCH  1
#define MS_EVENT_SWEEP  2
#define MS_EVENT_IDM    3

//+-------------------------------------------------------------------+
//| One structure event, drawn as a horizontal segment                |
//+-------------------------------------------------------------------+
struct MsEvent
  {
   int               type;      // MS_EVENT_*
   int               from;      // bar index of the level that was broken / swept
   int               to;        // bar index of the candle that broke / swept it
   double            price;     // the level itself
   bool              bullish;   // direction of the break
   bool              above;     // caption side: above the line in an uptrend
  };

//+-------------------------------------------------------------------+
//| Market structure engine                                           |
//|                                                                   |
//| Pure logic - no buffers, no chart objects, so it can be lifted    |
//| into an EA unchanged. It consumes closed bars plus the pullback   |
//| swings confirmed on them, and produces an alternating chain of    |
//| structure points and an append-only list of events.               |
//|                                                                   |
//| A point is published as soon as it is known, so the chart can     |
//| show the live structure, and `Dirty()` reports the oldest chain   |
//| slot that changed - all the chart layer has to redraw.            |
//+-------------------------------------------------------------------+
class CMarketStructure
  {
private:
   //--- configuration
   ENUM_TOUCH_MODE   m_touch_mode;
   bool              m_use_sweeps;

   //--- output
   MsPoint           m_chain[];
   int               m_dirty;         // oldest chain slot changed since the last Consume
   MsEvent           m_events[];

   //--- trend and the two live structure levels
   ENUM_MS_TREND     m_trend;
   bool              m_seeded;
   int               m_high_slot;     // chain slot of the live high, -1 when consumed
   int               m_low_slot;      // chain slot of the live low
   int               m_high_index;
   double            m_high_price;
   int               m_low_index;
   double            m_low_price;

   //--- pending extreme: the break side has no level until it is confirmed
   int               m_pending;       // 0 none, +1 waiting for a high, -1 waiting for a low
   int               m_break_bar;     // candle that broke the structure
   int               m_pend_index;    // running extreme since the break
   double            m_pend_price;
   int               m_ref_index;     // bar of that pullback, the left end of the IDM line
   double            m_ref_price;     // pullback level whose touch confirms the extreme

   //--- wick-only excursions, kept until the next break resolves them
   bool              m_swept_high;
   int               m_swept_high_index;
   double            m_swept_high_price;
   bool              m_swept_low;
   int               m_swept_low_index;
   double            m_swept_low_price;

   //--- newest confirmed pullback on each side, the source of m_ref_price
   bool              m_have_pb_high;
   int               m_pb_high_index;
   double            m_pb_high_price;
   bool              m_have_pb_low;
   int               m_pb_low_index;
   double            m_pb_low_price;

   void              Touch(const int slot)
     {
      if(slot >= 0 && slot < m_dirty)
         m_dirty = slot;
     }

   void              AddPoint(const bool is_high, const int index, const double price)
     {
      const int slot = ArraySize(m_chain);
      ArrayResize(m_chain, slot + 1);
      m_chain[slot].index   = index;
      m_chain[slot].price   = price;
      m_chain[slot].is_high = is_high;
      Touch(slot);

      //--- a wick-only excursion only counts while the structure has not
      //--- moved on. Once a new point is published the old sweep is stale,
      //--- and consuming it later would drag a point past its neighbour
      //--- and cross the zigzag.
      m_swept_high = false;
      m_swept_low  = false;

      if(is_high)
        {
         m_high_slot  = slot;
         m_high_index = index;
         m_high_price = price;
        }
      else
        {
         m_low_slot  = slot;
         m_low_index = index;
         m_low_price = price;
        }
     }

   void              MovePoint(const bool is_high, const int index, const double price)
     {
      const int slot = is_high ? m_high_slot : m_low_slot;
      if(slot < 0)
         return;
      m_chain[slot].index = index;
      m_chain[slot].price = price;
      Touch(slot);

      if(is_high)
        {
         m_high_index = index;
         m_high_price = price;
        }
      else
        {
         m_low_index = index;
         m_low_price = price;
        }
     }

   void              AddEvent(const int type, const int from, const int to,
                              const double price, const bool bullish,
                              const bool above)
     {
      const int n = ArraySize(m_events);
      ArrayResize(m_events, n + 1);
      m_events[n].type    = type;
      m_events[n].from    = from;
      m_events[n].to      = to;
      m_events[n].price   = price;
      m_events[n].bullish = bullish;
      m_events[n].above   = above;
     }

   void              FillOpposite(const bool want_high, const int bar,
                                  const double &h[], const double &l[]);
   void              Break(const bool bullish, const int bar,
                           const double &h[], const double &l[]);

public:
   void              Configure(const ENUM_TOUCH_MODE touch, const bool use_sweeps);
   void              Step(const int i, const double &h[], const double &l[], const double &c[],
                          const bool have_swing, const SwingPoint &swing);

   ENUM_MS_TREND     Trend(void)  const { return(m_trend); }
   int               Points(void) const { return(ArraySize(m_chain)); }
   void              Point(const int slot, MsPoint &out) const { out = m_chain[slot]; }
   int               Events(void) const { return(ArraySize(m_events)); }
   void              Event(const int k, MsEvent &out)    const { out = m_events[k]; }

   int               Dirty(void)        const { return(m_dirty); }
   void              ConsumeDirty(void)       { m_dirty = INT_MAX; }
  };

//+-------------------------------------------------------------------+
void CMarketStructure::Configure(const ENUM_TOUCH_MODE touch, const bool use_sweeps)
  {
   m_touch_mode = touch;
   m_use_sweeps = use_sweeps;

   ArrayResize(m_chain,  0);
   ArrayResize(m_events, 0);
   m_dirty = INT_MAX;

   m_trend      = MS_TREND_NONE;
   m_seeded     = false;
   m_high_slot  = -1;
   m_low_slot   = -1;
   m_high_index = -1;
   m_low_index  = -1;
   m_high_price = 0.0;
   m_low_price  = 0.0;

   m_pending    = 0;
   m_break_bar  = -1;
   m_pend_index = -1;
   m_pend_price = 0.0;
   m_ref_index  = -1;
   m_ref_price  = 0.0;

   m_swept_high       = false;
   m_swept_low        = false;
   m_swept_high_index = -1;
   m_swept_low_index  = -1;
   m_swept_high_price = 0.0;
   m_swept_low_price  = 0.0;

   m_have_pb_high  = false;
   m_have_pb_low   = false;
   m_pb_high_index = -1;
   m_pb_low_index  = -1;
   m_pb_high_price = 0.0;
   m_pb_low_price  = 0.0;
  }

//+-------------------------------------------------------------------+
//| Adds the point the chain is missing before the break side can be  |
//| published, scanning the range the break travelled through.        |
//|                                                                   |
//| A bullish break is on its way to a new HIGH, so if the newest     |
//| chain point is already a high, the lowest low between that high   |
//| and the breaking candle becomes the new structure low. When the   |
//| newest point is a low the chain already alternates and nothing    |
//| happens. The bearish case is the mirror image.                    |
//+-------------------------------------------------------------------+
void CMarketStructure::FillOpposite(const bool want_high, const int bar,
                                    const double &h[], const double &l[])
  {
   const int last = ArraySize(m_chain) - 1;
   if(last < 0 || m_chain[last].is_high != want_high)
      return;

   const int from = m_chain[last].index;
   int    best_i  = from;
   double best_p  = want_high ? l[from] : h[from];

   for(int k = from + 1; k <= bar; k++)
     {
      if(want_high ? (l[k] < best_p) : (h[k] > best_p))
        {
         best_p = want_high ? l[k] : h[k];
         best_i = k;
        }
     }
   AddPoint(!want_high, best_i, best_p);
  }

//+-------------------------------------------------------------------+
//| Handles a confirmed break of the structure on bar `bar`.          |
//+-------------------------------------------------------------------+
void CMarketStructure::Break(const bool bullish, const int bar,
                             const double &h[], const double &l[])
  {
//--- a wick-only excursion on the side the move is leaving behind was a
//--- liquidity sweep: that structure point moves onto the swept candle,
//--- so the swing the break starts from is the sweep, not the old level.
//--- The point has to be the newest one in the chain, otherwise moving it
//--- forward would push it past its successor and cross the zigzag.
   const int tail = ArraySize(m_chain) - 1;
   const int swept_slot = bullish ? m_low_slot : m_high_slot;

   if((bullish ? m_swept_low : m_swept_high) && swept_slot == tail && tail >= 0)
     {
      const int    s_i = bullish ? m_swept_low_index : m_swept_high_index;
      const double s_p = bullish ? m_swept_low_price : m_swept_high_price;

      //--- the liquidity line spans the level that was taken: from the
      //--- swing that set it to the candle whose wick ran through it
      const int    old_i = m_chain[tail].index;
      const double old_p = m_chain[tail].price;
      //--- the sweep belongs to the trend that was still in force when the
      //--- wick ran through, not to the break that is about to flip it
      AddEvent(MS_EVENT_SWEEP, old_i, s_i, old_p, !bullish,
               (m_trend != MS_TREND_NONE) ? (m_trend > 0) : !bullish);

      MovePoint(!bullish, s_i, s_p);
     }
   m_swept_high = false;
   m_swept_low  = false;

//--- the level being broken, captured before the chain changes
   const int    broken_i = bullish ? m_high_index : m_low_index;
   const double broken_p = bullish ? m_high_price : m_low_price;

   FillOpposite(bullish, bar, h, l);

   const ENUM_MS_TREND want = bullish ? MS_TREND_UP : MS_TREND_DOWN;
   const bool against = (m_trend != MS_TREND_NONE && m_trend != want);
   AddEvent(against ? MS_EVENT_CHOCH : MS_EVENT_BOS, broken_i, bar, broken_p,
            bullish, bullish);
   m_trend = want;

//--- the broken level is consumed. The new extreme on this side stays
//--- pending: it is only confirmed once price comes back to the highest
//--- pullback low seen so far (mirrored for a bearish break).
   m_pending    = bullish ? +1 : -1;
   m_break_bar  = bar;
   m_pend_index = bar;
   m_pend_price = bullish ? h[bar] : l[bar];

   if(bullish)
     {
      m_high_slot = -1;
      m_ref_index = m_have_pb_low ? m_pb_low_index : m_low_index;
      m_ref_price = m_have_pb_low ? m_pb_low_price : m_low_price;
     }
   else
     {
      m_low_slot  = -1;
      m_ref_index = m_have_pb_high ? m_pb_high_index : m_high_index;
      m_ref_price = m_have_pb_high ? m_pb_high_price : m_high_price;
     }
  }

//+-------------------------------------------------------------------+
//| Evaluates one closed bar together with the pullback swing, if     |
//| any, that was confirmed on it.                                    |
//+-------------------------------------------------------------------+
void CMarketStructure::Step(const int i, const double &h[], const double &l[], const double &c[],
                            const bool have_swing, const SwingPoint &swing)
  {
//--- remember the newest pullback on each side
   if(have_swing)
     {
      if(swing.is_high)
        {
         m_have_pb_high  = true;
         m_pb_high_index = swing.index;
         m_pb_high_price = swing.price;
        }
      else
        {
         m_have_pb_low  = true;
         m_pb_low_index = swing.index;
         m_pb_low_price = swing.price;
        }

      //--- the reference only ever moves with the move: the highest
      //--- pullback low after a bullish break, and the mirror after a
      //--- bearish one
      if(m_pending > 0 && !swing.is_high && swing.price > m_ref_price)
        {
         m_ref_index = swing.index;
         m_ref_price = swing.price;
        }
      if(m_pending < 0 &&  swing.is_high && swing.price < m_ref_price)
        {
         m_ref_index = swing.index;
         m_ref_price = swing.price;
        }
     }

//--- the structure starts as the first pullback high and low, no trend
   if(!m_seeded)
     {
      if(!m_have_pb_high || !m_have_pb_low)
         return;
      if(m_pb_high_index <= m_pb_low_index)
        {
         AddPoint(true,  m_pb_high_index, m_pb_high_price);
         AddPoint(false, m_pb_low_index,  m_pb_low_price);
        }
      else
        {
         AddPoint(false, m_pb_low_index,  m_pb_low_price);
         AddPoint(true,  m_pb_high_index, m_pb_high_price);
        }
      m_seeded = true;
      return;
     }

//--- extend and possibly confirm the pending extreme. This runs before
//--- the break test, so a candle that both completes the confirmation
//--- and breaks the opposite level is read in that order - which is
//--- exactly the sweep-then-CHoCH case.
   if(m_pending != 0)
     {
      if(m_pending > 0 ? (h[i] > m_pend_price) : (l[i] < m_pend_price))
        {
         m_pend_price = (m_pending > 0) ? h[i] : l[i];
         m_pend_index = i;
        }

      //--- the IDM has to be taken by a LATER candle than the one that
      //--- set it. Without the second test a pullback low confirmed on
      //--- this very bar (the outside-bar case) would be taken by its
      //--- own low the instant it becomes the reference.
      if(i > m_break_bar && i > m_ref_index)
        {
         const double probe = (m_touch_mode == TOUCH_BY_CLOSE) ? c[i]
                              : ((m_pending > 0) ? l[i] : h[i]);
         const bool   back  = (m_pending > 0) ? (probe <= m_ref_price)
                              : (probe >= m_ref_price);
         if(back)
           {
            //--- the IDM is the reference itself: drawn from the pullback
            //--- that set it to the candle that ran through it, which is
            //--- the candle confirming the new extreme
            AddEvent(MS_EVENT_IDM, m_ref_index, i, m_ref_price,
                     m_pending < 0, m_pending > 0);
            AddPoint(m_pending > 0, m_pend_index, m_pend_price);
            m_pending = 0;
           }
        }
     }

//--- a close beyond a live level is a break, a wick alone is a sweep.
//--- While an extreme is pending its own side holds no level, so only
//--- the opposite one can be tested.
   const bool test_up   = (m_pending <= 0 && m_high_slot >= 0);
   const bool test_down = (m_pending >= 0 && m_low_slot  >= 0);

   if(test_up && c[i] > m_high_price)
     {
      if(m_pending < 0)                  // give up on the pending low: publish what we tracked
        {
         AddPoint(false, m_pend_index, m_pend_price);
         m_pending = 0;
        }
      Break(true, i, h, l);
      return;
     }
   if(test_down && c[i] < m_low_price)
     {
      if(m_pending > 0)
        {
         AddPoint(true, m_pend_index, m_pend_price);
         m_pending = 0;
        }
      Break(false, i, h, l);
      return;
     }

   if(!m_use_sweeps)
      return;

   if(test_up && h[i] > m_high_price && (!m_swept_high || h[i] > m_swept_high_price))
     {
      m_swept_high       = true;
      m_swept_high_index = i;
      m_swept_high_price = h[i];
     }
   if(test_down && l[i] < m_low_price && (!m_swept_low || l[i] < m_swept_low_price))
     {
      m_swept_low       = true;
      m_swept_low_index = i;
      m_swept_low_price = l[i];
     }
  }

//+-------------------------------------------------------------------+
//| Chart layer                                                       |
//+-------------------------------------------------------------------+
CPullbackDetector Detector;
CMarketStructure  Structure;

string   ObjPrefix     = "";     // unique per chart, so instances cannot collide
int      NextBar       = 0;      // next closed bar to evaluate
datetime LastAlertTime = 0;

//--- pullback zigzag: append only, that layer never repaints
long     PbSerial      = 0;
bool     HasPbPrev     = false;
datetime PbPrevTime    = 0;
double   PbPrevPrice   = 0.0;

//--- structure zigzag and markers: rebuilt from the oldest dirty slot
int      MsDrawn       = 0;      // chain slots currently on the chart
int      MsDrawnBar[];           // bar each drawn slot used, so its cell can be cleared
int      EvDrawn       = 0;      // events currently on the chart

//+-------------------------------------------------------------------+
string MsLegName(const int k) { return(ObjPrefix + "MZ" + (string)k); }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string EvLineName(const int k) { return(ObjPrefix + "EV" + (string)k); }
string EvTextName(const int k) { return(ObjPrefix + "EL" + (string)k); }

//+-------------------------------------------------------------------+
void ResetChart(void)
  {
   ObjectsDeleteAll(0, ObjPrefix);
   PbSerial    = 0;
   HasPbPrev   = false;
   PbPrevTime  = 0;
   PbPrevPrice = 0.0;
   MsDrawn     = 0;
   EvDrawn     = 0;
   ArrayResize(MsDrawnBar, 0);
   NextBar     = 0;
  }

//+-------------------------------------------------------------------+
int OnInit(void)
  {
   ObjPrefix = StringFormat("SMCMS_%I64d_", ChartID());

   SetIndexBuffer(0, BufMsHigh, INDICATOR_DATA);
   SetIndexBuffer(1, BufMsLow,  INDICATOR_DATA);
   SetIndexBuffer(2, BufTrend,  INDICATOR_DATA);
   SetIndexBuffer(3, BufPbHigh, INDICATOR_DATA);
   SetIndexBuffer(4, BufPbLow,  INDICATOR_DATA);

   for(int p = 0; p < 5; p++)
     {
      PlotIndexSetDouble(p, PLOT_EMPTY_VALUE, SMC_EMPTY);
      PlotIndexSetInteger(p, PLOT_DRAW_BEGIN, 0);
     }

//--- the buffers are filled either way, so iCustom still sees everything
   PlotIndexSetInteger(0, PLOT_DRAW_TYPE, InpShowMarkers ? DRAW_ARROW : DRAW_NONE);
   PlotIndexSetInteger(1, PLOT_DRAW_TYPE, InpShowMarkers ? DRAW_ARROW : DRAW_NONE);
   PlotIndexSetInteger(0, PLOT_ARROW, InpArrowHigh);
   PlotIndexSetInteger(1, PLOT_ARROW, InpArrowLow);
   PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -InpArrowShift);
   PlotIndexSetInteger(1, PLOT_ARROW_SHIFT,  InpArrowShift);

   IndicatorSetString(INDICATOR_SHORTNAME, "SMC Market Structure");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

   Detector.Configure(InpStartSide, InpBreakMode);
   Structure.Configure(InpTouchMode, InpUseSweeps);
   LastAlertTime = 0;
   ResetChart();

   return(INIT_SUCCEEDED);
  }

//+-------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, ObjPrefix);
   ChartRedraw();
  }

//+-------------------------------------------------------------------+
void StyleLine(const string name, const color clr, const ENUM_LINE_STYLE style,
               const int width, const bool back)
  {
   ObjectSetInteger(0, name, OBJPROP_COLOR,      clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE,      style);
   ObjectSetInteger(0, name, OBJPROP_WIDTH,      width);
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT,  false);
   ObjectSetInteger(0, name, OBJPROP_RAY_LEFT,   false);
   ObjectSetInteger(0, name, OBJPROP_BACK,       back);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN,     true);
  }

//+-------------------------------------------------------------------+
//| Draws the pullback leg from the previous swing to this one.       |
//|                                                                   |
//| Legs cannot live in an indicator buffer: the outside-bar case     |
//| puts a high and a low on the SAME candle, and a DRAW_SECTION      |
//| plot holds one value per bar, so the second point would overwrite |
//| the first. With trend lines that leg is just vertical.            |
//+-------------------------------------------------------------------+
void DrawPbLeg(const datetime t, const double price)
  {
   if(HasPbPrev && InpShowPbLegs)
     {
      const string name = ObjPrefix + "PZ" + (string)(++PbSerial);
      if(ObjectCreate(0, name, OBJ_TREND, 0, PbPrevTime, PbPrevPrice, t, price))
         StyleLine(name, InpPbLegColor, InpPbLegStyle, InpPbLegWidth, true);
     }
   HasPbPrev   = true;
   PbPrevTime  = t;
   PbPrevPrice = price;
  }

//+-------------------------------------------------------------------+
//| Brings the structure markers and zigzag in line with the chain.   |
//|                                                                   |
//| Everything from the oldest changed slot onwards is thrown away and|
//| drawn again. That is what makes a moved point - a sweep, or an    |
//| extreme that is still being confirmed - update its marker and its |
//| legs together instead of leaving a stale copy behind.             |
//+-------------------------------------------------------------------+
void RefreshStructure(const datetime &time[])
  {
   const int total = Structure.Points();
   int from = Structure.Dirty();
   if(from >= total && from >= MsDrawn)
      return;                                  // nothing changed
   if(from > MsDrawn)
      from = MsDrawn;
   Structure.ConsumeDirty();

//--- drop the stale tail, markers included
   for(int k = from; k < MsDrawn; k++)
     {
      ObjectDelete(0, MsLegName(k));
      const int bar = MsDrawnBar[k];
      if(bar >= 0)
        {
         BufMsHigh[bar] = SMC_EMPTY;
         BufMsLow[bar]  = SMC_EMPTY;
        }
     }

   ArrayResize(MsDrawnBar, total);

   MsPoint p, prev;
   for(int k = from; k < total; k++)
     {
      Structure.Point(k, p);
      MsDrawnBar[k] = p.index;

      if(p.is_high)
         BufMsHigh[p.index] = p.price;
      else
         BufMsLow[p.index] = p.price;

      if(InpShowMsLegs && k > 0)
        {
         Structure.Point(k - 1, prev);
         const string name = MsLegName(k);
         ObjectDelete(0, name);
         if(ObjectCreate(0, name, OBJ_TREND, 0, time[prev.index], prev.price,
                         time[p.index], p.price))
            StyleLine(name, InpMsLegColor, InpMsLegStyle, InpMsLegWidth, false);
        }
     }
   MsDrawn = total;
  }

//+-------------------------------------------------------------------+
//| Draws the structure events that appeared since the last call.     |
//| Events never move, so they are append only.                       |
//+-------------------------------------------------------------------+
void RefreshEvents(const datetime &time[])
  {
   const int total = Structure.Events();
   if(!InpShowBreaks)
     {
      EvDrawn = total;
      return;
     }

   MsEvent e;
   for(; EvDrawn < total; EvDrawn++)
     {
      Structure.Event(EvDrawn, e);
      if(e.from < 0 || e.to < 0)
         continue;

      color           clr   = InpSweepColor;
      string          text  = "x";
      ENUM_LINE_STYLE style = STYLE_SOLID;

      if(e.type == MS_EVENT_BOS)
        {
         clr   = InpBosColor;
         text  = "BoS";
         style = STYLE_DASH;
        }
      else
         if(e.type == MS_EVENT_CHOCH)
           {
            clr   = InpChochColor;
            text  = "CHoCH";
            style = STYLE_DASH;
           }
         else
            if(e.type == MS_EVENT_IDM)
              {
               clr   = InpIdmColor;
               text  = "IDM";
               style = STYLE_DOT;
              }

      const string line = EvLineName(EvDrawn);
      if(ObjectCreate(0, line, OBJ_TREND, 0, time[e.from], e.price, time[e.to], e.price))
         StyleLine(line, clr, style, InpBreakWidth, false);

      if(InpShowLabels)
        {
         //--- the caption sits over the middle of its own line, on the side
         //--- the trend points: above in an uptrend, below in a downtrend
         const int    mid = (e.from + e.to) / 2;
         const string lbl = EvTextName(EvDrawn);
         if(ObjectCreate(0, lbl, OBJ_TEXT, 0, time[mid], e.price))
           {
            ObjectSetString(0, lbl, OBJPROP_TEXT, text);
            ObjectSetString(0, lbl, OBJPROP_FONT, "Arial");
            ObjectSetInteger(0, lbl, OBJPROP_FONTSIZE,   InpLabelSize);
            ObjectSetInteger(0, lbl, OBJPROP_COLOR,      clr);
            ObjectSetInteger(0, lbl, OBJPROP_ANCHOR,     e.above ? ANCHOR_LOWER
                             : ANCHOR_UPPER);
            ObjectSetInteger(0, lbl, OBJPROP_SELECTABLE, false);
            ObjectSetInteger(0, lbl, OBJPROP_HIDDEN,     true);
           }
        }
     }
  }

//+-------------------------------------------------------------------+
void RaiseAlert(const MsEvent &e, const datetime t)
  {
   if((!InpAlertPopup && !InpAlertPush) || t <= LastAlertTime)
      return;
   LastAlertTime = t;

   string kind = "Liquidity sweep";
   if(e.type == MS_EVENT_BOS)
      kind = "BoS";
   else
     {
      if(e.type == MS_EVENT_CHOCH)
         kind = "CHoCH";
      else
        {
         if(e.type == MS_EVENT_IDM)
            kind = "IDM";
        }
     }

   const string text = StringFormat("%s %s: %s %s at %s",
                                    _Symbol, EnumToString((ENUM_TIMEFRAMES)_Period),
                                    kind, e.bullish ? "up" : "down",
                                    DoubleToString(e.price, _Digits));
   if(InpAlertPopup)
      Alert(text);
   if(InpAlertPush)
      SendNotification(text);
  }

//+-------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total < 3)
      return(0);

//--- first call, or history reloaded / shifted: rebuild from scratch
   if(prev_calculated == 0)
     {
      ArrayInitialize(BufMsHigh, SMC_EMPTY);
      ArrayInitialize(BufMsLow,  SMC_EMPTY);
      ArrayInitialize(BufTrend,  SMC_EMPTY);
      ArrayInitialize(BufPbHigh, SMC_EMPTY);
      ArrayInitialize(BufPbLow,  SMC_EMPTY);
      Detector.Configure(InpStartSide, InpBreakMode);
      Structure.Configure(InpTouchMode, InpUseSweeps);
      ResetChart();
     }

   const int last_closed = rates_total - 2;    // the forming bar is never used
   const int first_event = Structure.Events();

//--- one pass per closed bar: the pullback layer first, then the
//--- structure, so the structure already sees the swing of that bar
   for(; NextBar <= last_closed; NextBar++)
     {
      const int  i = NextBar;
      SwingPoint swing;
      const bool have = Detector.Step(i, high, low, close, swing);

      if(have)
        {
         if(swing.is_high)
            BufPbHigh[swing.index] = swing.price;
         else
            BufPbLow[swing.index] = swing.price;
         DrawPbLeg(time[swing.index], swing.price);
        }

      Structure.Step(i, high, low, close, have, swing);
      BufTrend[i] = (double)Structure.Trend();
     }

//--- the forming bar carries the latest known trend, so a reader of the
//--- buffer at shift 0 never sees an empty value
   BufTrend[rates_total - 1] = (double)Structure.Trend();

   RefreshStructure(time);
   RefreshEvents(time);

   const int total_events = Structure.Events();
   if(total_events > first_event && prev_calculated > 0)
     {
      MsEvent e;
      Structure.Event(total_events - 1, e);
      RaiseAlert(e, time[e.to]);
     }

   return(rates_total);
  }
//+-------------------------------------------------------------------+