forked from animatedread/Warrior_EA
BLUEPRINT.md reviews the feature/label layers and designs fractional differencing, Garman-Klass / Yang-Zhang targets and a 48-72h expansion label; mql5_patches/ holds the MQL5 side (FFD safe past the 1024-bar series ceiling, vol estimators, the label + veto gate, NY-time swap window). premise_test.py measured the premise on real broker bars: the headline AUC 0.75 was a day-of-week / path-length artifact (a Friday-clipped path is shorter, so it touches K*ATR less). Honest residual 0.56-0.62, mostly within noise once overlap is deflated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.6 KiB
MQL5
180 lines
7.6 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| VolEstimators.mqh |
|
|
//| Garman-Klass / Parkinson / Rogers-Satchell / Yang-Zhang |
|
|
//+------------------------------------------------------------------+
|
|
//| sigma2_GK = 0.5*(ln(H/L))^2 - (2ln2 - 1)*(ln(C/O))^2 |
|
|
//| |
|
|
//| NON-NEGATIVE BY CONSTRUCTION for any valid bar: H >= max(O,C) and |
|
|
//| L <= min(O,C) give ln(H/L) >= |ln(C/O)|, so |
|
|
//| GK >= (0.5 - 0.3863)*ln(C/O)^2 = 0.1137*ln(C/O)^2 >= 0. |
|
|
//| Do NOT add a clamp and call it a safety net: on a CORRUPT bar |
|
|
//| (H < L, or an EMPTY_VALUE reading as DBL_MAX) GK does not go |
|
|
//| negative - it returns a plausible POSITIVE variance. The only |
|
|
//| defence is validating O/H/L/C, which is what GKValid() does and |
|
|
//| what every entry point here calls first. |
|
|
//| |
|
|
//| GK IS BLIND TO GAPS. It assumes the bar opened where the previous |
|
|
//| one closed. Index CFDs gap across the daily maintenance break and |
|
|
//| the weekend, so on D1 GK UNDERSTATES the variance a multi-day |
|
|
//| hold is actually exposed to. For sizing an intra-week hold use |
|
|
//| YangZhangSigma(), which prices the gap explicitly. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef VOLESTIMATORS_MQH
|
|
#define VOLESTIMATORS_MQH
|
|
|
|
#define VOL_LN2 0.69314718055994531
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Bar validity. Rejects the EMPTY_VALUE/DBL_MAX case explicitly - |
|
|
//| it passes MathIsValidNumber and would poison every estimator. |
|
|
//+------------------------------------------------------------------+
|
|
bool GKValid(const double o, const double h, const double l, const double c)
|
|
{
|
|
if(!MathIsValidNumber(o) || !MathIsValidNumber(h) ||
|
|
!MathIsValidNumber(l) || !MathIsValidNumber(c))
|
|
return false;
|
|
if(o == EMPTY_VALUE || h == EMPTY_VALUE || l == EMPTY_VALUE || c == EMPTY_VALUE)
|
|
return false;
|
|
if(o <= 0.0 || h <= 0.0 || l <= 0.0 || c <= 0.0)
|
|
return false;
|
|
//--- the inversion that produces a plausible positive variance
|
|
if(h < l)
|
|
return false;
|
|
if(h < o || h < c || l > o || l > c)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
//--- Per-bar GK variance. Returns false (not 0.0) on a bad bar, so a
|
|
//--- caller can drop the bar instead of averaging a fabricated zero in.
|
|
bool GarmanKlassVar(const double o, const double h, const double l,
|
|
const double c, double &v)
|
|
{
|
|
if(!GKValid(o, h, l, c))
|
|
return false;
|
|
double hl = MathLog(h / l);
|
|
double co = MathLog(c / o);
|
|
v = 0.5 * hl * hl - (2.0 * VOL_LN2 - 1.0) * co * co;
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Rolling GK sigma over `window` bars ending at `shift`. |
|
|
//| |
|
|
//| Reads via Copy* directly rather than the stdlib series wrappers: |
|
|
//| a 48-72h window on H4 is 12-18 bars so the 1024 ceiling is not in |
|
|
//| play here, but keeping one convention across the vol/FFD layer |
|
|
//| removes the chance of a later window quietly crossing it. |
|
|
//+------------------------------------------------------------------+
|
|
bool GarmanKlassSigma(const string symbol, const ENUM_TIMEFRAMES period,
|
|
const int shift, const int window, double &sigma)
|
|
{
|
|
if(window <= 0 || shift < 0)
|
|
return false;
|
|
double o[], h[], l[], c[];
|
|
if(CopyOpen(symbol, period, shift, window, o) != window ||
|
|
CopyHigh(symbol, period, shift, window, h) != window ||
|
|
CopyLow(symbol, period, shift, window, l) != window ||
|
|
CopyClose(symbol, period, shift, window, c) != window)
|
|
return false;
|
|
double acc = 0.0;
|
|
int used = 0;
|
|
for(int i = 0; i < window; i++)
|
|
{
|
|
double v;
|
|
if(!GarmanKlassVar(o[i], h[i], l[i], c[i], v))
|
|
continue; // drop the bar, do not zero it
|
|
acc += v;
|
|
used++;
|
|
}
|
|
//--- Refuse a reading built from a thin remnant: a "volatility" averaged
|
|
//--- over 3 of 18 bars is not the same statistic and must not be gated on.
|
|
if(used < (window + 1) / 2)
|
|
return false;
|
|
sigma = MathSqrt(MathMax(acc / used, 0.0));
|
|
return true;
|
|
}
|
|
|
|
//--- Parkinson: high-low only, no drift term. Useful as a cross-check when
|
|
//--- GK's close-open term behaves oddly on a gappy series.
|
|
bool ParkinsonSigma(const string symbol, const ENUM_TIMEFRAMES period,
|
|
const int shift, const int window, double &sigma)
|
|
{
|
|
if(window <= 0 || shift < 0)
|
|
return false;
|
|
double h[], l[];
|
|
if(CopyHigh(symbol, period, shift, window, h) != window ||
|
|
CopyLow(symbol, period, shift, window, l) != window)
|
|
return false;
|
|
double acc = 0.0;
|
|
int used = 0;
|
|
for(int i = 0; i < window; i++)
|
|
{
|
|
if(!MathIsValidNumber(h[i]) || !MathIsValidNumber(l[i]) ||
|
|
h[i] == EMPTY_VALUE || l[i] == EMPTY_VALUE || h[i] <= 0.0 || l[i] <= 0.0 || h[i] < l[i])
|
|
continue;
|
|
double hl = MathLog(h[i] / l[i]);
|
|
acc += hl * hl / (4.0 * VOL_LN2);
|
|
used++;
|
|
}
|
|
if(used < (window + 1) / 2)
|
|
return false;
|
|
sigma = MathSqrt(acc / used);
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Yang-Zhang: overnight gap variance + open-close variance + the |
|
|
//| Rogers-Satchell drift-robust term. THE ONE TO GATE ON for a |
|
|
//| multi-day CFD hold, because the gap it prices is the risk the |
|
|
//| position actually carries overnight. |
|
|
//+------------------------------------------------------------------+
|
|
bool YangZhangSigma(const string symbol, const ENUM_TIMEFRAMES period,
|
|
const int shift, const int window, double &sigma,
|
|
const double kAlpha = 1.34)
|
|
{
|
|
if(window < 2 || shift < 0)
|
|
return false;
|
|
//--- one extra OLDER bar: the first overnight term needs the prior close
|
|
int need = window + 1;
|
|
double o[], h[], l[], c[];
|
|
if(CopyOpen(symbol, period, shift, need, o) != need ||
|
|
CopyHigh(symbol, period, shift, need, h) != need ||
|
|
CopyLow(symbol, period, shift, need, l) != need ||
|
|
CopyClose(symbol, period, shift, need, c) != need)
|
|
return false;
|
|
//--- Copy* returns oldest-first; index 0 is the OLDEST of the need bars.
|
|
double sumO = 0.0, sumC = 0.0, sumRS = 0.0;
|
|
double oc[], co[];
|
|
ArrayResize(oc, window);
|
|
ArrayResize(co, window);
|
|
int used = 0;
|
|
for(int i = 1; i < need; i++)
|
|
{
|
|
if(!GKValid(o[i], h[i], l[i], c[i]) || !GKValid(o[i-1], h[i-1], l[i-1], c[i-1]))
|
|
return false; // YZ needs a contiguous run, not a subset
|
|
oc[used] = MathLog(o[i] / c[i - 1]); // overnight
|
|
co[used] = MathLog(c[i] / o[i]); // open-to-close
|
|
sumO += oc[used];
|
|
sumC += co[used];
|
|
sumRS += MathLog(h[i] / c[i]) * MathLog(h[i] / o[i]) +
|
|
MathLog(l[i] / c[i]) * MathLog(l[i] / o[i]);
|
|
used++;
|
|
}
|
|
if(used < 2)
|
|
return false;
|
|
double mO = sumO / used, mC = sumC / used;
|
|
double vO = 0.0, vC = 0.0;
|
|
for(int i = 0; i < used; i++)
|
|
{
|
|
vO += (oc[i] - mO) * (oc[i] - mO);
|
|
vC += (co[i] - mC) * (co[i] - mC);
|
|
}
|
|
vO /= (used - 1);
|
|
vC /= (used - 1);
|
|
double vRS = sumRS / used;
|
|
double k = kAlpha / (kAlpha + (used + 1.0) / (used - 1.0));
|
|
sigma = MathSqrt(MathMax(vO + k * vC + (1.0 - k) * vRS, 0.0));
|
|
return true;
|
|
}
|
|
#endif // VOLESTIMATORS_MQH
|