forked from mnbvc188199/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>
188 lines
8.3 KiB
MQL5
188 lines
8.3 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| FracDiff.mqh |
| |
//| Fixed-width fractional differentiation (Lopez de Prado AFML c.5)|
| |
//+------------------------------------------------------------------+
| |
//| WHY THIS EXISTS |
| |
//| |
| |
//| FeatureBuilder.mqh does not feed non-stationary prices. It feeds |
| |
//| (close-open)/atr, (close-close[20])/atr, (close-SMA20)/atr and |
| |
//| volume/volume_base. Those are INTEGER-order differences: d = 1. |
| |
//| They are stationary and they are memory-less. |
| |
//| |
| |
//| So this module does not fix a stationarity bug - there isn't one. |
| |
//| It fixes OVER-differencing: at d = 1 the correlation between the |
| |
//| transformed series and the price level is ~0, so every feature |
| |
//| the network sees has been stripped of where price actually IS. |
| |
//| Fractional d keeps that level information and still passes ADF. |
| |
//| |
| |
//| THE RECURRENCE |
| |
//| w_0 = 1, w_k = -w_{k-1} * (d - k + 1) / k |
| |
//| X_t^(d) = sum_{k=0}^{L} w_k * X_{t-k} |
| |
//| Truncated at the first |w_k| < tau, so every output uses the same |
| |
//| L taps and the transform is translation-invariant. |
| |
//| |
| |
//| *** THE 1024-BAR TRAP *** |
| |
//| The stdlib series wrappers (CiClose/CiOpen/... via CopyBuffer) |
| |
//| return 0.0 IN SILENCE past shift 1023. An FFD window is routinely |
| |
//| WIDER than that: at tau = 1e-5, d = 0.4 needs 1458 taps and |
| |
//| d = 0.1 needs 4076. Reading those through m_Close.GetData() would |
| |
//| multiply real weights by silent zeros and produce a confident, |
| |
//| wrong number that no guard catches. |
| |
//| Therefore this module calls CopyClose/CopyHigh/... DIRECTLY and |
| |
//| never touches the indicator series. Callers must do the same. |
| |
//| |
| |
//| Tap budget (from research/fracdiff.py, measured): |
| |
//| d tau=1e-5 tau=1e-4 tau=1e-3 |
| |
//| 0.10 4076 503 62 |
| |
//| 0.30 2275 388 66 |
| |
//| 0.40 1458 282 55 |
| |
//| 0.50 927 200 44 |
| |
//| tau = 1e-4 keeps every d >= 0.1 inside ~500 bars, which is the |
| |
//| setting this module defaults to. |
| |
//+------------------------------------------------------------------+
| |
#ifndef FRACDIFF_MQH
| |
#define FRACDIFF_MQH
| |
| |
#define FFD_MAX_TAPS 4096
| |
#define FFD_DEFAULT_TAU 1e-4
| |
| |
//+------------------------------------------------------------------+
| |
//| Weight vector, lag-ordered: w[0] multiplies X_t, w[1] X_{t-1}, ...|
| |
//| Returns the tap count, or 0 on a bad d. |
| |
//+------------------------------------------------------------------+
| |
int FFDWeights(const double d, double &w[], const double tau = FFD_DEFAULT_TAU)
| |
{
| |
if(d < 0.0 || !MathIsValidNumber(d))
| |
return 0;
| |
ArrayResize(w, FFD_MAX_TAPS);
| |
w[0] = 1.0;
| |
int n = 1;
| |
for(int k = 1; k < FFD_MAX_TAPS; k++)
| |
{
| |
double wk = -w[k - 1] * (d - k + 1.0) / (double)k;
| |
if(MathAbs(wk) < tau)
| |
break;
| |
w[k] = wk;
| |
n++;
| |
}
| |
ArrayResize(w, n);
| |
return n;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| CFracDiff - caches one weight vector and applies it to a series. |
| |
//| |
| |
//| Series convention is MQL5's: index 0 = newest. shift is the bar |
| |
//| to transform; the taps walk toward OLDER bars (increasing index), |
| |
//| so nothing here can read the future. |
| |
//+------------------------------------------------------------------+
| |
class CFracDiff
| |
{
| |
private:
| |
double m_w[];
| |
int m_taps;
| |
double m_d;
| |
double m_tau;
| |
string m_symbol;
| |
ENUM_TIMEFRAMES m_period;
| |
| |
public:
| |
CFracDiff(void) : m_taps(0), m_d(0.0), m_tau(FFD_DEFAULT_TAU),
| |
m_symbol(NULL), m_period(PERIOD_CURRENT) { }
| |
| |
bool Init(const string symbol, const ENUM_TIMEFRAMES period,
| |
const double d, const double tau = FFD_DEFAULT_TAU)
| |
{
| |
m_symbol = symbol;
| |
m_period = period;
| |
m_d = d;
| |
m_tau = tau;
| |
m_taps = FFDWeights(d, m_w, tau);
| |
if(m_taps <= 0)
| |
{
| |
Print(__FUNCTION__ + ": bad d=" + DoubleToString(d, 4));
| |
return false;
| |
}
| |
//--- A d so small the window exceeds what we will ever load is a config
| |
//--- error, not a runtime degradation: fail loudly at Init rather than
| |
//--- return a silently short-windowed transform for the rest of the run.
| |
if(m_taps > FFD_MAX_TAPS - 1)
| |
{
| |
Print(__FUNCTION__ + ": d=" + DoubleToString(d, 4) + " needs " +
| |
IntegerToString(m_taps) + " taps - raise tau or raise d");
| |
return false;
| |
}
| |
return true;
| |
}
| |
| |
int Taps(void) const { return m_taps; }
| |
double D(void) const { return m_d; }
| |
| |
//--- Bars of warm-up a caller must leave before the oldest usable output.
| |
int WarmupBars(void) const { return m_taps; }
| |
| |
//+---------------------------------------------------------------+
| |
//| Apply to an arbitrary pre-loaded array (index 0 = newest). |
| |
//| Returns false if the array cannot cover shift + taps. |
| |
//+---------------------------------------------------------------+
| |
bool ApplyArray(const double &src[], const int shift, double &out) const
| |
{
| |
if(m_taps <= 0)
| |
return false;
| |
int need = shift + m_taps;
| |
if(shift < 0 || need > ArraySize(src))
| |
return false;
| |
double acc = 0.0;
| |
for(int k = 0; k < m_taps; k++)
| |
{
| |
double v = src[shift + k];
| |
//--- == EMPTY_VALUE explicitly: DBL_MAX passes MathIsValidNumber and
| |
//--- would dominate the sum while still looking like a real reading.
| |
//--- Same reasoning as the price guards in FeatureBuilder.mqh.
| |
if(!MathIsValidNumber(v) || v == EMPTY_VALUE || v <= 0.0)
| |
return false;
| |
acc += m_w[k] * MathLog(v); // LOG price: scale-free across the fleet
| |
}
| |
out = acc;
| |
return true;
| |
}
| |
| |
//+---------------------------------------------------------------+
| |
//| Load straight from the terminal and transform one bar. |
| |
//| |
| |
//| CopyClose, NOT CiClose::GetData - see the 1024-bar trap above. |
| |
//+---------------------------------------------------------------+
| |
bool CloseAt(const int shift, double &out) const
| |
{
| |
if(m_taps <= 0 || shift < 0)
| |
return false;
| |
double buf[];
| |
int need = m_taps;
| |
if(CopyClose(m_symbol, m_period, shift, need, buf) != need)
| |
return false;
| |
//--- CopyClose with a start/count returns OLDEST-first; flip to series
| |
//--- order so index 0 is the bar at `shift`.
| |
ArraySetAsSeries(buf, true);
| |
return ApplyArray(buf, 0, out);
| |
}
| |
| |
//+---------------------------------------------------------------+
| |
//| Same, for tick volume (log1p: CFD bars can legitimately be 0). |
| |
//+---------------------------------------------------------------+
| |
bool VolumeAt(const int shift, double &out) const
| |
{
| |
if(m_taps <= 0 || shift < 0)
| |
return false;
| |
long vbuf[];
| |
int need = m_taps;
| |
if(CopyTickVolume(m_symbol, m_period, shift, need, vbuf) != need)
| |
return false;
| |
ArraySetAsSeries(vbuf, true);
| |
double acc = 0.0;
| |
for(int k = 0; k < m_taps; k++)
| |
acc += m_w[k] * MathLog(1.0 + (double)vbuf[k]);
| |
out = acc;
| |
return true;
| |
}
| |
};
| |
#endif // FRACDIFF_MQH
|