//+------------------------------------------------------------------+ //| 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 " #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 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=0 && drawn0); 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 && shown0); 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 && shown0); 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; k0) { 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