| //+------------------------------------------------------------------+
|
| //| 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(k<EX_ATRN-1)
|
| return(EMPTY_VALUE);
|
| double sum=0;
|
| for(int i=k-EX_ATRN+1; i<=k; i++)
|
| {
|
| double t=(i==0 ? EXH(s,i)-EXL(s,i) :
|
| MathMax(EXH(s,i)-EXL(s,i),MathMax(MathAbs(EXH(s,i)-EXC(s,i-1)),MathAbs(EXL(s,i)-EXC(s,i-1)))));
|
| sum+=t;
|
| }
|
| return(sum/EX_ATRN);
|
| }
|
|
|
| //--- the sequence changed on bar b: arm a setup when A-B-C is an impulse plus a pullback
|
| void EXArm(EXState &s,const EXParams &p,const int b)
|
| {
|
| if(s.nsq<3)
|
| return;
|
| EXSwing A=s.sq[0],B=s.sq[1],C=s.sq[2];
|
| int sd=B.type;
|
| if(!(sd>0 ? (A.px<C.px && C.px<B.px) : (B.px<C.px && C.px<A.px)))
|
| return;
|
| double atr=EXAtr(s,b);
|
| if(atr==EMPTY_VALUE)
|
| return;
|
| double leg=MathAbs(B.px-A.px);
|
| if(p.conf>0)
|
| {
|
| 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<s.npv; i++)
|
| if(s.pv[i].bar>=A.bar-EX_PRIOR && s.pv[i].bar<A.bar)
|
| {
|
| double d=MathMin(MathAbs(s.pv[i].px-lv[0]),MathMin(MathAbs(s.pv[i].px-lv[1]),MathAbs(s.pv[i].px-lv[2])));
|
| if(d<=0.5*atr)
|
| near++;
|
| }
|
| if(near<p.conf)
|
| return;
|
| }
|
| int k=s.nst;
|
| ArrayResize(s.st,k+1,64);
|
| s.st[k].s=sd; s.st[k].ab=A.bar; s.st[k].bb=B.bar; s.st[k].cb=C.bar; s.st[k].armed=b; s.st[k].touch=-1;
|
| s.st[k].ap=A.px; s.st[k].bp=B.px; s.st[k].cp=C.px; s.st[k].e=C.px+sd*2.618*leg; s.st[k].ext=0;
|
| s.nst++;
|
| s.armed_total++;
|
| }
|
|
|
| //--- one confirmed pivot (type, bar j, price px) confirmed on bar b
|
| void EXPivotIn(EXState &s,const EXParams &p,const int typ,const int j,const double px,const int b)
|
| {
|
| int k=s.npv;
|
| ArrayResize(s.pv,k+1,512);
|
| s.pv[k].bar=j; s.pv[k].px=px;
|
| s.npv++;
|
| bool changed=false;
|
| if(s.nsq>0 && 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)<ml) ml=EXL(s,i);
|
| }
|
| if(EXH(s,j)==mh)
|
| EXPivotIn(s,p,1,j,EXH(s,j),b);
|
| if(EXL(s,j)==ml)
|
| EXPivotIn(s,p,-1,j,EXL(s,j),b);
|
| }
|
| if(s.nsq>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<s.npv; i++)
|
| if(s.pv[i].bar>=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; i<s.nst; i++)
|
| {
|
| EXSetup st=s.st[i];
|
| int sg=st.s;
|
| if(st.touch<0)
|
| {
|
| if(b<=st.armed)
|
| { s.st[w++]=st; continue; }
|
| if(b>st.armed+EX_LIFE || (sg>0 ? low<st.cp : high>st.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 ? close<ref : close>ref)
|
| {
|
| 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
|
| //+------------------------------------------------------------------+
|