divergence-signal-alerts/mql5/Include/Divergence/Div.mqh
289 lines
9.7 KiB
MQL5
//+------------------------------------------------------------------+
//| Div.mqh |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| Divergence engine, one CLOSED bar per step (no repaint), used by |
//| Divergence Signal Alerts. |
//| |
//| Pivots on price: a pivot low at bar p has a low below each of |
//| the `left` bars before it and not above any of the `right` bars |
//| after it, so it is known `right` bars later (the confirmation |
//| bar). Pivot highs mirror it. |
//| At a new pivot low p, the previous pivot lows q (nearest first, |
//| up to 3, `min_dist`..`max_dist` bars back) are tried: |
//| regular bullish: low[p] < low[q] and oscillator[p] > osc[q]; |
//| hidden bullish: low[p] > low[q] and oscillator[p] < osc[q]. |
//| Bearish mirrors it on pivot highs. With "clean lines" on, no bar |
//| between q and p may cross the straight line q-p, on price or on |
//| that oscillator. The first q where at least `min_count` |
//| oscillators diverge is the result; `mask` says which ones. |
//| |
//| Oscillators (textbook formulas, tested against tests/reference.py):|
//| RSI Wilder, close |
//| MACD histogram = EMA(fast) - EMA(slow) - EMA(signal) of that |
//| EMAs and RMAs are seeded with the SMA of their first n values. |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#ifndef DIV_MQH
#define DIV_MQH
#define DV_N 160 // ring of recent bars (max_dist + left + right must stay below it)
#define DV_RSI 0
#define DV_MACD 1
#define DV_OSC 2
#define DV_PIV 4 // previous pivots remembered per side
//--- EMA seeded with the SMA of the first n values (as SignalDashboard Calc.mqh)
struct DVEma
{
int cnt;
double sum;
double v;
};
void DVEmaInit(DVEma &s) { s.cnt=0; s.sum=0.0; s.v=EMPTY_VALUE; }
double DVEmaStep(DVEma &s,const int n,const double x)
{
s.cnt++;
if(s.cnt<n)
{ s.sum+=x; s.v=EMPTY_VALUE; }
else
if(s.cnt==n)
{ s.sum+=x; s.v=s.sum/n; }
else
s.v=s.v+2.0/(n+1)*(x-s.v);
return(s.v);
}
struct DVParams
{
bool use[DV_OSC];
int rsi_n;
int macd_fast,macd_slow,macd_sig;
int left,right,min_dist,max_dist;
bool regular,hidden;
bool clean; // no bar may cross the q-p line, on price or the oscillator
int min_count; // oscillators that must agree
};
struct DVState
{
int cnt; // bars fed; bar k is the k-th fed bar (0-based)
double hi[DV_N],lo[DV_N];
double osc[DV_OSC*DV_N]; // osc[o*DV_N + k%DV_N]
//--- RSI (Wilder)
double prev_close,rsi_sg,rsi_sl,rsi_gain,rsi_loss;
//--- MACD
DVEma ef,es,esg;
//--- previous pivots: absolute bar numbers, newest first
int pl[DV_PIV],ph[DV_PIV];
int npl,nph;
//--- the event the last fed bar confirmed
int ev_dir; // +1 bullish, -1 bearish, 0 none
int ev_type; // 1 regular, 2 hidden
int ev_mask; // bit o set = oscillator o diverged
int ev_count;
int ev_p,ev_q; // pivot bars (absolute), newer and older
double ev_ppx,ev_qpx; // their prices (low for bullish, high for bearish)
};
int DVi(const int k) { return(k%DV_N); }
double DVOsc(const DVState &s,const int o,const int k) { return(s.osc[o*DV_N+DVi(k)]); }
void DVInit(DVState &s)
{
s.cnt=0;
ArrayInitialize(s.hi,0);
ArrayInitialize(s.lo,0);
ArrayInitialize(s.osc,EMPTY_VALUE);
s.prev_close=0; s.rsi_sg=0; s.rsi_sl=0; s.rsi_gain=0; s.rsi_loss=0;
DVEmaInit(s.ef); DVEmaInit(s.es); DVEmaInit(s.esg);
s.npl=0; s.nph=0;
for(int i=0; i<DV_PIV; i++)
{ s.pl[i]=-1; s.ph[i]=-1; }
s.ev_dir=0; s.ev_type=0; s.ev_mask=0; s.ev_count=0; s.ev_p=-1; s.ev_q=-1; s.ev_ppx=0; s.ev_qpx=0;
}
//--- the oscillators on the new bar k
void DVOscStep(DVState &s,const DVParams &p,const int k,const double h,const double l,const double c,const double vol)
{
int r=DVi(k);
for(int o=0; o<DV_OSC; o++)
s.osc[o*DV_N+r]=EMPTY_VALUE;
//--- RSI (Wilder, as SignalDashboard RsiStep)
if(k>=1)
{
double ch=c-s.prev_close,g=MathMax(ch,0.0),ls=MathMax(-ch,0.0);
int n=p.rsi_n;
if(k<n)
{ s.rsi_sg+=g; s.rsi_sl+=ls; }
else
if(k==n)
{ s.rsi_sg+=g; s.rsi_sl+=ls; s.rsi_gain=s.rsi_sg/n; s.rsi_loss=s.rsi_sl/n; }
else
{ s.rsi_gain=(s.rsi_gain*(n-1)+g)/n; s.rsi_loss=(s.rsi_loss*(n-1)+ls)/n; }
if(k>=n)
s.osc[DV_RSI*DV_N+r]=(s.rsi_loss==0.0 ? 100.0 : (s.rsi_gain==0.0 ? 0.0 : 100.0-100.0/(1.0+s.rsi_gain/s.rsi_loss)));
}
//--- MACD histogram
double ef=DVEmaStep(s.ef,p.macd_fast,c),es=DVEmaStep(s.es,p.macd_slow,c);
if(ef!=EMPTY_VALUE && es!=EMPTY_VALUE)
{
double m=ef-es,sg=DVEmaStep(s.esg,p.macd_sig,m);
if(sg!=EMPTY_VALUE)
s.osc[DV_MACD*DV_N+r]=m-sg;
}
s.prev_close=c;
}
//--- is bar p (absolute) a pivot low / high, judged on the bars up to k = p + right?
bool DVPivotLow(const DVState &s,const DVParams &pr,const int p)
{
if(p-pr.left<0)
return(false);
double v=s.lo[DVi(p)];
for(int j=1; j<=pr.left; j++)
if(!(v<s.lo[DVi(p-j)]))
return(false);
for(int j=1; j<=pr.right; j++)
if(v>s.lo[DVi(p+j)])
return(false);
return(true);
}
bool DVPivotHigh(const DVState &s,const DVParams &pr,const int p)
{
if(p-pr.left<0)
return(false);
double v=s.hi[DVi(p)];
for(int j=1; j<=pr.left; j++)
if(!(v>s.hi[DVi(p-j)]))
return(false);
for(int j=1; j<=pr.right; j++)
if(v<s.hi[DVi(p+j)])
return(false);
return(true);
}
//--- no bar strictly between q and p crosses the straight line q-p: below it for lows (bull), above it for highs (bear)
bool DVClean(const DVState &s,const int o,const int q,const int p,const int dir)
{
double a=(o<0 ? (dir>0 ? s.lo[DVi(q)] : s.hi[DVi(q)]) : DVOsc(s,o,q));
double b=(o<0 ? (dir>0 ? s.lo[DVi(p)] : s.hi[DVi(p)]) : DVOsc(s,o,p));
for(int j=q+1; j<p; j++)
{
double line=a+(b-a)*(j-q)/(double)(p-q);
double v=(o<0 ? (dir>0 ? s.lo[DVi(j)] : s.hi[DVi(j)]) : DVOsc(s,o,j));
if(v==EMPTY_VALUE)
return(false);
if(dir>0 ? v<line : v>line)
return(false);
}
return(true);
}
//--- divergences at the new pivot p (dir +1 low, -1 high); fills ev_*
void DVCheck(DVState &s,const DVParams &pr,const int p,const int dir)
{
int n=(dir>0 ? s.npl : s.nph);
for(int i=0; i<n; i++)
{
int q=(dir>0 ? s.pl[i] : s.ph[i]);
int d=p-q;
if(d<pr.min_dist)
continue;
if(d>pr.max_dist)
break; // newest first: the rest are older still
double pp=(dir>0 ? s.lo[DVi(p)] : s.hi[DVi(p)]),pq=(dir>0 ? s.lo[DVi(q)] : s.hi[DVi(q)]);
int type=0;
if(dir>0)
type=(pp<pq ? 1 : (pp>pq ? 2 : 0));
else
type=(pp>pq ? 1 : (pp<pq ? 2 : 0));
if(type==0 || (type==1 && !pr.regular) || (type==2 && !pr.hidden))
continue;
if(pr.clean && !DVClean(s,-1,q,p,dir))
continue;
int mask=0,cnt=0;
for(int o=0; o<DV_OSC; o++)
{
if(!pr.use[o])
continue;
double op=DVOsc(s,o,p),oq=DVOsc(s,o,q);
if(op==EMPTY_VALUE || oq==EMPTY_VALUE)
continue;
//--- regular bull: osc higher low; hidden bull: osc lower low; bear mirrors (sign flip)
bool div=(type==1 ? dir*(op-oq)>0 : dir*(op-oq)<0);
if(!div)
continue;
if(pr.clean && !DVClean(s,o,q,p,dir))
continue;
mask|=(1<<o);
cnt++;
}
if(cnt>=MathMax(1,pr.min_count))
{
s.ev_dir=dir; s.ev_type=type; s.ev_mask=mask; s.ev_count=cnt;
s.ev_p=p; s.ev_q=q; s.ev_ppx=pp; s.ev_qpx=pq;
return;
}
}
}
void DVRemember(int &arr[],int &n,const int p)
{
for(int i=DV_PIV-1; i>0; i--)
arr[i]=arr[i-1];
arr[0]=p;
n=MathMin(n+1,DV_PIV);
}
//--- feed one CLOSED bar; true when it confirmed a divergence (ev_*)
bool DVFeed(DVState &s,const DVParams &pr,const double h,const double l,const double c,const double vol)
{
int k=s.cnt;
s.hi[DVi(k)]=h;
s.lo[DVi(k)]=l;
DVOscStep(s,pr,k,h,l,c,vol);
s.cnt++;
s.ev_dir=0; s.ev_type=0; s.ev_mask=0; s.ev_count=0; s.ev_p=-1; s.ev_q=-1;
int p=k-pr.right;
if(p<0)
return(false);
if(DVPivotLow(s,pr,p))
{
DVCheck(s,pr,p,1);
DVRemember(s.pl,s.npl,p);
}
if(s.ev_dir==0 && DVPivotHigh(s,pr,p))
{
DVCheck(s,pr,p,-1);
DVRemember(s.ph,s.nph,p);
}
else
if(s.ev_dir!=0 && DVPivotHigh(s,pr,p)) // a bar can be both; keep the high for later pivots
DVRemember(s.ph,s.nph,p);
if(s.ev_dir==0)
return(false);
return(true);
}
string DVOscName(const int o)
{
switch(o)
{
case DV_RSI: return("RSI");
case DV_MACD: return("MACD");
default: return("?");
}
}
string DVMaskNames(const int mask)
{
string s="";
for(int o=0; o<DV_OSC; o++)
if((mask & (1<<o))!=0)
s+=(s=="" ? "" : "+")+DVOscName(o);
return(s);
}
#endif
//+------------------------------------------------------------------+