//+------------------------------------------------------------------+ //| SmcEngine.mqh | //| Copyright 2026, Allan Niñal | //| https://www.allanninal.dev/mql5 | //+------------------------------------------------------------------+ //| Smart-money zone engine: swings, BOS/CHoCH, order blocks, fair | //| value gaps, liquidity sweeps, mitigation. A bar-by-bar state | //| machine: Step(i) uses bars 0..i only, so what it decided about | //| a bar never changes later. A zone's creation bar and its | //| mitigation bar are set once. Concepts: swing points, liquidity | //| sweeps, structure breaks, order block, fair value gap. | //+------------------------------------------------------------------+ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) #property strict #define B_OB_BULL_TOP 0 #define B_OB_BULL_BOT 1 #define B_OB_BEAR_TOP 2 #define B_OB_BEAR_BOT 3 #define B_FVG_BULL_TOP 4 #define B_FVG_BULL_BOT 5 #define B_FVG_BEAR_TOP 6 #define B_FVG_BEAR_BOT 7 #define B_STRUCT 8 // +1 BOS up, -1 BOS down, +2 CHoCH up, -2 CHoCH down #define B_LEVEL 9 // the broken swing's price #define B_SWEEP_BULL 10 // swing low taken, bar closed back above #define B_SWEEP_BEAR 11 #define B_MITIGATED 12 // zones mitigated on this bar #define B_TOUCHED 13 // live zones touched for the first time on this bar #define B_ACTIVE_BULL 14 // live bullish zones after this bar #define B_ACTIVE_BEAR 15 #define B_COUNT 16 #define ZK_OB 0 #define ZK_FVG 1 struct SSmcZone { int kind; int dir; double top; double bot; int t0; int born; int end; int touches; bool inside; }; struct SSmcSwing { int i; double p; int side; bool swept; bool broken; }; struct SSmcBreak { int dir; bool choch; double level; int frm; int at; }; struct SSmcSweep { int dir; double level; int frm; int at; }; class CSmcEngine { public: //--- settings int swing, atr_period, ob_mit, fvg_mit, sweep_look, s_start, s_end; double fvg_min, sweep_min; bool use_session; //--- results SSmcZone zones[]; SSmcBreak breaks[]; SSmcSweep sweeps[]; int nz, nb, ns; int done; // last bar processed, -1 = none private: SSmcSwing m_sw[]; int m_nsw; int m_last[2]; // newest swing per side: [0] highs, [1] lows int m_trend; int m_live[]; int m_nlive; double m_atr, m_atr_sum; int m_active[2]; // live zones: [0] bullish, [1] bearish double Far(const double &h[],const double &l[],const int i,const int d) { return(d>0 ? l[i] : h[i]); } double Near(const double &h[],const double &l[],const int i,const int d) { return(d>0 ? h[i] : l[i]); } bool Against(const double &o[],const double &c[],const int i,const int d) { return(d>0 ? c[i]o[i]); } bool InSession(const datetime t) { if(!use_session || s_start==s_end) return(true); MqlDateTime dt; TimeToStruct(t,dt); return(s_start=s_start && dt.hour=s_start || dt.hour0 && (h[p-k]>=h[p] || h[p+k]>h[p])) return(false); if(side<0 && (l[p-k]<=l[p] || l[p+k]0) return(mode==0 ? l<=z.top : mode==1 ? l<=mid+tol : mode==2 ? l<=z.bot : c=z.bot : mode==1 ? h>=mid-tol : mode==2 ? h>=z.top : c>z.top); } int AddZone(const int kind,const int dir,const double top,const double bot,const int t0,const int born) { if(nz>=ArraySize(zones)) ArrayResize(zones,nz+256); zones[nz].kind=kind; zones[nz].dir=dir; zones[nz].top=top; zones[nz].bot=bot; zones[nz].t0=t0; zones[nz].born=born; zones[nz].end=-1; zones[nz].touches=0; zones[nz].inside=false; if(m_nlive>=ArraySize(m_live)) ArrayResize(m_live,m_nlive+256); m_live[m_nlive++]=nz; m_active[dir>0 ? 0 : 1]++; return(nz++); } public: void Reset(void) { ArrayResize(zones,0,1024); ArrayResize(breaks,0,256); ArrayResize(sweeps,0,256); ArrayResize(m_sw,0,1024); ArrayResize(m_live,0,256); nz=nb=ns=m_nsw=m_nlive=0; m_last[0]=m_last[1]=-1; m_trend=0; m_atr=EMPTY_VALUE; m_atr_sum=0; m_active[0]=m_active[1]=0; done=-1; } //--- process closed bar i (bars 0..i are known); out[] gets the B_* values for it void Step(const int i,const datetime &t[],const double &o[],const double &h[],const double &l[],const double &c[],double &out[]) { ArrayInitialize(out,EMPTY_VALUE); done=i; //--- ATR: Wilder's smoothing of the true range, seeded with the SMA of the first atr_period values double tr=(i==0 ? h[i]-l[i] : MathMax(h[i]-l[i],MathMax(MathAbs(h[i]-c[i-1]),MathAbs(l[i]-c[i-1])))); if(i=atr_period-1 && a>0) { bool sess=InSession(t[i]); //--- 1. zones made before this bar: mitigated or touched int n_mit=0,n_touch=0,keep=0; for(int q=0; q=i) { m_live[keep++]=z; continue; } if(Mitigates(zones[z],zones[z].kind==ZK_OB ? ob_mit : fvg_mit,h[i],l[i],c[i])) { zones[z].end=i; n_mit++; m_active[zones[z].dir>0 ? 0 : 1]--; continue; } bool in=(zones[z].dir>0 ? l[i]<=zones[z].top : h[i]>=zones[z].bot); if(in && !zones[z].inside) { zones[z].touches++; n_touch++; } zones[z].inside=in; m_live[keep++]=z; } m_nlive=keep; out[B_MITIGATED]=n_mit; out[B_TOUCHED]=n_touch; //--- 2. liquidity sweeps: a wick beyond an untouched swing that closes back inside for(int d=1; d>=-1; d-=2) { int best=-1; for(int k=m_nsw-1; k>=0; k--) { if(i-m_sw[k].i>sweep_look) break; if(m_sw[k].side!=-d || m_sw[k].swept) continue; if(Far(h,l,i,d)*d<(m_sw[k].p-d*sweep_min*a)*d) { m_sw[k].swept=true; if(c[i]*d>m_sw[k].p*d && (best<0 || m_sw[k].p*d=0 && sess) { out[d>0 ? B_SWEEP_BULL : B_SWEEP_BEAR]=m_sw[best].p; if(ns>=ArraySize(sweeps)) ArrayResize(sweeps,ns+128); sweeps[ns].dir=d; sweeps[ns].level=m_sw[best].p; sweeps[ns].frm=m_sw[best].i; sweeps[ns].at=i; ns++; } } //--- 3. fair value gap ending at this bar if(i>=2 && sess) for(int d=1; d>=-1; d-=2) { double gap_far=Near(h,l,i-2,d),gap_near=Far(h,l,i,d); if((gap_near-gap_far)*d>=fvg_min*a) { double top=MathMax(gap_far,gap_near),bot=MathMin(gap_far,gap_near); AddZone(ZK_FVG,d,top,bot,i-1,i); out[d>0 ? B_FVG_BULL_TOP : B_FVG_BEAR_TOP]=top; out[d>0 ? B_FVG_BULL_BOT : B_FVG_BEAR_BOT]=bot; } } //--- 4. structure break and its order block for(int d=1; d>=-1; d-=2) { int s=m_last[d>0 ? 0 : 1]; if(s<0 || m_sw[s].broken || c[i]*d<=m_sw[s].p*d) continue; m_sw[s].broken=true; bool choch=(m_trend==-d); m_trend=d; int org=m_sw[s].i; for(int j=m_sw[s].i; j<=i; j++) if(Far(h,l,j,d)*d=MathMax(0,org-3); j--) if(Against(o,c,j,d)) { ob=j; break; } if(ob<0) ob=org; double top=(d>0 ? h[ob] : MathMax(h[ob],h[org])); double bot=(d>0 ? MathMin(l[ob],l[org]) : l[ob]); if(sess) { AddZone(ZK_OB,d,top,bot,MathMin(ob,org),i); out[d>0 ? B_OB_BULL_TOP : B_OB_BEAR_TOP]=top; out[d>0 ? B_OB_BULL_BOT : B_OB_BEAR_BOT]=bot; } out[B_STRUCT]=d*(choch ? 2 : 1); out[B_LEVEL]=m_sw[s].p; if(nb>=ArraySize(breaks)) ArrayResize(breaks,nb+128); breaks[nb].dir=d; breaks[nb].choch=choch; breaks[nb].level=m_sw[s].p; breaks[nb].frm=m_sw[s].i; breaks[nb].at=i; nb++; } out[B_ACTIVE_BULL]=m_active[0]; out[B_ACTIVE_BEAR]=m_active[1]; } //--- 5. the swing `swing` bars back is known from the next bar on int p=i-swing; if(p>=swing) for(int side=1; side>=-1; side-=2) if(IsSwing(h,l,p,side)) { if(m_nsw>=ArraySize(m_sw)) ArrayResize(m_sw,m_nsw+256); m_sw[m_nsw].i=p; m_sw[m_nsw].p=(side>0 ? h[p] : l[p]); m_sw[m_nsw].side=side; m_sw[m_nsw].swept=false; m_sw[m_nsw].broken=false; m_last[side>0 ? 0 : 1]=m_nsw; m_nsw++; } } }; //+------------------------------------------------------------------+