293 lines
11 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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++;
| |||
}
| |||
}
| |||
};
| |||
//+------------------------------------------------------------------+
|