harmonic-signal-alerts/mql5/Indicators/HarmonicSignalAlerts/Exhaust.mqh
262 lines
9.3 KiB
MQL5
//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+