smc-zones/mql5/Indicators/SMCZones/SmcEngine.mqh
Allan Niñal Allan Niñal 0fe965eb07 SMC Zones: free MT5 and MT4 indicator source, Python reference, tests and README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:04:52 +08:00
//+------------------------------------------------------------------+
//| 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] : 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_end ? (dt.hour>=s_start && dt.hour<s_end) : (dt.hour>=s_start || dt.hour<s_end));
}
bool IsSwing(const double &h[],const double &l[],const int p,const int side)
{
for(int k=1; k<=swing; k++)
{
if(side>0 && (h[p-k]>=h[p] || h[p+k]>h[p]))
return(false);
if(side<0 && (l[p-k]<=l[p] || l[p+k]<l[p]))
return(false);
}
return(true);
}
bool Mitigates(const SSmcZone &z,const int mode,const double h,const double l,const double c)
{
double mid=(z.top+z.bot)/2.0;
double tol=MathAbs(mid)*1e-10; // a wick exactly on the midpoint counts ((top+bot)/2 can be 1 ulp off)
if(z.dir>0)
return(mode==0 ? l<=z.top : mode==1 ? l<=mid+tol : mode==2 ? l<=z.bot : c<z.bot);
return(mode==0 ? h>=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)
m_atr_sum+=tr;
else
if(i==atr_period-1)
{
m_atr_sum+=tr;
m_atr=m_atr_sum/atr_period;
}
else
m_atr=(m_atr*(atr_period-1)+tr)/atr_period;
double a=m_atr;
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<m_nlive; q++)
{
int z=m_live[q];
if(zones[z].born>=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<m_sw[best].p*d))
best=k;
}
}
if(best>=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<Far(h,l,org,d)*d)
org=j;
int ob=-1;
if(Against(o,c,org,d))
{
ob=org;
while(ob+1<i && Against(o,c,ob+1,d))
ob++;
}
else
for(int j=org-1; j>=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++;
}
}
};
//+------------------------------------------------------------------+