FreeMT5MQL5 source code

Swing Detector by Pullback (Smart Money Concepts)

Almost every structure-based method starts with the same question: which highs and lows actually matter? Everything downstream — market structure, BOS and CHoCH, order blocks, liquidity levels — is built on top of that one answer, so the definition you pick decides what the rest of your analysis sees.

There are several common ways to answer it. The best known is the 3-candle pattern (the fractal): a candle whose high is above both neighbours is a swing high. It is symmetric, trivial to compute, and it is what most swing indicators use. Others widen the window to 5 or 7 candles, or filter candidates by a minimum move in points or ATR.

This indicator uses a different definition — the pullback definition used in Trading Hub 3.0. Instead of asking whether a candle is taller than its neighbours, it asks whether price came back and rejected that candle. A high becomes a swing only once the market trades back through the low of the candle that made it. Nothing is decided by shape; everything is decided by what price does afterwards.

The practical difference: a fractal describes geometry, while a pullback describes an event. Because a pullback is an event, it also tells you when the swing became valid — and that timestamp is what makes a non-repainting implementation possible.

How the rule works

The detector is never looking for both sides at once. It searches for one side at a time, and it holds exactly three pieces of information:

State Meaning
Direction which side is currently being searched for — a high or a low
Tracked extreme the highest high (or lowest low) seen so far in the current leg
Reference level the opposite edge of the candle that made that extreme

The reference is the important part. When a candle sets a new highest high, that same candle also supplies the level that will later confirm it: its own low. Price must come back through that low for the high to count.

Reading the figure:

  • A — a leg opens. The tracked high and the reference low both come from one candle.
  • B — every higher high replaces both values. The high is still provisional; nothing is drawn.
  • C — a candle finally trades below the reference low. That is the pullback, and it confirms the tracked high as a swing high.
  • D — the confirming candle immediately opens the search for a low, mirrored: track the lowest low, the reference is its high, a break above confirms it.

Because confirming one side always flips the search to the other, swing highs and lows alternate automatically. There is no separate rule enforcing it and no comparison against earlier swings.

The rules, candle by candle

On every closed candle the detector asks two independent questions: did this candle make a new extreme? and did it break the reference? Four combinations are possible per direction.

# Current trend Current candle Action 1 Action 2
1 Looking for a HIGH Inside bar — no new high, reference low intact nothing nothing
2 Looking for a HIGH New high, reference low intact tracked high = this candle's high reference low = this candle's low
3 Looking for a HIGH Break — no new high, but trades below the reference low confirm the tracked high as a swing high flip to LOW: tracked low = this candle's low, reference high = this candle's high
4 Looking for a HIGH Outside bar — new high and breaks the reference low confirm this candle's own high as a swing high flip to LOW: tracked low = this candle's low, reference high = this candle's high
5 Looking for a LOW Inside bar — no new low, reference high intact nothing nothing
6 Looking for a LOW New low, reference high intact tracked low = this candle's low reference high = this candle's high
7 Looking for a LOW Break — no new low, but trades above the reference high confirm the tracked low as a swing low flip to HIGH: tracked high = this candle's high, reference low = this candle's low
8 Looking for a LOW Outside bar — new low and breaks the reference high confirm this candle's own low as a swing low flip to HIGH: tracked high = this candle's high, reference low = this candle's low

Rows 1–4 and 5–8 are exact mirrors, which is why the whole engine is only a few dozen lines.

The rare case: a candle that breaks both directions

Rows 4 and 8 deserve their own explanation. Usually a candle either extends the leg or breaks the reference. An outside bar can do both at once — it makes a new high and trades below the reference low in the same candle.

Both rules then claim it, and they contradict each other. Treating it as an extension throws away a real break; treating it as a break would confirm a high that this very candle has already exceeded.

The resolution is to let the candle be the swing itself:

  1. Its own high is confirmed as the swing high — not the older tracked high, which it has just taken out.
  2. The same candle then opens the opposite leg: the tracked low becomes its low and the reference becomes its high.

So one candle carries both a swing high and the start of the low leg. On the chart this shows up as a marker above and below the same bar, with a vertical leg between them — which is exactly what happened inside that candle.

This is also why the legs are drawn as trend-line objects rather than a DRAW_SECTION buffer. A section plot stores one value per bar and could not hold two points on the same candle.

It does not repaint

The tracked extreme is provisional and is never drawn. A swing is written to the chart only on the candle that confirms it, and once written it is never moved, recoloured or removed. Only closed candles are evaluated; the forming candle is ignored entirely.

The trade-off is honest and worth stating: confirmation lag. The newest swing appears only after the break that validates it. If you want a marker the instant a new high prints, this is the wrong tool — that marker would have to move later, which is repainting.

Inputs

Detection

Input Default What it does
Start by looking for Looking for a swing high Which side the very first leg searches for. It only affects the oldest bars on the chart; after the first confirmation the direction is driven entirely by the breaks. Leave it alone unless the first swing on your history looks wrong.
Break confirmed by Wick Wick uses the candle's high/low — the literal Trading Hub rule, and the more sensitive setting. Close requires the candle to close through the reference, which ignores single-wick pokes and gives noticeably fewer, larger swings.

How to choose: start with Wick. If you are working on M1–M15, or on a symbol with long noisy wicks such as gold or an index CFD, switch to Close — it removes most of the swings caused by a single spike without changing the logic anywhere else. A higher timeframe is the other way to get larger structure; the rule itself has no sensitivity or period setting to tune.

Display

Input Default What it does
Draw the swing markers true Turns the arrows on or off. The buffers stay filled either way, so iCustom still returns every swing.
Arrow code — swing high / low 217 / 218 Wingdings codes. Common alternatives: 234/233, 241/242, 159 for dots.
Arrow distance from the bar 10 Gap in pixels between the candle and its arrow. Increase it on dense charts.
Draw the legs between swings true The dotted zig-zag connecting consecutive swings. Turn it off if you already run a zig-zag.
Leg colour / style / width silver, dotted, 1 Appearance of the legs.

Turning both markers and legs off leaves a purely computational instance — useful when an EA reads the buffers and you do not want anything on the chart.

Alerts

Input Default What it does
Popup alert on a confirmed swing false Terminal popup when a swing is confirmed.
Push notification on a confirmed swing false Push to the MetaQuotes ID set in Tools → Options → Notifications.

Alerts fire only on live updates, never while the indicator loads history, and only once per swing.

Using it from an EA

Two buffers, both non-empty only on confirmed swings (empty value is 0.0):

Buffer Contents
0 price of a confirmed swing high
1 price of a confirmed swing low
MQL5
int h = iCustom(_Symbol, _Period, "SMC_Pullback");
double buf[];
CopyBuffer(h, 0, 0, 100, buf);   // swing highs

The detection engine lives in a single self-contained class, CPullbackDetector, with no chart or buffer dependencies — it takes the price arrays and reports confirmed swings, so it can be copied straight into an EA if you would rather not go through iCustom.

What it deliberately does not do

This indicator stops at the swing points. It does not label HH/HL/LH/LL, and it does not derive BOS, CHoCH, IDM, order blocks or liquidity levels. Those are separate steps that need a reliable set of swings first — which is exactly what this provides.

The project is supported on MQL5 Algo Forge. You can find the code and next updates at below link.

The repo is here: https://forge.mql5.io/SandroBegashvil/SMC-Pullback-CodeBase

Source code

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

SMC_Pullback_v1_0.mq5
//+------------------------------------------------------------------+
//|                                            SMC Pullback v1.0.mq5 |
//|                                Copyright 2026, Sandro Begashvili |
//|                                                                  |
//| Swing detection by the pullback (Trading Hub 3.0) rule.          |
//|                                                                  |
//| The detector looks for one side at a time. 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.    |
//|                                                                  |
//| Does not repaint: the tracked extreme is never drawn, and a      |
//| swing is drawn only on the candle that confirms it, then never   |
//| moved. The cost is confirmation lag - the newest swing appears   |
//| only once the break confirming it has happened.                  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Sandro Begashvili"
#property link      "https://www.mql5.com/en/users/sandrobegashvil"
#property version   "1.00"
#property description "Swing high / low detection by the pullback rule of Trading Hub 3.0. Does not repaint."

#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

#property indicator_label1  "Pullback High"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrOrangeRed
#property indicator_width1  1

#property indicator_label2  "Pullback Low"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrDodgerBlue
#property indicator_width2  1

//--- side the 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 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
  };

input group             "Detection"
input ENUM_LOOKING_FOR  InpStartSide   = LOOK_HIGH;      // Start by looking for
input ENUM_BREAK_MODE   InpBreakMode   = BREAK_BY_WICK;  // Break confirmed by

input group             "Display"
input bool              InpShowMarkers = true;           // Draw the swing markers
input int               InpArrowHigh   = 217;            // Arrow code - swing high
input int               InpArrowLow    = 218;            // Arrow code - swing low
input int               InpArrowShift  = 10;             // Arrow distance from the bar (pixels)
input bool              InpShowLegs    = true;           // Draw the legs between swings
input color             InpLegColor    = clrSilver;      // Leg colour
input ENUM_LINE_STYLE   InpLegStyle    = STYLE_DOT;      // Leg style
input int               InpLegWidth    = 1;              // Leg width

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

#define SMC_EMPTY 0.0

double BufHigh[];
double BufLow[];

//+------------------------------------------------------------------+
//| One confirmed 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
  };

//+------------------------------------------------------------------+
//| Detection engine                                                 |
//|                                                                  |
//| Pure logic - no buffers, no chart objects, so it can be lifted   |
//| into an EA unchanged. A swing leaves the class exactly once,     |
//| through `confirmed`, and is final at that moment.                |
//+------------------------------------------------------------------+
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
   int               m_next_bar;      // next bar to evaluate

   //--- (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];
     }

   void              Confirm(const int i, const double price, SwingPoint &out[])
     {
      const int n = ArraySize(out);
      ArrayResize(out, n + 1);
      out[n].index   = i;
      out[n].price   = price;
      out[n].is_high = m_looking_high;
     }

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;
      m_next_bar     = 0;
     }

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

   void              Update(const int rates_total, const double &h[], const double &l[],
                            const double &c[], SwingPoint &confirmed[]);
  };

//+------------------------------------------------------------------+
//| Evaluates every closed bar not seen yet and appends the swings   |
//| that became final during this call.                              |
//+------------------------------------------------------------------+
void CPullbackDetector::Update(const int rates_total, const double &h[], const double &l[],
                               const double &c[], SwingPoint &confirmed[])
  {
   ArrayResize(confirmed, 0);

   const int last_closed = rates_total - 2;   // the forming bar is never used

   for(; m_next_bar <= last_closed; m_next_bar++)
     {
      const int i = m_next_bar;

      if(!m_started)                          // seed the very first leg
        {
         StartLeg(m_looking_high, i, h, l);
         m_started = true;
         continue;
        }

      //--- 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
         Confirm(i, m_looking_high ? h[i] : l[i], confirmed);
      else
         if(extreme)                          // leg extends, this candle is the new reference
           {
            StartLeg(m_looking_high, i, h, l);
            continue;
           }
         else
            if(broke)                         // the tracked extreme is confirmed
               Confirm(m_ext_index, m_ext_price, confirmed);
            else
               continue;                      // inside bar: nothing changes

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

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

string   LegPrefix      = "";      // unique per chart, so instances cannot collide
datetime LastAlertTime  = 0;
bool     HasPrevSwing   = false;   // tail of the leg chain
datetime PrevSwingTime  = 0;
double   PrevSwingPrice = 0.0;
long     LegSerial      = 0;

//+------------------------------------------------------------------+
void ResetLegs(void)
  {
   ObjectsDeleteAll(0, LegPrefix);
   HasPrevSwing = false;
   LegSerial    = 0;
  }

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

   SetIndexBuffer(0, BufHigh, INDICATOR_DATA);
   SetIndexBuffer(1, BufLow,  INDICATOR_DATA);

   for(int p = 0; p < 2; p++)
     {
      PlotIndexSetDouble(p, PLOT_EMPTY_VALUE, SMC_EMPTY);
      PlotIndexSetInteger(p, PLOT_DRAW_BEGIN, 0);
      //--- the buffers are filled either way, so iCustom still sees every swing
      PlotIndexSetInteger(p, 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 Pullback");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

   Detector.Configure(InpStartSide, InpBreakMode);
   LastAlertTime = 0;
   ResetLegs();

   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Draws the 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 DrawLeg(const datetime t, const double price)
  {
   if(HasPrevSwing)
     {
      const string name = LegPrefix + (string)(++LegSerial);
      if(ObjectCreate(0, name, OBJ_TREND, 0, PrevSwingTime, PrevSwingPrice, t, price))
        {
         ObjectSetInteger(0, name, OBJPROP_COLOR,      InpLegColor);
         ObjectSetInteger(0, name, OBJPROP_STYLE,      InpLegStyle);
         ObjectSetInteger(0, name, OBJPROP_WIDTH,      InpLegWidth);
         ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT,  false);
         ObjectSetInteger(0, name, OBJPROP_RAY_LEFT,   false);
         ObjectSetInteger(0, name, OBJPROP_BACK,       true);
         ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
         ObjectSetInteger(0, name, OBJPROP_HIDDEN,     true);
        }
     }
   HasPrevSwing   = true;
   PrevSwingTime  = t;
   PrevSwingPrice = price;
  }

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

   const string text = StringFormat("%s %s: swing %s confirmed at %s",
                                    _Symbol, EnumToString((ENUM_TIMEFRAMES)_Period),
                                    p.is_high ? "HIGH" : "LOW",
                                    DoubleToString(p.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(BufHigh, SMC_EMPTY);
      ArrayInitialize(BufLow,  SMC_EMPTY);
      Detector.Configure(InpStartSide, InpBreakMode);
      ResetLegs();
     }

   SwingPoint confirmed[];
   Detector.Update(rates_total, high, low, close, confirmed);

   const int count = ArraySize(confirmed);
   for(int i = 0; i < count; i++)
     {
      const int idx = confirmed[i].index;
      if(confirmed[i].is_high)
         BufHigh[idx] = confirmed[i].price;
      else
         BufLow[idx] = confirmed[i].price;

      if(InpShowLegs)
         DrawLeg(time[idx], confirmed[i].price);
     }

   if(count > 0)
     {
      if(prev_calculated > 0)                 // never alert while loading history
         RaiseAlert(confirmed[count - 1], time[confirmed[count - 1].index]);
     }

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