180 行
7.6 KiB
MQL5
180 行
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
|