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