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
| |||
//+------------------------------------------------------------------+
|