mirror of
https://github.com/dinethlive/dbasket-EA.git
synced 2026-08-16 04:08:15 +00:00
417 lines
15 KiB
Plaintext
417 lines
15 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| DBasket_HalfLifeEngine.mqh |
|
|
//| D-Basket Correlation Hedging EA |
|
|
//| Ornstein-Uhlenbeck Half-Life |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "D-Basket EA"
|
|
#property version "2.00"
|
|
#property strict
|
|
|
|
#ifndef DBASKET_HALFLIFEENGINE_MQH
|
|
#define DBASKET_HALFLIFEENGINE_MQH
|
|
|
|
#include "DBasket_Defines.mqh"
|
|
#include "DBasket_Structures.mqh"
|
|
#include "DBasket_Logger.mqh"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Half-Life Data Structure |
|
|
//+------------------------------------------------------------------+
|
|
struct HalfLifeData
|
|
{
|
|
double lambda; // AR(1) coefficient (must be < 0)
|
|
double alpha; // Intercept
|
|
double halfLife; // Calculated half-life in bars
|
|
double sigma; // Residual standard deviation
|
|
double ouVariance; // Long-term O-U variance
|
|
datetime lastUpdateTime; // Timestamp of last calculation
|
|
bool isValid; // True if lambda < 0 (mean-reverting)
|
|
bool isMeanReverting; // True if spread is mean-reverting
|
|
string invalidReason; // Description if invalid
|
|
|
|
void Reset()
|
|
{
|
|
lambda = 0;
|
|
alpha = 0;
|
|
halfLife = 100; // Default fallback
|
|
sigma = 0;
|
|
ouVariance = 0;
|
|
lastUpdateTime = 0;
|
|
isValid = false;
|
|
isMeanReverting = false;
|
|
invalidReason = "";
|
|
}
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Half-Life Engine Class |
|
|
//| Estimates mean reversion speed via AR(1) regression |
|
|
//+------------------------------------------------------------------+
|
|
class CHalfLifeEngine
|
|
{
|
|
private:
|
|
// Configuration
|
|
int m_lookbackPeriod; // Bars for regression
|
|
int m_updateIntervalBars; // Bars between updates
|
|
int m_minHalfLife; // Minimum acceptable half-life
|
|
int m_maxHalfLife; // Maximum acceptable half-life
|
|
double m_exitMultiplier; // Max holding = multiplier * halfLife
|
|
double m_stopLossSigma; // Stop loss distance in sigma
|
|
|
|
// State
|
|
HalfLifeData 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));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| AR(1) Regression: delta_y = alpha + lambda * y_lag + epsilon |
|
|
//| Tests for mean reversion in spread series |
|
|
//+------------------------------------------------------------------+
|
|
bool AR1Regression(const double &spread[], int count,
|
|
double &lambda, double &alpha, double &sigma)
|
|
{
|
|
if(count < 50)
|
|
{
|
|
Logger.Warning("AR1: Insufficient data points: " + IntegerToString(count));
|
|
return false;
|
|
}
|
|
|
|
int n = count - 1; // Number of differences
|
|
|
|
// Construct arrays
|
|
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] = spread[i];
|
|
delta_y[i] = spread[i + 1] - spread[i];
|
|
}
|
|
|
|
// Calculate means
|
|
double meanYLag = ArrayMean(y_lag, n);
|
|
double meanDeltaY = ArrayMean(delta_y, n);
|
|
|
|
// Calculate covariance and variance
|
|
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("AR1: Near-zero variance in lagged series");
|
|
return false;
|
|
}
|
|
|
|
// Lambda coefficient
|
|
lambda = covYD / varYLag;
|
|
alpha = meanDeltaY - lambda * meanYLag;
|
|
|
|
// Calculate residuals for sigma estimation
|
|
double residualSumSq = 0;
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
double fitted = alpha + lambda * y_lag[i];
|
|
double resid = delta_y[i] - fitted;
|
|
residualSumSq += resid * resid;
|
|
}
|
|
|
|
sigma = MathSqrt(residualSumSq / (n - 2));
|
|
|
|
return true;
|
|
}
|
|
|
|
public:
|
|
//+------------------------------------------------------------------+
|
|
//| Constructor |
|
|
//+------------------------------------------------------------------+
|
|
CHalfLifeEngine()
|
|
{
|
|
m_lookbackPeriod = 250;
|
|
m_updateIntervalBars = 20;
|
|
m_minHalfLife = 10;
|
|
m_maxHalfLife = 500;
|
|
m_exitMultiplier = 2.0;
|
|
m_stopLossSigma = 1.5;
|
|
m_barsSinceUpdate = 999; // Force initial calculation
|
|
m_isInitialized = false;
|
|
m_cache.Reset();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize engine |
|
|
//+------------------------------------------------------------------+
|
|
bool Initialize(int lookbackPeriod, int updateIntervalBars,
|
|
int minHalfLife, int maxHalfLife,
|
|
double exitMultiplier, double stopLossSigma)
|
|
{
|
|
if(lookbackPeriod < 50)
|
|
{
|
|
Logger.Error("HalfLife: Lookback period too short (min 50)");
|
|
return false;
|
|
}
|
|
|
|
m_lookbackPeriod = lookbackPeriod;
|
|
m_updateIntervalBars = updateIntervalBars;
|
|
m_minHalfLife = minHalfLife;
|
|
m_maxHalfLife = maxHalfLife;
|
|
m_exitMultiplier = exitMultiplier;
|
|
m_stopLossSigma = stopLossSigma;
|
|
m_barsSinceUpdate = 999;
|
|
m_isInitialized = true;
|
|
m_cache.Reset();
|
|
|
|
Logger.Info("Half-Life Engine initialized - Lookback: " + IntegerToString(m_lookbackPeriod) +
|
|
", Exit Multiplier: " + DoubleToString(m_exitMultiplier, 1) +
|
|
", SL Sigma: " + DoubleToString(m_stopLossSigma, 1));
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update half-life calculation |
|
|
//| spread[] = spread series (AUDNZD - syntheticRatio) |
|
|
//+------------------------------------------------------------------+
|
|
bool Update(const double &spread[], int dataCount, bool forceUpdate = false)
|
|
{
|
|
if(!m_isInitialized)
|
|
{
|
|
Logger.Error("Half-Life 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 < 50)
|
|
{
|
|
m_cache.isValid = false;
|
|
m_cache.invalidReason = "Insufficient data: " + IntegerToString(count);
|
|
return false;
|
|
}
|
|
|
|
// Reset update counter
|
|
m_barsSinceUpdate = 0;
|
|
|
|
// Run AR(1) regression
|
|
double lambda, alpha, sigma;
|
|
|
|
if(!AR1Regression(spread, count, lambda, alpha, sigma))
|
|
{
|
|
m_cache.isValid = false;
|
|
m_cache.invalidReason = "AR(1) regression failed";
|
|
return false;
|
|
}
|
|
|
|
// Check if mean-reverting (lambda must be negative)
|
|
if(lambda >= 0 || lambda > -0.001)
|
|
{
|
|
m_cache.isValid = true;
|
|
m_cache.isMeanReverting = false;
|
|
m_cache.lambda = lambda;
|
|
m_cache.halfLife = 9999; // Very long (no reversion)
|
|
m_cache.invalidReason = "Non-mean-reverting (lambda >= 0)";
|
|
|
|
Logger.Debug("HalfLife: Spread is non-mean-reverting, lambda = " +
|
|
DoubleToString(lambda, 6));
|
|
return true;
|
|
}
|
|
|
|
// Calculate half-life: tau = -ln(2) / lambda
|
|
double halfLife = -MathLog(2.0) / lambda;
|
|
|
|
// Calculate O-U variance: sigma^2 / (-2 * lambda)
|
|
double ouVariance = (sigma * sigma) / (-2.0 * lambda);
|
|
|
|
// Validate half-life range
|
|
bool isReasonable = (halfLife >= m_minHalfLife && halfLife <= m_maxHalfLife);
|
|
|
|
// Update cache
|
|
m_cache.lambda = lambda;
|
|
m_cache.alpha = alpha;
|
|
m_cache.halfLife = halfLife;
|
|
m_cache.sigma = sigma;
|
|
m_cache.ouVariance = ouVariance;
|
|
m_cache.lastUpdateTime = TimeCurrent();
|
|
m_cache.isMeanReverting = true;
|
|
m_cache.isValid = true;
|
|
m_cache.invalidReason = isReasonable ? "" : "Half-life out of range";
|
|
|
|
Logger.Debug("HalfLife: " + DoubleToString(halfLife, 1) + " bars" +
|
|
", Lambda: " + DoubleToString(lambda, 6) +
|
|
", O-U Variance: " + DoubleToString(ouVariance, 6));
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get half-life value |
|
|
//+------------------------------------------------------------------+
|
|
double GetHalfLife()
|
|
{
|
|
return m_cache.halfLife;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get maximum holding time in bars |
|
|
//+------------------------------------------------------------------+
|
|
int GetMaxHoldingBars()
|
|
{
|
|
if(!m_cache.isValid || !m_cache.isMeanReverting)
|
|
return 100; // Default fallback
|
|
|
|
return (int)(m_cache.halfLife * m_exitMultiplier);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get stop loss z-score distance |
|
|
//+------------------------------------------------------------------+
|
|
double GetStopLossSigma()
|
|
{
|
|
return m_stopLossSigma;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get O-U variance (for stop-loss calculation) |
|
|
//+------------------------------------------------------------------+
|
|
double GetOUVariance()
|
|
{
|
|
return m_cache.ouVariance;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if spread is mean-reverting |
|
|
//+------------------------------------------------------------------+
|
|
bool IsMeanReverting()
|
|
{
|
|
return m_cache.isValid && m_cache.isMeanReverting;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if half-life is within reasonable range |
|
|
//+------------------------------------------------------------------+
|
|
bool IsHalfLifeValid()
|
|
{
|
|
if(!m_cache.isValid || !m_cache.isMeanReverting)
|
|
return false;
|
|
|
|
return (m_cache.halfLife >= m_minHalfLife &&
|
|
m_cache.halfLife <= m_maxHalfLife);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get cached half-life data |
|
|
//+------------------------------------------------------------------+
|
|
void GetData(HalfLifeData &data)
|
|
{
|
|
data = m_cache;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Is cache valid |
|
|
//+------------------------------------------------------------------+
|
|
bool IsValid()
|
|
{
|
|
return m_cache.isValid;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get lambda coefficient |
|
|
//+------------------------------------------------------------------+
|
|
double GetLambda()
|
|
{
|
|
return m_cache.lambda;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Force recalculation on next update |
|
|
//+------------------------------------------------------------------+
|
|
void Invalidate()
|
|
{
|
|
m_barsSinceUpdate = 999;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if exit triggered by time |
|
|
//| barsOpen: number of bars since basket opened |
|
|
//+------------------------------------------------------------------+
|
|
bool IsTimeExitTriggered(int barsOpen)
|
|
{
|
|
if(!m_cache.isValid || !m_cache.isMeanReverting)
|
|
return (barsOpen > 100); // Fallback
|
|
|
|
int maxBars = GetMaxHoldingBars();
|
|
return (barsOpen > maxBars);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if stop-loss triggered |
|
|
//| entryZScore: z-score at entry |
|
|
//| currentZScore: current z-score |
|
|
//+------------------------------------------------------------------+
|
|
bool IsStopLossTriggered(double entryZScore, double currentZScore)
|
|
{
|
|
// Stop loss if spread diverges further by stopLossSigma
|
|
double stopDistance = m_stopLossSigma;
|
|
|
|
if(entryZScore > 0) // Short basket entry
|
|
{
|
|
// Z-score was positive, should decrease
|
|
// Stop if it increases beyond entry + sigma
|
|
return (currentZScore > entryZScore + stopDistance);
|
|
}
|
|
else // Long basket entry
|
|
{
|
|
// Z-score was negative, should increase toward 0
|
|
// Stop if it decreases beyond entry - sigma
|
|
return (currentZScore < entryZScore - stopDistance);
|
|
}
|
|
}
|
|
};
|
|
|
|
#endif // DBASKET_HALFLIFEENGINE_MQH
|
|
//+------------------------------------------------------------------+
|