//+------------------------------------------------------------------+ //| Exhaust.mqh | //| Copyright 2026, Allan NiƱal | //| https://www.allanninal.dev/mql5 | //+------------------------------------------------------------------+ //| Fibonacci-extension EXHAUSTION marker, one CLOSED bar per step | //| (no repaint), used by Harmonic Signal Alerts. | //| Spec: docs/SPEC.md ("Exhaustion marker"); tested against | //| exh_stream() in tests/reference.py. | //| | //| Swings: a pivot with `w` bars on each side (not strictly: equal | //| prices count), known `w` bars later, added in the order they are | //| confirmed to an alternating sequence (two highs in a row keep | //| the higher, two lows the lower). Whenever the sequence changes | //| and its last three points A-B-C are an impulse plus a pullback | //| (top case: A low, B high, C a higher low below B; the bottom case| //| mirrors it) a setup is armed on that bar with | //| E = C + 2.618 x (B - A). It is cancelled if price trades beyond C| //| first or after 300 bars. When price reaches E the setup is | //| touched; within 10 bars after the touch the first close beyond | //| the extreme of the previous 3 bars (top: below their lowest low) | //| is the exhaustion signal. A setup signals once; several setups | //| on one bar count once. Optional confluence: at least `conf` | //| earlier swing pivots (up to 250 bars before A) within 0.5 ATR(14)| //| of the 100 / 161.8 / 261.8 % levels. | //| The sequence is built bar by bar from confirmed pivots only, so | //| a later swing never removes a setup that was already armed. | //| Analysis marker, not a trading system: on 37 symbols, H1/H4/D1 | //| it showed no edge as a trade rule. | //+------------------------------------------------------------------+ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) #ifndef EXHAUST_MQH #define EXHAUST_MQH #define EX_N 128 // ring of recent bars (2 x w + 2 must stay below it) #define EX_ATRN 14 #define EX_LIFE 300 // bars a setup waits for the touch #define EX_WINDOW 10 // bars after the touch in which the signal must come #define EX_REF 3 // the signal closes beyond the extreme of this many previous bars #define EX_PRIOR 250 // confluence looks this many bars before A #define EX_STOPATR 0.25 // invalidation level beyond the extreme, x ATR(14) #define EX_MAXW 30 struct EXParams { int w; // pivot bars on each side (2 - EX_MAXW) int conf; // earlier swings needed near the levels (0 = not required) }; struct EXSwing { int bar,conf_bar,type; // pivot bar, bar it was confirmed on, 1 high / -1 low double px; }; struct EXPivot { int bar; double px; }; struct EXSetup { int s; // +1 top case (E above), -1 bottom case int ab,bb,cb,armed,touch; // touch = -1 until E is reached double ap,bp,cp,e,ext; }; struct EXEvent { int bar,dir; // signal bar; direction of the signal: -1 down (after a top), +1 up int ab,bb,cb,armed,touch; double ap,bp,cp,e,ext,stop; }; struct EXState { int cnt; // bars fed double h[EX_N],l[EX_N],c[EX_N]; EXSwing sq[3]; int nsq; EXPivot pv[]; int npv; EXSetup st[]; int nst; int armed_total; // setups armed so far bool fired; // the last bar gave a signal: ev EXEvent ev; int last_dir,last_bar; // the latest signal (engine bar), -1 none EXEvent last; }; void EXInit(EXState &s) { s.cnt=0; s.nsq=0; s.npv=0; s.nst=0; s.armed_total=0; s.fired=false; s.last_dir=0; s.last_bar=-1; ArrayResize(s.pv,0); // the reserve is given on the first growth (EXPivotIn, EXArm): a cell with the marker off holds no memory for it ArrayResize(s.st,0); } double EXH(const EXState &s,const int k) { return(s.h[k%EX_N]); } double EXL(const EXState &s,const int k) { return(s.l[k%EX_N]); } double EXC(const EXState &s,const int k) { return(s.c[k%EX_N]); } //--- ATR(14) = mean of the last 14 true ranges ending at bar k (iATR); EMPTY_VALUE before bar 13 double EXAtr(const EXState &s,const int k) { if(k0 ? (A.px0) { double lv[3]={C.px+sd*1.0*leg,C.px+sd*1.618*leg,C.px+sd*2.618*leg}; int near=0; for(int i=0; i=A.bar-EX_PRIOR && s.pv[i].bar0 && s.sq[s.nsq-1].type==typ) { if(typ>0 ? px>=s.sq[s.nsq-1].px : px<=s.sq[s.nsq-1].px) { s.sq[s.nsq-1].bar=j; s.sq[s.nsq-1].conf_bar=b; s.sq[s.nsq-1].px=px; changed=true; } } else if(!(s.nsq>0 && s.sq[s.nsq-1].bar==j)) { if(s.nsq==3) { s.sq[0]=s.sq[1]; s.sq[1]=s.sq[2]; s.nsq--; } s.sq[s.nsq].bar=j; s.sq[s.nsq].conf_bar=b; s.sq[s.nsq].px=px; s.sq[s.nsq].type=typ; s.nsq++; changed=true; } if(changed) EXArm(s,p,b); } //--- feed one CLOSED bar; true when it gave an exhaustion signal (s.ev) bool EXFeed(EXState &s,const EXParams &p,const double high,const double low,const double close) { int b=s.cnt; s.h[b%EX_N]=high; s.l[b%EX_N]=low; s.c[b%EX_N]=close; s.cnt++; s.fired=false; //--- pivot confirmed on this bar: j = b - w int j=b-p.w; if(j>=p.w) { double mh=EXH(s,j),ml=EXL(s,j); for(int i=j-p.w; i<=b; i++) { if(EXH(s,i)>mh) mh=EXH(s,i); if(EXL(s,i)0) // pivots older than any later A - 250 are never read again { int lo=s.sq[0].bar-EX_PRIOR,m=0; for(int i=0; i=lo) s.pv[m++]=s.pv[i]; s.npv=m; } //--- setups take this bar, in the order they were armed double atr=EXAtr(s,b); int w=0; for(int i=0; ist.armed+EX_LIFE || (sg>0 ? lowst.cp)) continue; if(sg>0 ? high>=st.e : low<=st.e) { st.touch=b; st.ext=(sg>0 ? high : low); } s.st[w++]=st; continue; } st.ext=(sg>0 ? MathMax(st.ext,high) : MathMin(st.ext,low)); if(b>st.touch+EX_WINDOW) continue; double ref=(sg>0 ? EXL(s,b-1) : EXH(s,b-1)); for(int q=2; q<=EX_REF; q++) ref=(sg>0 ? MathMin(ref,EXL(s,b-q)) : MathMax(ref,EXH(s,b-q))); if(sg>0 ? closeref) { if(!s.fired && atr!=EMPTY_VALUE) { s.fired=true; s.ev.bar=b; s.ev.dir=-sg; s.ev.ab=st.ab; s.ev.bb=st.bb; s.ev.cb=st.cb; s.ev.armed=st.armed; s.ev.touch=st.touch; s.ev.ap=st.ap; s.ev.bp=st.bp; s.ev.cp=st.cp; s.ev.e=st.e; s.ev.ext=st.ext; s.ev.stop=st.ext+sg*EX_STOPATR*atr; } continue; // a setup signals once } s.st[w++]=st; } s.nst=w; if(s.fired) { s.last=s.ev; s.last_dir=s.ev.dir; s.last_bar=b; } return(s.fired); } #endif //+------------------------------------------------------------------+