//+------------------------------------------------------------------+ //| DzEngine.mqh | //| Copyright 2026, Allan Niñal | //| https://www.allanninal.dev/mql5 | //+------------------------------------------------------------------+ //| Displacement zone engine. A bar-by-bar state machine: Step(i) | //| uses bars 0..i only, and what it decided about a bar never | //| changes later. Rules (d = +1 demand, -1 supply): | //| ATR Wilder's smoothing of the true range, seeded with the | //| SMA of the first `atr_period` values. The displacement | //| test uses the ATR as it was BEFORE the bar (bar i-1). | //| swing bar p is a swing high when the `swing` bars on its left| //| have lower highs and the `swing` bars on its right | //| highs not above it (mirror for lows). Known at p+swing.| //| break the first close beyond the newest swing high (low). | //| That close uses the swing up, with or without a zone. | //| impulse the break bar must be a displacement candle: range at | //| least disp_mult x ATR, body at least body_pct % of the | //| range, closing in the break's direction. | //| base base_mode 0: the nearest candle of the other colour | //| among the `search` bars before the break bar. | //| base_mode 1: where the move started: the lowest low | //| (demand; supply: highest high, latest bar on a tie) of | //| those bars, then the nearest candle of the other | //| colour at or before it, in the same bars. None: no zone| //| zone 0 full candle, 1 body, 2 mixed (demand: low to body | //| top, supply: body bottom to high). | //| states fresh -> tested (price reaches the near edge, middle | //| or far edge, from the bar after the zone was made) -> | //| broken (a close or a wick beyond the far edge; checked | //| first, so a bar that does both only breaks it). Each | //| later re-entry after a bar outside counts a retest. | //| cap at most `max_live` live zones per side (0 = no limit): | //| the oldest live one of that side is dropped. | //| An analysis tool: it describes zones, it does not trade. | //+------------------------------------------------------------------+ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) #property strict #ifndef DZENGINE_MQH #define DZENGINE_MQH #define DZ_B_DEM_TOP 0 #define DZ_B_DEM_BOT 1 #define DZ_B_SUP_TOP 2 #define DZ_B_SUP_BOT 3 #define DZ_B_BREAK_UP 4 // the swing high broken on this bar (price) #define DZ_B_BREAK_DN 5 // the swing low broken on this bar (price) #define DZ_B_TESTED 6 // zones tested for the first time on this bar #define DZ_B_RETESTS 7 // retests of already tested zones on this bar #define DZ_B_BROKEN 8 // zones broken on this bar #define DZ_B_LIVE_DEM 9 // live demand zones after this bar #define DZ_B_LIVE_SUP 10 // live supply zones after this bar #define DZ_B_DEM_BASE 11 // bars from the break bar back to the base candle of a new demand zone #define DZ_B_SUP_BASE 12 #define DZ_B_TREND 13 // validated trend after this bar: +1 up, -1 down #define DZ_B_COUNT 14 #define DZ_FRESH 0 #define DZ_TESTED 1 #define DZ_BROKEN 2 #define DZ_DROPPED 3 struct SDzZone { int dir; double top,bot,mid; int t0; // base candle int born; // break bar (the zone exists from the next bar on) int state; int test_bar; // first test, -1 none int end; // broken or dropped, -1 live int retests; // entries counted: 1 = the first test int entry; // bar of the latest entry (first test or retest), -1 none bool inside; double atr; // ATR before the break bar double disp; // break bar range / atr int trend; // validated trend on the break bar (+1 / -1) double rr; // reward-to-risk of the zone as a setup (0 when the risk is not above 0) bool ok; // passes the switched-on filters (always true when both are off) }; struct SDzSwing { int i; double p; bool broken; }; struct SDzBreak { int dir; double level; int frm; int at; int zone; }; class CDzEngine { public: //--- settings int swing,atr_period,search,zone_mode,test_mode,break_mode,max_live,base_mode; double disp_mult,body_pct,min_rr; bool trend_only; //--- results SDzZone zones[]; SDzBreak breaks[]; int nz,nb; int done; // last bar processed, -1 = none private: SDzSwing m_sw[2]; // newest swing: [0] high, [1] low int m_live[]; int m_nlive; double m_atr,m_atr_sum; int m_active[2]; int m_tr; // validated trend: +1 up, -1 down double m_vlow,m_vhigh,m_H,m_P,m_L,m_Q; 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]0) return(test_mode==0 ? l<=z.top : test_mode==1 ? l<=z.mid+tol : l<=z.bot); return(test_mode==0 ? h>=z.bot : test_mode==1 ? h>=z.mid-tol : h>=z.top); } bool Breaks(const SDzZone &z,const double h,const double l,const double c) { if(z.dir>0) return(break_mode==0 ? cz.top : h>z.top); } //--- validated market structure after closed bar i void TrendStep(const int i,const double &h[],const double &l[],const double &c[]) { if(i==0) { m_tr=1; m_vlow=l[0]; m_vhigh=DBL_MAX; m_H=h[0]; m_P=DBL_MAX; m_L=l[0]; m_Q=-DBL_MAX; return; } if(m_tr>0) { if(c[i]m_H && m_Pm_H) { m_H=h[i]; m_P=DBL_MAX; } else m_P=MathMin(m_P,l[i]); } else { if(c[i]>m_vhigh) { m_tr=1; m_vlow=m_L; m_H=h[i]; m_P=DBL_MAX; } else if(c[i]-DBL_MAX) { m_vhigh=m_Q; m_L=l[i]; m_Q=-DBL_MAX; } else if(l[i]0 ? 0 : 1)!=side) continue; zones[z].state=DZ_DROPPED; zones[z].end=i; for(int k=q; k=ArraySize(zones)) ArrayResize(zones,nz+256); zones[nz].dir=dir; zones[nz].top=top; zones[nz].bot=bot; zones[nz].mid=(top+bot)/2.0; zones[nz].t0=t0; zones[nz].born=born; zones[nz].state=DZ_FRESH; zones[nz].test_bar=-1; zones[nz].end=-1; zones[nz].retests=0; zones[nz].entry=-1; zones[nz].inside=false; zones[nz].atr=atr; zones[nz].disp=disp; zones[nz].trend=m_tr; zones[nz].rr=0; zones[nz].ok=true; if(m_nlive>=ArraySize(m_live)) ArrayResize(m_live,m_nlive+256); m_live[m_nlive++]=nz; int side=(dir>0 ? 0 : 1); m_active[side]++; if(max_live>0 && m_active[side]>max_live) Drop(side,born); return(nz++); } public: void Defaults(void) { swing=3; atr_period=14; search=12; zone_mode=0; test_mode=0; break_mode=0; max_live=12; base_mode=0; disp_mult=1.0; body_pct=55.0; trend_only=false; min_rr=0.0; } void Reset(void) { ArrayResize(zones,0,1024); ArrayResize(breaks,0,256); ArrayResize(m_live,0,256); nz=nb=m_nlive=0; for(int s=0; s<2; s++) { m_sw[s].i=-1; m_sw[s].p=0; m_sw[s].broken=true; } m_atr=EMPTY_VALUE; m_atr_sum=0; m_active[0]=m_active[1]=0; m_tr=1; m_vlow=0; m_vhigh=DBL_MAX; m_H=0; m_P=DBL_MAX; m_L=0; m_Q=-DBL_MAX; done=-1; } //--- live zones of one side that pass the filters (all live zones when both are off) int Live(const int dir) const { int n=0; for(int q=0; q0)==(dir>0)) n++; return(n); } int Trend(void) const { return(m_tr); } //--- the validated low (uptrend) or validated high (downtrend) double ValidLevel(void) const { return(m_tr>0 ? m_vlow : m_vhigh); } double Atr(void) const { return(m_atr); } //--- process closed bar i (bars 0..i are known); out[] gets the DZ_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; TrendStep(i,h,l,c); out[DZ_B_TREND]=m_tr; double prev=m_atr; 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(i0) { //--- 1. zones made before this bar: reaction window, broken, tested, retested int n_test=0,n_re=0,n_brk=0,keep=0; for(int q=0; q0 ? 0 : 1]--; continue; } if(reach) { if(zones[z].state==DZ_FRESH) { zones[z].state=DZ_TESTED; zones[z].test_bar=i; zones[z].retests=1; zones[z].entry=i; if(zones[z].ok) n_test++; } else if(!zones[z].inside) { zones[z].retests++; zones[z].entry=i; if(zones[z].ok) n_re++; } } zones[z].inside=reach; m_live[keep++]=z; } m_nlive=keep; out[DZ_B_TESTED]=n_test; out[DZ_B_RETESTS]=n_re; out[DZ_B_BROKEN]=n_brk; //--- 2. a break of the newest swing, and its zone for(int d=1; d>=-1; d-=2) { int s=(d>0 ? 0 : 1); if(m_sw[s].broken || c[i]*d<=m_sw[s].p*d) continue; m_sw[s].broken=true; out[d>0 ? DZ_B_BREAK_UP : DZ_B_BREAK_DN]=m_sw[s].p; double rng=h[i]-l[i],body=MathAbs(c[i]-o[i]); //--- a body of exactly body_pct % (common with decimal prices) counts: 1e-9 of the range absorbs the last-bit //--- difference between compilers in body_pct/100*rng bool impulse=(rng>0 && rng>=disp_mult*prev && body>=body_pct/100.0*rng-rng*1e-9 && (c[i]-o[i])*d>0); int base=-1,lo_j=MathMax(0,i-search),from_j=i-1; if(impulse && base_mode==1) for(int j=i-2; j>=lo_j; j--) // the move's start: the lowest low (highest high), latest on a tie if(d>0 ? l[j]h[from_j]) from_j=j; if(impulse) for(int j=from_j; j>=lo_j; j--) if((c[j]-o[j])*d<0) { base=j; break; } int zi=-1; if(base>=0) { double bt=MathMax(o[base],c[base]),bb=MathMin(o[base],c[base]); double top=(zone_mode==1 ? bt : (zone_mode==2 && d>0 ? bt : h[base])); double bot=(zone_mode==1 ? bb : (zone_mode==2 && d<0 ? bb : l[base])); double ht=top-bot; if(ht>0) { zi=AddZone(d,top,bot,base,i,prev,rng/prev); //--- reward-to-risk: entry at the near edge, stop beyond the far edge, target = the move's extreme double entry=(d>0 ? top : bot),stop=(d>0 ? bot-0.1*m_atr : top+0.1*m_atr),tgt=(d>0 ? h[base] : l[base]); for(int j=base+1; j<=i; j++) tgt=(d>0 ? MathMax(tgt,h[j]) : MathMin(tgt,l[j])); double risk=d*(entry-stop),reward=d*(tgt-entry); zones[zi].rr=(risk>0 ? reward/risk : 0.0); zones[zi].ok=!((trend_only && m_tr!=d) || (min_rr>0 && zones[zi].rr0 ? DZ_B_DEM_TOP : DZ_B_SUP_TOP]=top; out[d>0 ? DZ_B_DEM_BOT : DZ_B_SUP_BOT]=bot; out[d>0 ? DZ_B_DEM_BASE : DZ_B_SUP_BASE]=i-base; } } } if(nb>=ArraySize(breaks)) ArrayResize(breaks,nb+128); breaks[nb].dir=d; breaks[nb].level=m_sw[s].p; breaks[nb].frm=m_sw[s].i; breaks[nb].at=i; breaks[nb].zone=zi; nb++; } out[DZ_B_LIVE_DEM]=Live(1); out[DZ_B_LIVE_SUP]=Live(-1); } //--- 3. 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)) { int s=(side>0 ? 0 : 1); m_sw[s].i=p; m_sw[s].p=(side>0 ? h[p] : l[p]); m_sw[s].broken=false; } } }; #endif //+------------------------------------------------------------------+