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