Files
dbasket-EA/MQL5/Include/DBasket/DBasket_CointegrationEngine.mqh

425 lines
14 KiB
Plaintext

//+------------------------------------------------------------------+
//| DBasket_CointegrationEngine.mqh |
//| D-Basket Correlation Hedging EA |
//| ADF Test for Cointegration |
//+------------------------------------------------------------------+
#property copyright "D-Basket EA"
#property version "2.00"
#property strict
#ifndef DBASKET_COINTEGRATIONENGINE_MQH
#define DBASKET_COINTEGRATIONENGINE_MQH
#include "DBasket_Defines.mqh"
#include "DBasket_Structures.mqh"
#include "DBasket_Logger.mqh"
//+------------------------------------------------------------------+
//| Cointegration Data Structure |
//+------------------------------------------------------------------+
struct CointegrationData
{
double adfStatistic; // ADF test statistic
double pValue; // Approximate p-value
double beta; // Hedge ratio from OLS regression
double alpha; // Intercept from OLS regression
double residualStdDev; // Standard deviation of residuals
datetime lastUpdateTime; // Timestamp of last calculation
bool isCointegrated; // True if p-value < threshold
bool isValid; // True if calculation succeeded
string invalidReason; // Description if invalid
void Reset()
{
adfStatistic = 0;
pValue = 1.0;
beta = 1.0;
alpha = 0;
residualStdDev = 0;
lastUpdateTime = 0;
isCointegrated = false;
isValid = false;
invalidReason = "";
}
};
//+------------------------------------------------------------------+
//| Cointegration Engine Class |
//| Implements Engle-Granger two-step cointegration test |
//+------------------------------------------------------------------+
class CCointegrationEngine
{
private:
// Configuration
int m_lookbackPeriod; // Bars for regression
int m_adfLags; // Lags for ADF test
double m_pValueThreshold; // Cointegration threshold
int m_updateIntervalBars; // Bars between updates
// State
CointegrationData m_cache;
int m_barsSinceUpdate;
bool m_isInitialized;
//+------------------------------------------------------------------+
//| Calculate mean of array |
//+------------------------------------------------------------------+
double ArrayMean(const double &arr[], int count)
{
if(count <= 0) return 0;
double sum = 0;
for(int i = 0; i < count; i++)
sum += arr[i];
return sum / count;
}
//+------------------------------------------------------------------+
//| Calculate standard deviation of array |
//+------------------------------------------------------------------+
double ArrayStdDev(const double &arr[], int count, double mean)
{
if(count <= 1) return 0;
double sumSq = 0;
for(int i = 0; i < count; i++)
{
double diff = arr[i] - mean;
sumSq += diff * diff;
}
return MathSqrt(sumSq / (count - 1));
}
//+------------------------------------------------------------------+
//| OLS Regression: Y = alpha + beta * X + residuals |
//| Returns residuals array |
//+------------------------------------------------------------------+
bool OLSRegression(const double &X[], const double &Y[], int count,
double &beta, double &alpha, double &residuals[])
{
if(count < 30)
{
Logger.Warning("OLS: Insufficient data points: " + IntegerToString(count));
return false;
}
// Calculate means
double meanX = ArrayMean(X, count);
double meanY = ArrayMean(Y, count);
// Calculate covariance and variance
double covXY = 0;
double varX = 0;
for(int i = 0; i < count; i++)
{
double dx = X[i] - meanX;
double dy = Y[i] - meanY;
covXY += dx * dy;
varX += dx * dx;
}
// Avoid division by zero
if(MathAbs(varX) < 0.0000001)
{
Logger.Warning("OLS: Near-zero variance in X series");
return false;
}
// Calculate coefficients
beta = covXY / varX;
alpha = meanY - beta * meanX;
// Calculate residuals
if(ArrayResize(residuals, count) != count)
return false;
for(int i = 0; i < count; i++)
{
residuals[i] = Y[i] - (alpha + beta * X[i]);
}
return true;
}
//+------------------------------------------------------------------+
//| Augmented Dickey-Fuller Test (simplified, 1 lag) |
//| Tests if series has unit root (non-stationary) |
//| Returns: ADF statistic (more negative = more stationary) |
//+------------------------------------------------------------------+
bool ADFTest(const double &series[], int count, double &adfStat, double &pValue)
{
if(count < 50)
{
Logger.Warning("ADF: Insufficient data points: " + IntegerToString(count));
return false;
}
int n = count - 1; // Number of differences
// Construct lagged series and first differences
double y_lag[];
double delta_y[];
if(ArrayResize(y_lag, n) != n || ArrayResize(delta_y, n) != n)
return false;
for(int i = 0; i < n; i++)
{
y_lag[i] = series[i];
delta_y[i] = series[i + 1] - series[i];
}
// Run regression: delta_y = alpha + gamma * y_lag + epsilon
// We need gamma coefficient and its standard error
// Calculate means
double meanYLag = ArrayMean(y_lag, n);
double meanDeltaY = ArrayMean(delta_y, n);
// Calculate covariance and variance for regression
double covYD = 0;
double varYLag = 0;
for(int i = 0; i < n; i++)
{
double dy_lag = y_lag[i] - meanYLag;
double dy = delta_y[i] - meanDeltaY;
covYD += dy_lag * dy;
varYLag += dy_lag * dy_lag;
}
if(MathAbs(varYLag) < 0.0000001)
{
Logger.Warning("ADF: Near-zero variance in lagged series");
return false;
}
// Gamma coefficient
double gamma = covYD / varYLag;
double alpha = meanDeltaY - gamma * meanYLag;
// Calculate residuals and MSE
double residualSumSq = 0;
for(int i = 0; i < n; i++)
{
double fitted = alpha + gamma * y_lag[i];
double resid = delta_y[i] - fitted;
residualSumSq += resid * resid;
}
double mse = residualSumSq / (n - 2); // 2 parameters estimated
// Standard error of gamma
double seGamma = MathSqrt(mse / varYLag);
if(seGamma < 0.0000001)
{
Logger.Warning("ADF: Near-zero standard error");
return false;
}
// ADF statistic (t-statistic of gamma)
adfStat = gamma / seGamma;
// Convert to approximate p-value using MacKinnon critical values
// Critical values for ADF test with constant, no trend
// 1%: -3.43, 5%: -2.86, 10%: -2.57
if(adfStat < -3.43)
pValue = 0.01;
else if(adfStat < -2.86)
pValue = 0.05;
else if(adfStat < -2.57)
pValue = 0.10;
else
pValue = 0.20; // Not stationary
return true;
}
public:
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CCointegrationEngine()
{
m_lookbackPeriod = 250;
m_adfLags = 1;
m_pValueThreshold = 0.05;
m_updateIntervalBars = 50;
m_barsSinceUpdate = 999; // Force initial calculation
m_isInitialized = false;
m_cache.Reset();
}
//+------------------------------------------------------------------+
//| Initialize engine |
//+------------------------------------------------------------------+
bool Initialize(int lookbackPeriod, double pValueThreshold, int updateIntervalBars, int adfLags = 1)
{
if(lookbackPeriod < 60)
{
Logger.Error("Cointegration: Lookback period too short (min 60)");
return false;
}
m_lookbackPeriod = lookbackPeriod;
m_pValueThreshold = pValueThreshold;
m_updateIntervalBars = updateIntervalBars;
m_adfLags = adfLags;
m_barsSinceUpdate = 999;
m_isInitialized = true;
m_cache.Reset();
Logger.Info("Cointegration Engine initialized - Lookback: " + IntegerToString(m_lookbackPeriod) +
", P-Value Threshold: " + DoubleToString(m_pValueThreshold, 2) +
", Update Interval: " + IntegerToString(m_updateIntervalBars) + " bars");
return true;
}
//+------------------------------------------------------------------+
//| Update cointegration test |
//| X = synthetic ratio (AUDCAD/NZDCAD) |
//| Y = AUDNZD |
//+------------------------------------------------------------------+
bool Update(const double &syntheticRatio[], const double &audnzd[], int dataCount, bool forceUpdate = false)
{
if(!m_isInitialized)
{
Logger.Error("Cointegration Engine not initialized");
return false;
}
// Check if update needed
m_barsSinceUpdate++;
if(!forceUpdate && m_barsSinceUpdate < m_updateIntervalBars && m_cache.isValid)
{
return true; // Use cached values
}
// Validate data
int count = MathMin(dataCount, m_lookbackPeriod);
if(count < 60)
{
m_cache.isValid = false;
m_cache.invalidReason = "Insufficient data: " + IntegerToString(count);
return false;
}
// Reset update counter
m_barsSinceUpdate = 0;
// Step 1: OLS Regression to get residuals
double residuals[];
double beta, alpha;
if(!OLSRegression(syntheticRatio, audnzd, count, beta, alpha, residuals))
{
m_cache.isValid = false;
m_cache.invalidReason = "OLS regression failed";
return false;
}
// Step 2: ADF Test on residuals
double adfStat, pValue;
int residCount = ArraySize(residuals);
if(!ADFTest(residuals, residCount, adfStat, pValue))
{
m_cache.isValid = false;
m_cache.invalidReason = "ADF test failed";
return false;
}
// Step 3: Calculate residual statistics
double residMean = ArrayMean(residuals, residCount);
double residStdDev = ArrayStdDev(residuals, residCount, residMean);
// Step 4: Update cache
m_cache.adfStatistic = adfStat;
m_cache.pValue = pValue;
m_cache.beta = beta;
m_cache.alpha = alpha;
m_cache.residualStdDev = residStdDev;
m_cache.lastUpdateTime = TimeCurrent();
m_cache.isCointegrated = (pValue < m_pValueThreshold);
m_cache.isValid = true;
m_cache.invalidReason = "";
// Log results
string status = m_cache.isCointegrated ? "COINTEGRATED" : "NOT COINTEGRATED";
Logger.Debug("Cointegration Test: " + status +
" - ADF: " + DoubleToString(adfStat, 3) +
", P-Value: " + DoubleToString(pValue, 2) +
", Beta: " + DoubleToString(beta, 4));
return true;
}
//+------------------------------------------------------------------+
//| Check if spread is cointegrated |
//+------------------------------------------------------------------+
bool IsCointegrated()
{
return m_cache.isValid && m_cache.isCointegrated;
}
//+------------------------------------------------------------------+
//| Get cached cointegration data |
//+------------------------------------------------------------------+
void GetData(CointegrationData &data)
{
data = m_cache;
}
//+------------------------------------------------------------------+
//| Get ADF statistic |
//+------------------------------------------------------------------+
double GetADFStatistic()
{
return m_cache.adfStatistic;
}
//+------------------------------------------------------------------+
//| Get p-value |
//+------------------------------------------------------------------+
double GetPValue()
{
return m_cache.pValue;
}
//+------------------------------------------------------------------+
//| Get hedge ratio (beta) |
//+------------------------------------------------------------------+
double GetBeta()
{
return m_cache.beta;
}
//+------------------------------------------------------------------+
//| Is cache valid |
//+------------------------------------------------------------------+
bool IsValid()
{
return m_cache.isValid;
}
//+------------------------------------------------------------------+
//| Get reason if invalid |
//+------------------------------------------------------------------+
string GetInvalidReason()
{
return m_cache.invalidReason;
}
//+------------------------------------------------------------------+
//| Force recalculation on next update |
//+------------------------------------------------------------------+
void Invalidate()
{
m_barsSinceUpdate = 999;
}
};
#endif // DBASKET_COINTEGRATIONENGINE_MQH
//+------------------------------------------------------------------+