forked from animatedread/Warrior_EA
128 lines
5.9 KiB
MQL5
128 lines
5.9 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| EdgeStats.mqh |
| |||
//| AnimateDread |
| |||
//| |
| |||
//| HOW SURE ARE WE, in Lopez de Prado's terms (Advances in Financial |
| |||
//| Machine Learning, ch. 8 and 14). Stateless math, no trading. |
| |||
//| |
| |||
//| Phi / PhiInv the normal CDF and its inverse |
| |||
//| PSR(SR*) Probabilistic Sharpe Ratio: the probability |
| |||
//| that the true Sharpe exceeds SR*, given the |
| |||
//| sample's length, skew and kurtosis. A Sharpe |
| |||
//| of 1 on 40 trades with fat tails is far less |
| |||
//| certain than the number suggests. |
| |||
//| DSR(N) Deflated Sharpe Ratio: PSR against the Sharpe |
| |||
//| the BEST of N independent trials would show by |
| |||
//| luck. N is how many configurations were tried |
| |||
//| to arrive at this one - honestly, including the |
| |||
//| ones that were discarded. |
| |||
//| |
| |||
//| The Sharpe here is PER TRADE (mean R / sd R), not annualised: PSR |
| |||
//| is defined on the sampling frequency of the returns it is given. |
| |||
//| The trial-Sharpe variance uses its large-sample form 1/(T-1) - the |
| |||
//| caller has one run, not N, to estimate it from. That makes DSR an |
| |||
//| approximation; it is reported as one. |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef WARRIOR_EDGESTATS_MQH
| |||
#define WARRIOR_EDGESTATS_MQH
| |||
| |||
class CEdgeStats
| |||
{
| |||
public:
| |||
//--- Abramowitz & Stegun 7.1.26, |error| < 1.5e-7.
| |||
static double Phi(const double x)
| |||
{
| |||
const double t = 1.0 / (1.0 + 0.2316419 * MathAbs(x));
| |||
const double d = 0.3989422804 * MathExp(-0.5 * x * x);
| |||
const double p = d * t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 +
| |||
t * (-1.821255978 + t * 1.330274429))));
| |||
return (x >= 0.0) ? 1.0 - p : p;
| |||
}
| |||
//--- Acklam's rational approximation, relative error < 1.2e-9.
| |||
static double PhiInv(const double p)
| |||
{
| |||
if(p <= 0.0 || p >= 1.0)
| |||
return 0.0;
| |||
static const double a[6] = {-3.969683028665376e+01, 2.209460984245205e+02,
| |||
-2.759285104469687e+02, 1.383577518672690e+02,
| |||
-3.066479806614716e+01, 2.506628277459239e+00};
| |||
static const double b[5] = {-5.447609879822406e+01, 1.615858368580409e+02,
| |||
-1.556989798598866e+02, 6.680131188771972e+01,
| |||
-1.328068155288572e+01};
| |||
static const double c[6] = {-7.784894002430293e-03, -3.223964580411365e-01,
| |||
-2.400758277161838e+00, -2.549732539343734e+00,
| |||
4.374664141464968e+00, 2.938163982698783e+00};
| |||
static const double d[4] = {7.784695709041462e-03, 3.224671290700398e-01,
| |||
2.445134137142996e+00, 3.754408661907416e+00};
| |||
const double lo = 0.02425;
| |||
if(p < lo)
| |||
{
| |||
const double q = MathSqrt(-2.0 * MathLog(p));
| |||
return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
| |||
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0);
| |||
}
| |||
if(p > 1.0 - lo)
| |||
{
| |||
const double q = MathSqrt(-2.0 * MathLog(1.0 - p));
| |||
return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
| |||
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0);
| |||
}
| |||
const double q = p - 0.5, r = q * q;
| |||
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
| |||
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0);
| |||
}
| |||
//--- Moments of a sample. Kurtosis is the plain (non-excess) fourth standardised moment.
| |||
static bool Moments(const double &x[], double &mean, double &sd, double &skew, double &kurt)
| |||
{
| |||
const int n = ArraySize(x);
| |||
if(n < 4)
| |||
return false;
| |||
double s = 0.0;
| |||
for(int i = 0; i < n; i++)
| |||
s += x[i];
| |||
mean = s / n;
| |||
double m2 = 0.0, m3 = 0.0, m4 = 0.0;
| |||
for(int i = 0; i < n; i++)
| |||
{
| |||
const double d = x[i] - mean;
| |||
m2 += d * d;
| |||
m3 += d * d * d;
| |||
m4 += d * d * d * d;
| |||
}
| |||
m2 /= n; m3 /= n; m4 /= n;
| |||
if(m2 <= 0.0)
| |||
return false;
| |||
sd = MathSqrt(m2 * n / (n - 1));
| |||
skew = m3 / MathPow(m2, 1.5);
| |||
kurt = m4 / (m2 * m2);
| |||
return true;
| |||
}
| |||
//--- PSR(SR*) for per-trade returns. -1 when the sample is too small to say anything.
| |||
static double PSR(const double &x[], const double srStar = 0.0)
| |||
{
| |||
double mean, sd, skew, kurt;
| |||
if(!Moments(x, mean, sd, skew, kurt) || sd <= 0.0)
| |||
return -1.0;
| |||
const int n = ArraySize(x);
| |||
const double sr = mean / sd;
| |||
const double den = 1.0 - skew * sr + (kurt - 1.0) / 4.0 * sr * sr;
| |||
if(den <= 0.0)
| |||
return -1.0;
| |||
return Phi((sr - srStar) * MathSqrt(n - 1.0) / MathSqrt(den));
| |||
}
| |||
//--- The Sharpe the best of `trials` unskilled strategies would show (AFML 14.1, eq. 14.1).
| |||
static double ExpectedMaxSharpe(const int trials, const int n)
| |||
{
| |||
if(trials < 2 || n < 3)
| |||
return 0.0;
| |||
const double g = 0.5772156649;
| |||
const double sigma = 1.0 / MathSqrt(n - 1.0);
| |||
return sigma * ((1.0 - g) * PhiInv(1.0 - 1.0 / trials) +
| |||
g * PhiInv(1.0 - 1.0 / (trials * M_E)));
| |||
}
| |||
static double DSR(const double &x[], const int trials)
| |||
{
| |||
return PSR(x, ExpectedMaxSharpe(trials, ArraySize(x)));
| |||
}
| |||
};
| |||
#endif // WARRIOR_EDGESTATS_MQH
|