| //+------------------------------------------------------------------+
|
| //| SMCZones.mq4 |
|
| //| Copyright 2026, Allan Niñal |
|
| //| https://www.allanninal.dev/mql5 |
|
| //+------------------------------------------------------------------+
|
| //| Smart-money concepts zones: order blocks, fair value gaps, |
|
| //| BOS / CHoCH, optional liquidity sweeps and session filter, and |
|
| //| optional zones from a higher timeframe. |
|
| //| Every zone is decided from closed bars only: its creation and |
|
| //| its mitigation never change afterwards, so nothing appears or |
|
| //| disappears after the bar closed. Rules: |
|
| //| swing N bars each side, known N bars later |
|
| //| BOS/CHoCH a close beyond the last unbroken swing; CHoCH when |
|
| //| it reverses the previous break |
|
| //| order blk the last candle against the move at the start of the |
|
| //| leg that broke the swing |
|
| //| FVG a three-candle gap of at least k x ATR |
|
| //| mitigated chosen rule: touch, midpoint, far-edge wick, or a |
|
| //| close beyond the far edge |
|
| //| An indicator, not a trading system: it draws and alerts only. |
|
| //| MT4 edition: the same engine (SmcEngine.mqh, an exact copy of |
|
| //| the MT5 one), the same inputs and the same 16 iCustom buffers. |
|
| //+------------------------------------------------------------------+
|
| // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| #property copyright "Copyright 2026, Allan Niñal <https://www.allanninal.dev/mql5>"
|
| #property link "https://www.allanninal.dev/mql5"
|
| #property version "1.1"
|
| #property description "Smart-money zones: order blocks, fair value gaps, BOS/CHoCH, optional sweeps, session filter and higher-timeframe zones."
|
| #property description "Non-repainting: zones are decided from closed bars and never change afterwards. Alerts for new, touched and mitigated zones."
|
| #property indicator_chart_window
|
| #property indicator_buffers 16
|
|
|
| #include "SmcEngine.mqh"
|
| #include "ObjSync.mqh"
|
| #include <ChartKit/CkDebug.mqh>
|
|
|
| enum ENUM_SMCZ_MIT
|
| {
|
| SMCZ_MIT_TOUCH=0, // Touch: a wick reaches the near edge
|
| SMCZ_MIT_MID=1, // Midpoint: a wick reaches the middle
|
| SMCZ_MIT_WICK=2, // Far edge, wick: a wick reaches the far edge
|
| SMCZ_MIT_CLOSE=3 // Far edge, close: a bar closes beyond the far edge
|
| };
|
|
|
| input int InpSwing = 3; // Swing strength, bars on each side
|
| input int InpATR = 14; // ATR period
|
| input bool InpShowOB = true; // Draw order blocks
|
| input ENUM_SMCZ_MIT InpOBMit = SMCZ_MIT_CLOSE; // Order block is mitigated when
|
| input bool InpShowFVG = true; // Draw fair value gaps
|
| input double InpFVGMin = 0.20; // Minimum gap, x ATR
|
| input ENUM_SMCZ_MIT InpFVGMit = SMCZ_MIT_WICK; // Gap is mitigated when
|
| input bool InpShowSweeps = false; // Mark liquidity sweeps
|
| input int InpSweepLook = 60; // Swing looked back, bars
|
| input double InpSweepMin = 0.10; // Wick beyond the swing, min x ATR
|
| input bool InpUseSession = false; // Only create zones and sweeps in the session
|
| input int InpSessStart = 7; // Session start, server hour
|
| input int InpSessEnd = 17; // Session end, server hour (exclusive)
|
| input ENUM_TIMEFRAMES InpHTF = PERIOD_CURRENT; // Also draw zones of this timeframe (current = off)
|
| input bool InpShowStruct = true; // Draw BOS / CHoCH lines
|
| input bool InpShowMitigated = false; // Keep mitigated zones, up to their mitigation bar
|
| input int InpDrawBars = 1000; // Draw zones created within this many bars
|
| input int InpMaxZones = 80; // Draw at most this many zones (newest first)
|
| input int InpExtendBars = 12; // Extend live zones this many bars past the last bar
|
| input bool InpFill = true; // Fill the zones
|
| input color InpClrOBBull = 6057759; // Bullish order block
|
| input color InpClrOBBear = 3421312; // Bearish order block
|
| input color InpClrFVGBull = 9198638; // Bullish gap
|
| input color InpClrFVGBear = 2516108; // Bearish gap
|
| input color InpClrHTF = 7888992; // Higher-timeframe zones (outline colour)
|
|
|
| #include "SignalAlerts.mqh"
|
|
|
| input bool InpAlNew = true; // Alert: new order block or gap
|
| input bool InpAlTouch = true; // Alert: price touches a live zone
|
| input bool InpAlMit = false; // Alert: a zone is mitigated
|
| input bool InpAlStruct = true; // Alert: BOS / CHoCH
|
| input bool InpAlSweep = false; // Alert: liquidity sweep
|
| input bool InpDebug = false; // Debug log for bug reports (Experts tab)
|
|
|
| const string PRODUCT="SMC Zones";
|
| const string PFX="SMCZ_";
|
|
|
| //--- the 16 buffers, series-indexed (0 = live bar) as every MT4 buffer; the engine counts bars from the oldest
|
| double b0[],b1[],b2[],b3[],b4[],b5[],b6[],b7[],b8[],b9[],b10[],b11[],b12[],b13[],b14[],b15[];
|
| CSmcEngine g_eng,g_htf;
|
| double g_out[];
|
| datetime g_htf_last=0;
|
| datetime g_ht[]; // bar times of the higher-timeframe series the engine ran on
|
| int g_hn=0;
|
| int g_obj_fail=0; // objects the chart refused in the last pass
|
| int g_dbg_fail=0;
|
| int g_draw_stamp=-1;
|
| bool g_no_draw=false; // set in OnInit: tester without visual mode
|
|
|
| //--- buffer k at series index s
|
| void SetB(const int k,const int s,const double v)
|
| {
|
| switch(k)
|
| {
|
| case 0: b0[s]=v; break;
|
| case 1: b1[s]=v; break;
|
| case 2: b2[s]=v; break;
|
| case 3: b3[s]=v; break;
|
| case 4: b4[s]=v; break;
|
| case 5: b5[s]=v; break;
|
| case 6: b6[s]=v; break;
|
| case 7: b7[s]=v; break;
|
| case 8: b8[s]=v; break;
|
| case 9: b9[s]=v; break;
|
| case 10: b10[s]=v; break;
|
| case 11: b11[s]=v; break;
|
| case 12: b12[s]=v; break;
|
| case 13: b13[s]=v; break;
|
| case 14: b14[s]=v; break;
|
| case 15: b15[s]=v; break;
|
| }
|
| }
|
|
|
| void Configure(CSmcEngine &e)
|
| {
|
| e.swing=InpSwing;
|
| e.atr_period=InpATR;
|
| e.fvg_min=InpFVGMin;
|
| e.ob_mit=(int)InpOBMit;
|
| e.fvg_mit=(int)InpFVGMit;
|
| e.sweep_look=InpSweepLook;
|
| e.sweep_min=InpSweepMin;
|
| e.use_session=InpUseSession;
|
| e.s_start=InpSessStart;
|
| e.s_end=InpSessEnd;
|
| }
|
|
|
| int OnInit()
|
| {
|
| CkDebugStart(PRODUCT,"1.1",InpDebug,StringFormat("swing %d, ATR %d, OB %d mit %d, FVG %d min %.2f mit %d, sweeps %d (%d bars, %.2f), session %d (%d-%d), HTF %s, struct %d, mitigated %d, draw %d bars, max %d zones, extend %d",
|
| InpSwing,InpATR,InpShowOB,(int)InpOBMit,InpShowFVG,InpFVGMin,(int)InpFVGMit,InpShowSweeps,InpSweepLook,InpSweepMin,
|
| InpUseSession,InpSessStart,InpSessEnd,TfName(InpHTF),InpShowStruct,InpShowMitigated,InpDrawBars,InpMaxZones,InpExtendBars));
|
| if(InpSwing<1 || InpSwing>50 || InpATR<1 || InpATR>1000 || InpFVGMin<0 || InpFVGMin>100 || InpSweepLook<1 || InpSweepLook>10000 ||
|
| InpSweepMin<0 || InpSweepMin>100 || InpDrawBars<1 || InpMaxZones<0 || InpMaxZones>1000 || InpExtendBars<0 || InpExtendBars>1000 ||
|
| InpSessStart<0 || InpSessStart>23 || InpSessEnd<0 || InpSessEnd>24)
|
| {
|
| Print("SMC Zones: check the inputs (swing 1-50, ATR period 1-1000, sweep look-back 1-10000, zones 0-1000, extension 0-1000 bars, hours 0-23)");
|
| CkDbg("init refused: inputs out of range");
|
| return(INIT_PARAMETERS_INCORRECT);
|
| }
|
| string names[B_COUNT]={"OB bull top","OB bull bottom","OB bear top","OB bear bottom","FVG bull top","FVG bull bottom",
|
| "FVG bear top","FVG bear bottom","Structure (1 BOS, 2 CHoCH, sign = direction)","Broken level",
|
| "Sweep of a low","Sweep of a high","Zones mitigated","Zones touched","Live bullish zones","Live bearish zones"};
|
| ArrayResize(g_out,B_COUNT);
|
| SetIndexBuffer(0,b0); SetIndexBuffer(1,b1); SetIndexBuffer(2,b2); SetIndexBuffer(3,b3);
|
| SetIndexBuffer(4,b4); SetIndexBuffer(5,b5); SetIndexBuffer(6,b6); SetIndexBuffer(7,b7);
|
| SetIndexBuffer(8,b8); SetIndexBuffer(9,b9); SetIndexBuffer(10,b10); SetIndexBuffer(11,b11);
|
| SetIndexBuffer(12,b12); SetIndexBuffer(13,b13); SetIndexBuffer(14,b14); SetIndexBuffer(15,b15);
|
| for(int k=0; k<B_COUNT; k++)
|
| {
|
| SetIndexStyle(k,DRAW_NONE);
|
| SetIndexEmptyValue(k,EMPTY_VALUE);
|
| SetIndexLabel(k,names[k]);
|
| }
|
| Configure(g_eng);
|
| Configure(g_htf);
|
| g_eng.Reset();
|
| g_htf.Reset();
|
| g_htf_last=0;
|
| g_draw_stamp=-1;
|
| g_hn=0;
|
| ArrayResize(g_ht,0);
|
| g_obj_fail=0;
|
| g_dbg_fail=0;
|
| g_no_draw=(IsTesting() && !IsVisualMode());
|
| IndicatorShortName("SMC Zones");
|
| IndicatorDigits(_Digits);
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| ObjectsDeleteAll(0,PFX);
|
| ChartRedraw();
|
| }
|
|
|
| //--- one rectangle (created once, changed only when it differs)
|
| void Rect(const string name,const datetime t1,const double top,const datetime t2,const double bot,const color clr,
|
| const bool fill,const string tip)
|
| {
|
| if(!OsPlace(name,OBJ_RECTANGLE,2,t1,top,t2,bot))
|
| {
|
| g_obj_fail++;
|
| return;
|
| }
|
| OsInt(name,OBJPROP_COLOR,clr);
|
| OsInt(name,OBJPROP_FILL,fill);
|
| OsInt(name,OBJPROP_BACK,fill);
|
| OsInt(name,OBJPROP_WIDTH,1);
|
| OsStr(name,OBJPROP_TOOLTIP,tip);
|
| }
|
|
|
| void Seg(const string name,const datetime t1,const double p1,const datetime t2,const double p2,const color clr,
|
| const ENUM_LINE_STYLE style,const string tip)
|
| {
|
| if(!OsPlace(name,OBJ_TREND,2,t1,p1,t2,p2))
|
| {
|
| g_obj_fail++;
|
| return;
|
| }
|
| OsInt(name,OBJPROP_COLOR,clr);
|
| OsInt(name,OBJPROP_STYLE,style);
|
| OsInt(name,OBJPROP_RAY_RIGHT,false);
|
| OsInt(name,OBJPROP_BACK,true);
|
| OsStr(name,OBJPROP_TOOLTIP,tip);
|
| }
|
|
|
| void Label(const string name,const datetime t,const double p,const string text,const color clr,const ENUM_ANCHOR_POINT anchor)
|
| {
|
| if(!OsPlace(name,OBJ_TEXT,1,t,p,0,0))
|
| {
|
| g_obj_fail++;
|
| return;
|
| }
|
| OsStr(name,OBJPROP_TEXT,text);
|
| OsInt(name,OBJPROP_COLOR,clr);
|
| OsInt(name,OBJPROP_FONTSIZE,8);
|
| OsInt(name,OBJPROP_ANCHOR,anchor);
|
| }
|
|
|
| //--- draw the zones of one engine; `t` are the bar times of the series the engine ran on, `last_t` is the time of the
|
| //--- last bar of the chart, `ex` the extension in seconds
|
| int DrawEngine(CSmcEngine &e,const datetime &t[],const int bars,const string tag,const bool htf,
|
| const datetime from_t,const datetime right_t,int budget)
|
| {
|
| int drawn=0;
|
| for(int z=e.nz-1; z>=0 && drawn<budget; z--)
|
| {
|
| bool live=(e.zones[z].end<0);
|
| if(!live && !InpShowMitigated)
|
| continue;
|
| if(t[e.zones[z].born]<from_t)
|
| break; // zones are stored in birth order: all older ones are out too
|
| if(e.zones[z].kind==ZK_OB ? !InpShowOB : !InpShowFVG)
|
| continue;
|
| bool bull=(e.zones[z].dir>0);
|
| color clr=htf ? InpClrHTF : (e.zones[z].kind==ZK_OB ? (bull ? InpClrOBBull : InpClrOBBear) : (bull ? InpClrFVGBull : InpClrFVGBear));
|
| datetime t2=live ? right_t : t[e.zones[z].end];
|
| string tip=StringFormat("%s%s %s %s, %s - %s%s",htf ? TfName(InpHTF)+" " : "",bull ? "Bullish" : "Bearish",
|
| e.zones[z].kind==ZK_OB ? "order block" : "fair value gap",
|
| TimeToString(t[e.zones[z].born]),DoubleToString(e.zones[z].bot,_Digits),
|
| DoubleToString(e.zones[z].top,_Digits),live ? "" : " (mitigated)");
|
| string nm=StringFormat("%s%s%d",PFX,tag,z);
|
| Rect(nm,t[e.zones[z].t0],e.zones[z].top,t2,e.zones[z].bot,clr,htf ? false : (InpFill && live),tip);
|
| drawn++;
|
| }
|
| return(drawn);
|
| }
|
|
|
| void Redraw(const datetime &time[],const int n,const datetime &htime[],const int hn)
|
| {
|
| OsBegin();
|
| g_obj_fail=0;
|
| int from_bar=MathMax(0,n-1-InpDrawBars);
|
| datetime from_t=time[from_bar];
|
| datetime right_t=time[n-1]+(datetime)((long)InpExtendBars*PeriodSeconds(PERIOD_CURRENT));
|
| int used=0;
|
| if(InpHTF!=PERIOD_CURRENT && PeriodSeconds(InpHTF)>PeriodSeconds(PERIOD_CURRENT))
|
| used+=DrawEngine(g_htf,htime,hn,"H",true,from_t,right_t,InpMaxZones);
|
| used+=DrawEngine(g_eng,time,n,"Z",false,from_t,right_t,MathMax(0,InpMaxZones-used));
|
| if(InpShowStruct || InpShowSweeps)
|
| {
|
| int shown=0;
|
| if(InpShowStruct)
|
| for(int b=g_eng.nb-1; b>=0 && shown<InpMaxZones; b--)
|
| {
|
| if(g_eng.breaks[b].at<from_bar)
|
| break;
|
| bool up=(g_eng.breaks[b].dir>0);
|
| color clr=up ? InpClrOBBull : InpClrOBBear;
|
| string tx=g_eng.breaks[b].choch ? "CHoCH" : "BOS";
|
| Seg(StringFormat("%sS%d",PFX,b),time[g_eng.breaks[b].frm],g_eng.breaks[b].level,time[g_eng.breaks[b].at],
|
| g_eng.breaks[b].level,clr,STYLE_DOT,tx);
|
| Label(StringFormat("%sT%d",PFX,b),time[(g_eng.breaks[b].frm+g_eng.breaks[b].at)/2],g_eng.breaks[b].level,tx,clr,
|
| up ? ANCHOR_LOWER : ANCHOR_UPPER);
|
| shown++;
|
| }
|
| shown=0;
|
| if(InpShowSweeps)
|
| for(int s=g_eng.ns-1; s>=0 && shown<InpMaxZones; s--)
|
| {
|
| if(g_eng.sweeps[s].at<from_bar)
|
| break;
|
| bool up=(g_eng.sweeps[s].dir>0);
|
| Seg(StringFormat("%sW%d",PFX,s),time[g_eng.sweeps[s].frm],g_eng.sweeps[s].level,time[g_eng.sweeps[s].at],
|
| g_eng.sweeps[s].level,clrSilver,STYLE_DASH,"Liquidity sweep");
|
| Label(StringFormat("%sX%d",PFX,s),time[g_eng.sweeps[s].at],g_eng.sweeps[s].level,"sweep",clrSilver,
|
| up ? ANCHOR_UPPER : ANCHOR_LOWER);
|
| shown++;
|
| }
|
| }
|
| OsEnd(PFX);
|
| if(g_obj_fail!=g_dbg_fail)
|
| {
|
| g_dbg_fail=g_obj_fail;
|
| CkDbg(StringFormat("the chart refused %d object(s), error %d",g_obj_fail,GetLastError()));
|
| }
|
| ChartRedraw();
|
| }
|
|
|
| //--- the alert text for the events of the closed bar `i`
|
| string Events(const double &o[])
|
| {
|
| string s="";
|
| if(InpAlNew)
|
| {
|
| if(o[B_OB_BULL_TOP]!=EMPTY_VALUE && InpShowOB)
|
| s+="new bullish order block "+DoubleToString(o[B_OB_BULL_BOT],_Digits)+"-"+DoubleToString(o[B_OB_BULL_TOP],_Digits)+"; ";
|
| if(o[B_OB_BEAR_TOP]!=EMPTY_VALUE && InpShowOB)
|
| s+="new bearish order block "+DoubleToString(o[B_OB_BEAR_BOT],_Digits)+"-"+DoubleToString(o[B_OB_BEAR_TOP],_Digits)+"; ";
|
| if(o[B_FVG_BULL_TOP]!=EMPTY_VALUE && InpShowFVG)
|
| s+="new bullish gap "+DoubleToString(o[B_FVG_BULL_BOT],_Digits)+"-"+DoubleToString(o[B_FVG_BULL_TOP],_Digits)+"; ";
|
| if(o[B_FVG_BEAR_TOP]!=EMPTY_VALUE && InpShowFVG)
|
| s+="new bearish gap "+DoubleToString(o[B_FVG_BEAR_BOT],_Digits)+"-"+DoubleToString(o[B_FVG_BEAR_TOP],_Digits)+"; ";
|
| }
|
| if(InpAlStruct && o[B_STRUCT]!=EMPTY_VALUE)
|
| s+=StringFormat("%s %s at %s; ",MathAbs(o[B_STRUCT])>1 ? "CHoCH" : "BOS",o[B_STRUCT]>0 ? "up" : "down",
|
| DoubleToString(o[B_LEVEL],_Digits));
|
| if(InpAlSweep && InpShowSweeps)
|
| {
|
| if(o[B_SWEEP_BULL]!=EMPTY_VALUE)
|
| s+="low swept at "+DoubleToString(o[B_SWEEP_BULL],_Digits)+"; ";
|
| if(o[B_SWEEP_BEAR]!=EMPTY_VALUE)
|
| s+="high swept at "+DoubleToString(o[B_SWEEP_BEAR],_Digits)+"; ";
|
| }
|
| if(InpAlTouch && o[B_TOUCHED]!=EMPTY_VALUE && o[B_TOUCHED]>0)
|
| s+=StringFormat("%d zone(s) touched; ",(int)o[B_TOUCHED]);
|
| if(InpAlMit && o[B_MITIGATED]!=EMPTY_VALUE && o[B_MITIGATED]>0)
|
| s+=StringFormat("%d zone(s) mitigated; ",(int)o[B_MITIGATED]);
|
| return(s);
|
| }
|
|
|
| //--- the higher-timeframe engine: completed HTF bars only, rebuilt (and the series copied) only when a new HTF bar has
|
| //--- closed; between those moments the copy in g_ht is used as it is
|
| int UpdateHTF(bool &changed)
|
| {
|
| changed=false;
|
| if(InpHTF==PERIOD_CURRENT || PeriodSeconds(InpHTF)<=PeriodSeconds(PERIOD_CURRENT))
|
| return(0);
|
| datetime cur=iTime(_Symbol,InpHTF,0);
|
| if(cur==0)
|
| return(0);
|
| if(g_htf_last==cur && g_htf.done>=0)
|
| return(g_hn);
|
| int want=MathMin(Bars(_Symbol,InpHTF),3000);
|
| double ho[],hh[],hl[],hc[];
|
| if(want<3 || CopyTime(_Symbol,InpHTF,0,want,g_ht)!=want || CopyOpen(_Symbol,InpHTF,0,want,ho)!=want ||
|
| CopyHigh(_Symbol,InpHTF,0,want,hh)!=want || CopyLow(_Symbol,InpHTF,0,want,hl)!=want ||
|
| CopyClose(_Symbol,InpHTF,0,want,hc)!=want)
|
| {
|
| CkDbg(StringFormat("higher-timeframe %s history not ready (%d bars wanted), error %d",TfName(InpHTF),want,GetLastError()));
|
| g_hn=0;
|
| return(0);
|
| }
|
| g_htf.Reset();
|
| double ob[];
|
| ArrayResize(ob,B_COUNT);
|
| for(int i=0; i<=want-2; i++)
|
| g_htf.Step(i,g_ht,ho,hh,hl,hc,ob);
|
| g_htf_last=cur;
|
| g_hn=want;
|
| changed=true;
|
| CkDbg(StringFormat("higher-timeframe %s engine rebuilt on %d bars, %d zones",TfName(InpHTF),want,g_htf.nz));
|
| return(want);
|
| }
|
|
|
| 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);
|
| //--- the engine and the drawing count bars from the oldest, as MT5 does
|
| ArraySetAsSeries(time,false);
|
| ArraySetAsSeries(open,false);
|
| ArraySetAsSeries(high,false);
|
| ArraySetAsSeries(low,false);
|
| ArraySetAsSeries(close,false);
|
| if(prev_calculated==0 || g_eng.done>rates_total-2)
|
| g_eng.Reset();
|
| bool moved=false;
|
| for(int i=g_eng.done+1; i<=rates_total-2; i++)
|
| {
|
| g_eng.Step(i,time,open,high,low,close,g_out);
|
| for(int k=0; k<B_COUNT; k++)
|
| SetB(k,rates_total-1-i,g_out[k]);
|
| moved=true;
|
| if(i==rates_total-2 && prev_calculated>0)
|
| {
|
| string ev=Events(g_out);
|
| if(ev!="")
|
| {
|
| CkDbg("alert for the closed bar "+TimeToString(time[i])+": "+ev);
|
| SignalAlert(PRODUCT,ev,time[i],true);
|
| }
|
| }
|
| }
|
| for(int k=0; k<B_COUNT; k++)
|
| SetB(k,0,EMPTY_VALUE);
|
| if(g_no_draw) // non-visual Strategy Tester: no chart, so no objects (the
|
| return(rates_total); // buffers above, read by iCustom and alerts, are unchanged)
|
| bool hchanged=false;
|
| int hn=UpdateHTF(hchanged);
|
| if(moved || hchanged || g_draw_stamp!=rates_total)
|
| {
|
| Redraw(time,rates_total,g_ht,hn);
|
| g_draw_stamp=rates_total;
|
| }
|
| return(rates_total);
|
| }
|
| //+------------------------------------------------------------------+
|