mirror of
https://github.com/dinethlive/dbasket-EA.git
synced 2026-08-16 20:28:13 +00:00
Initial commit: D-Basket EA v2.0 Pro
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| 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
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,484 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_CorrelationEngine.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Correlation Calculation Module |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_CORRELATIONENGINE_MQH
|
||||
#define DBASKET_CORRELATIONENGINE_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
#include "DBasket_Structures.mqh"
|
||||
#include "DBasket_Logger.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Correlation Engine Class |
|
||||
//| Handles price data collection, correlation, and z-score calc |
|
||||
//+------------------------------------------------------------------+
|
||||
class CCorrelationEngine
|
||||
{
|
||||
private:
|
||||
// Configuration
|
||||
string m_symbols[NUM_SYMBOLS]; // Symbol names
|
||||
int m_lookbackPeriod; // Rolling window size
|
||||
ENUM_TIMEFRAMES m_timeframe; // Timeframe for data
|
||||
int m_updateIntervalSec; // Cache update interval
|
||||
|
||||
// Price buffers for each symbol
|
||||
PriceHistoryBuffer m_priceBuffers[NUM_SYMBOLS];
|
||||
|
||||
// Cached calculation results
|
||||
CorrelationData m_cache;
|
||||
datetime m_lastCalculationTime;
|
||||
datetime m_lastBarTime[NUM_SYMBOLS];
|
||||
|
||||
// State
|
||||
bool m_isInitialized;
|
||||
bool m_isWarmedUp;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate mean of array |
|
||||
//+------------------------------------------------------------------+
|
||||
double CalculateMean(const double &arr[], int size)
|
||||
{
|
||||
if(size <= 0)
|
||||
return 0;
|
||||
|
||||
double sum = 0;
|
||||
for(int i = 0; i < size; i++)
|
||||
sum += arr[i];
|
||||
|
||||
return sum / size;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate standard deviation |
|
||||
//+------------------------------------------------------------------+
|
||||
double CalculateStdDev(const double &arr[], int size, double mean)
|
||||
{
|
||||
if(size <= 1)
|
||||
return 0;
|
||||
|
||||
double sumSq = 0;
|
||||
for(int i = 0; i < size; i++)
|
||||
{
|
||||
double diff = arr[i] - mean;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
|
||||
return MathSqrt(sumSq / size);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate Pearson correlation between two arrays |
|
||||
//+------------------------------------------------------------------+
|
||||
double CalculatePearsonCorrelation(const double &x[], const double &y[], int size)
|
||||
{
|
||||
if(size < 2)
|
||||
return 0;
|
||||
|
||||
// Calculate means
|
||||
double meanX = CalculateMean(x, size);
|
||||
double meanY = CalculateMean(y, size);
|
||||
|
||||
// Calculate covariance and standard deviations
|
||||
double sumXY = 0;
|
||||
double sumX2 = 0;
|
||||
double sumY2 = 0;
|
||||
|
||||
for(int i = 0; i < size; i++)
|
||||
{
|
||||
double dx = x[i] - meanX;
|
||||
double dy = y[i] - meanY;
|
||||
sumXY += dx * dy;
|
||||
sumX2 += dx * dx;
|
||||
sumY2 += dy * dy;
|
||||
}
|
||||
|
||||
// Calculate correlation
|
||||
double denominator = MathSqrt(sumX2 * sumY2);
|
||||
|
||||
if(denominator == 0)
|
||||
return 0;
|
||||
|
||||
double correlation = sumXY / denominator;
|
||||
|
||||
// Clamp to valid range due to floating point errors
|
||||
return CLAMP(correlation, -1.0, 1.0);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Load historical prices for a symbol |
|
||||
//+------------------------------------------------------------------+
|
||||
bool LoadHistoricalPrices(int symbolIndex)
|
||||
{
|
||||
if(symbolIndex < 0 || symbolIndex >= NUM_SYMBOLS)
|
||||
return false;
|
||||
|
||||
string symbol = m_symbols[symbolIndex];
|
||||
double prices[];
|
||||
|
||||
// Copy close prices
|
||||
int copied = CopyClose(symbol, m_timeframe, 0, m_lookbackPeriod, prices);
|
||||
|
||||
if(copied < m_lookbackPeriod)
|
||||
{
|
||||
Logger.Warning("Insufficient historical data for " + symbol +
|
||||
". Required: " + IntegerToString(m_lookbackPeriod) +
|
||||
", Got: " + IntegerToString(copied));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize buffer
|
||||
if(!m_priceBuffers[symbolIndex].Initialize(m_lookbackPeriod))
|
||||
{
|
||||
Logger.Error("Failed to initialize price buffer for " + symbol);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate buffer (prices array is oldest to newest)
|
||||
for(int i = 0; i < m_lookbackPeriod; i++)
|
||||
{
|
||||
m_priceBuffers[symbolIndex].prices[i] = prices[i];
|
||||
}
|
||||
|
||||
m_priceBuffers[symbolIndex].head = m_lookbackPeriod - 1;
|
||||
m_priceBuffers[symbolIndex].isWarmedUp = true;
|
||||
m_priceBuffers[symbolIndex].lastUpdateTime = TimeCurrent();
|
||||
|
||||
Logger.Info("Loaded " + IntegerToString(copied) + " historical prices for " + symbol);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if new bar formed for symbol |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsNewBar(int symbolIndex)
|
||||
{
|
||||
datetime currentBarTime = iTime(m_symbols[symbolIndex], m_timeframe, 0);
|
||||
|
||||
if(currentBarTime != m_lastBarTime[symbolIndex])
|
||||
{
|
||||
m_lastBarTime[symbolIndex] = currentBarTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate spread series for z-score |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CalculateSpreadSeries(double &spreadSeries[], double ¤tSpread)
|
||||
{
|
||||
if(ArrayResize(spreadSeries, m_lookbackPeriod) != m_lookbackPeriod)
|
||||
return false;
|
||||
|
||||
// Get ordered price arrays
|
||||
double pricesAUDCAD[], pricesNZDCAD[], pricesAUDNZD[];
|
||||
|
||||
if(!m_priceBuffers[SYMBOL_AUDCAD].GetPricesOrdered(pricesAUDCAD) ||
|
||||
!m_priceBuffers[SYMBOL_NZDCAD].GetPricesOrdered(pricesNZDCAD) ||
|
||||
!m_priceBuffers[SYMBOL_AUDNZD].GetPricesOrdered(pricesAUDNZD))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate spread series: (AUDCAD/NZDCAD) - AUDNZD
|
||||
for(int i = 0; i < m_lookbackPeriod; i++)
|
||||
{
|
||||
if(pricesNZDCAD[i] == 0)
|
||||
{
|
||||
spreadSeries[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
double ratio = pricesAUDCAD[i] / pricesNZDCAD[i];
|
||||
spreadSeries[i] = ratio - pricesAUDNZD[i];
|
||||
}
|
||||
|
||||
// Current spread is the last element
|
||||
currentSpread = spreadSeries[m_lookbackPeriod - 1];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
//+------------------------------------------------------------------+
|
||||
//| Constructor |
|
||||
//+------------------------------------------------------------------+
|
||||
CCorrelationEngine()
|
||||
{
|
||||
m_lookbackPeriod = 250;
|
||||
m_timeframe = PERIOD_M15;
|
||||
m_updateIntervalSec = DEFAULT_CACHE_UPDATE_INTERVAL;
|
||||
m_isInitialized = false;
|
||||
m_isWarmedUp = false;
|
||||
m_lastCalculationTime = 0;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
m_symbols[i] = "";
|
||||
m_lastBarTime[i] = 0;
|
||||
}
|
||||
|
||||
m_cache.Reset();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize the correlation engine |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(const string &symbols[], int lookbackPeriod, ENUM_TIMEFRAMES timeframe, int updateInterval = 30)
|
||||
{
|
||||
// Validate lookback period
|
||||
if(lookbackPeriod < MIN_LOOKBACK_PERIOD || lookbackPeriod > MAX_LOOKBACK_PERIOD)
|
||||
{
|
||||
Logger.Error("Invalid lookback period: " + IntegerToString(lookbackPeriod) +
|
||||
". Must be " + IntegerToString(MIN_LOOKBACK_PERIOD) + "-" + IntegerToString(MAX_LOOKBACK_PERIOD));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_lookbackPeriod = lookbackPeriod;
|
||||
m_timeframe = timeframe;
|
||||
m_updateIntervalSec = updateInterval;
|
||||
|
||||
// Copy symbol names
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
m_symbols[i] = symbols[i];
|
||||
}
|
||||
|
||||
// Load historical data for all symbols
|
||||
bool allLoaded = true;
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(!LoadHistoricalPrices(i))
|
||||
{
|
||||
allLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(!allLoaded)
|
||||
{
|
||||
Logger.Warning("Not all historical data loaded. Engine will warm up during trading.");
|
||||
}
|
||||
|
||||
m_isInitialized = true;
|
||||
m_isWarmedUp = allLoaded;
|
||||
|
||||
Logger.Info("Correlation Engine initialized - Lookback: " + IntegerToString(m_lookbackPeriod) +
|
||||
", Timeframe: " + EnumToString(m_timeframe));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Update price buffers (call on each tick or new bar) |
|
||||
//+------------------------------------------------------------------+
|
||||
void UpdatePriceBuffers()
|
||||
{
|
||||
if(!m_isInitialized)
|
||||
return;
|
||||
|
||||
// Check for new bar on each symbol
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(IsNewBar(i))
|
||||
{
|
||||
// Get latest close price
|
||||
double price = iClose(m_symbols[i], m_timeframe, 1); // Previous bar close (completed)
|
||||
|
||||
if(price > 0)
|
||||
{
|
||||
m_priceBuffers[i].AddPrice(price, TimeCurrent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all buffers are warmed up
|
||||
if(!m_isWarmedUp)
|
||||
{
|
||||
bool allWarmedUp = true;
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(!m_priceBuffers[i].isWarmedUp)
|
||||
{
|
||||
allWarmedUp = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_isWarmedUp = allWarmedUp;
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate and update correlation cache |
|
||||
//+------------------------------------------------------------------+
|
||||
bool UpdateCorrelationCache(bool forceUpdate = false)
|
||||
{
|
||||
if(!m_isInitialized)
|
||||
return false;
|
||||
|
||||
// Check cache freshness
|
||||
datetime currentTime = TimeCurrent();
|
||||
if(!forceUpdate && (currentTime - m_lastCalculationTime) < m_updateIntervalSec)
|
||||
{
|
||||
return m_cache.isValid;
|
||||
}
|
||||
|
||||
// Check if warmed up
|
||||
if(!m_isWarmedUp)
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
m_cache.invalidReason = "Price buffers not warmed up";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get ordered price arrays
|
||||
double pricesAUDCAD[], pricesNZDCAD[], pricesAUDNZD[];
|
||||
|
||||
if(!m_priceBuffers[SYMBOL_AUDCAD].GetPricesOrdered(pricesAUDCAD) ||
|
||||
!m_priceBuffers[SYMBOL_NZDCAD].GetPricesOrdered(pricesNZDCAD) ||
|
||||
!m_priceBuffers[SYMBOL_AUDNZD].GetPricesOrdered(pricesAUDNZD))
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
m_cache.invalidReason = "Failed to get ordered prices";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate primary correlation (AUDCAD vs NZDCAD)
|
||||
m_cache.corrAUDCAD_NZDCAD = CalculatePearsonCorrelation(pricesAUDCAD, pricesNZDCAD, m_lookbackPeriod);
|
||||
|
||||
// Calculate secondary correlations (for validation)
|
||||
m_cache.corrAUDCAD_AUDNZD = CalculatePearsonCorrelation(pricesAUDCAD, pricesAUDNZD, m_lookbackPeriod);
|
||||
m_cache.corrNZDCAD_AUDNZD = CalculatePearsonCorrelation(pricesNZDCAD, pricesAUDNZD, m_lookbackPeriod);
|
||||
|
||||
// Calculate synthetic ratio and spread
|
||||
double currentAUDCAD = m_priceBuffers[SYMBOL_AUDCAD].GetPrice(0);
|
||||
double currentNZDCAD = m_priceBuffers[SYMBOL_NZDCAD].GetPrice(0);
|
||||
double currentAUDNZD = m_priceBuffers[SYMBOL_AUDNZD].GetPrice(0);
|
||||
|
||||
if(currentNZDCAD == 0)
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
m_cache.invalidReason = "NZDCAD price is zero";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_cache.syntheticRatio = currentAUDCAD / currentNZDCAD;
|
||||
m_cache.actualAUDNZD = currentAUDNZD;
|
||||
m_cache.spreadValue = m_cache.syntheticRatio - currentAUDNZD;
|
||||
|
||||
// Calculate spread z-score
|
||||
double spreadSeries[];
|
||||
double currentSpread;
|
||||
|
||||
if(!CalculateSpreadSeries(spreadSeries, currentSpread))
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
m_cache.invalidReason = "Failed to calculate spread series";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_cache.spreadMean = CalculateMean(spreadSeries, m_lookbackPeriod);
|
||||
m_cache.spreadStdDev = CalculateStdDev(spreadSeries, m_lookbackPeriod, m_cache.spreadMean);
|
||||
|
||||
if(m_cache.spreadStdDev == 0)
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
m_cache.invalidReason = "Spread standard deviation is zero";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_cache.spreadZScore = (currentSpread - m_cache.spreadMean) / m_cache.spreadStdDev;
|
||||
|
||||
// Validate z-score
|
||||
if(!MathIsValidNumber(m_cache.spreadZScore))
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
m_cache.invalidReason = "Z-score calculation resulted in invalid number";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update metadata
|
||||
m_cache.calculationTime = currentTime;
|
||||
m_cache.lookbackPeriod = m_lookbackPeriod;
|
||||
m_cache.isValid = true;
|
||||
m_cache.invalidReason = "";
|
||||
|
||||
m_lastCalculationTime = currentTime;
|
||||
|
||||
Logger.LogCorrelationData(m_cache);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current correlation data |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetCorrelationData(CorrelationData &data)
|
||||
{
|
||||
data = m_cache;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get primary correlation coefficient |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetPrimaryCorrelation()
|
||||
{
|
||||
return m_cache.corrAUDCAD_NZDCAD;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current z-score |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetSpreadZScore()
|
||||
{
|
||||
return m_cache.spreadZScore;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if engine is ready for trading |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsReady()
|
||||
{
|
||||
return m_isInitialized && m_isWarmedUp && m_cache.isValid;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if engine is warmed up |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsWarmedUp()
|
||||
{
|
||||
return m_isWarmedUp;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current prices for all symbols |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetCurrentPrices(double &prices[])
|
||||
{
|
||||
if(ArrayResize(prices, NUM_SYMBOLS) != NUM_SYMBOLS)
|
||||
return;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
prices[i] = SymbolInfoDouble(m_symbols[i], SYMBOL_BID);
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Force recalculation of cache |
|
||||
//+------------------------------------------------------------------+
|
||||
void ForceRecalculation()
|
||||
{
|
||||
UpdateCorrelationCache(true);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_CORRELATIONENGINE_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,149 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_Defines.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Constants, Enums, and Macros |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_DEFINES_MQH
|
||||
#define DBASKET_DEFINES_MQH
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Symbol Configuration |
|
||||
//+------------------------------------------------------------------+
|
||||
#define SYMBOL_AUDCAD 0
|
||||
#define SYMBOL_NZDCAD 1
|
||||
#define SYMBOL_AUDNZD 2
|
||||
#define NUM_SYMBOLS 3
|
||||
|
||||
// Maximum lookback period for correlation calculation
|
||||
#define MAX_LOOKBACK_PERIOD 1000
|
||||
#define MIN_LOOKBACK_PERIOD 50
|
||||
|
||||
// Cache update interval defaults (seconds)
|
||||
#define DEFAULT_CACHE_UPDATE_INTERVAL 30
|
||||
|
||||
// Risk management defaults
|
||||
#define DEFAULT_MAX_DRAWDOWN_PERCENT 15.0
|
||||
#define DEFAULT_DAILY_LOSS_LIMIT 100.0
|
||||
#define DEFAULT_MIN_MARGIN_LEVEL 200.0
|
||||
#define DEFAULT_WARNING_MARGIN_LEVEL 500.0
|
||||
|
||||
// Circuit breaker defaults
|
||||
#define CB_WARNING_DRAWDOWN_PERCENT 8.0
|
||||
#define CB_TRIP_DRAWDOWN_PERCENT 15.0
|
||||
#define CB_MAX_CONSECUTIVE_LOSSES 6
|
||||
|
||||
// Position management
|
||||
#define MAX_OPEN_BASKETS 5
|
||||
#define DEFAULT_MAX_HOLDING_HOURS 24
|
||||
|
||||
// Execution
|
||||
#define DEFAULT_SLIPPAGE_POINTS 10
|
||||
#define MAX_RETRY_ATTEMPTS 3
|
||||
#define RETRY_DELAY_MS 500
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Enumerations |
|
||||
//+------------------------------------------------------------------+
|
||||
|
||||
//--- Basket Signal Types
|
||||
enum ENUM_BASKET_SIGNAL
|
||||
{
|
||||
SIGNAL_NONE = 0, // No signal
|
||||
SIGNAL_LONG_BASKET, // Long basket (expect AUDNZD to rise)
|
||||
SIGNAL_SHORT_BASKET, // Short basket (expect AUDNZD to fall)
|
||||
SIGNAL_EXIT // Exit existing basket
|
||||
};
|
||||
|
||||
//--- Basket State
|
||||
enum ENUM_BASKET_STATE
|
||||
{
|
||||
BASKET_NONE = 0, // No active basket
|
||||
BASKET_ENTRY_PENDING, // Entry signal detected, awaiting execution
|
||||
BASKET_OPEN, // Basket fully opened
|
||||
BASKET_PARTIAL, // Only some legs opened (error state)
|
||||
BASKET_EXIT_PENDING, // Exit signal detected, closing in progress
|
||||
BASKET_CLOSED // Basket closed, ready for next cycle
|
||||
};
|
||||
|
||||
//--- Circuit Breaker States
|
||||
enum ENUM_CIRCUIT_BREAKER_STATE
|
||||
{
|
||||
CB_NORMAL = 0, // Trading allowed
|
||||
CB_WARNING, // Warning issued, reduced operations
|
||||
CB_TRIPPED // Circuit breaker active, trading halted
|
||||
};
|
||||
|
||||
//--- Logging Levels
|
||||
enum ENUM_LOG_LEVEL
|
||||
{
|
||||
LOG_LEVEL_NONE = 0, // No logging
|
||||
LOG_LEVEL_ERROR, // Critical errors only
|
||||
LOG_LEVEL_WARNING, // Errors and warnings
|
||||
LOG_LEVEL_INFO, // Normal operations
|
||||
LOG_LEVEL_DEBUG // Detailed debugging
|
||||
};
|
||||
|
||||
//--- Position Sizing Modes
|
||||
enum ENUM_SIZING_MODE
|
||||
{
|
||||
SIZING_FIXED = 0, // Fixed lot size
|
||||
SIZING_RISK_BASED // Risk-based position sizing
|
||||
};
|
||||
|
||||
//--- Exit Reasons
|
||||
enum ENUM_EXIT_REASON
|
||||
{
|
||||
EXIT_NONE = 0, // No exit reason
|
||||
EXIT_MEAN_REVERSION, // Z-score returned to exit threshold
|
||||
EXIT_TAKE_PROFIT, // Take profit target reached
|
||||
EXIT_STOP_LOSS, // Stop loss triggered
|
||||
EXIT_MAX_TIME, // Maximum holding time exceeded
|
||||
EXIT_CORRELATION_BREAK, // Correlation dropped below minimum
|
||||
EXIT_RISK_LIMIT, // Risk limit breached
|
||||
EXIT_EMERGENCY, // Emergency exit (margin, massive loss)
|
||||
EXIT_MANUAL // Manual close request
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Helper Macros |
|
||||
//+------------------------------------------------------------------+
|
||||
|
||||
// Get opposite order type
|
||||
#define OPPOSITE_ORDER_TYPE(type) ((type) == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY)
|
||||
|
||||
// Check if value is valid (not NaN or INF)
|
||||
#define IS_VALID_DOUBLE(x) (!MathIsValidNumber(x) ? false : ((x) != EMPTY_VALUE))
|
||||
|
||||
// Safe division to avoid divide by zero
|
||||
#define SAFE_DIVIDE(num, den) ((den) == 0 ? 0 : (num) / (den))
|
||||
|
||||
// Convert pips to points (for 5-digit brokers)
|
||||
#define PIPS_TO_POINTS(pips, symbol) ((int)((pips) / SymbolInfoDouble(symbol, SYMBOL_POINT) * 10))
|
||||
|
||||
// Clamp value within range
|
||||
#define CLAMP(val, minVal, maxVal) (MathMax((minVal), MathMin((maxVal), (val))))
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| String Constants |
|
||||
//+------------------------------------------------------------------+
|
||||
#define EA_NAME "D-Basket Correlation Hedging EA"
|
||||
#define EA_VERSION "1.00"
|
||||
#define EA_COPYRIGHT "2024"
|
||||
|
||||
// Default symbol names (without suffix)
|
||||
#define DEFAULT_SYMBOL_AUDCAD "AUDCAD"
|
||||
#define DEFAULT_SYMBOL_NZDCAD "NZDCAD"
|
||||
#define DEFAULT_SYMBOL_AUDNZD "AUDNZD"
|
||||
|
||||
// Log file prefix
|
||||
#define LOG_FILE_PREFIX "DBasket_"
|
||||
|
||||
// Position comment prefix
|
||||
#define BASKET_COMMENT_PREFIX "DBasket_"
|
||||
|
||||
#endif // DBASKET_DEFINES_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,416 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| 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
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,359 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_Logger.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Logging Utility |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_LOGGER_MQH
|
||||
#define DBASKET_LOGGER_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Logger Class |
|
||||
//| Centralized logging with configurable levels and file output |
|
||||
//+------------------------------------------------------------------+
|
||||
class CLogger
|
||||
{
|
||||
private:
|
||||
ENUM_LOG_LEVEL m_logLevel; // Current log level
|
||||
bool m_logToFile; // Enable file logging
|
||||
int m_fileHandle; // Log file handle
|
||||
string m_logFileName; // Log file name
|
||||
bool m_isInitialized; // Initialization status
|
||||
|
||||
// Format timestamp for logging
|
||||
string FormatTimestamp(datetime time)
|
||||
{
|
||||
return TimeToString(time, TIME_DATE | TIME_SECONDS);
|
||||
}
|
||||
|
||||
// Get log level string
|
||||
string GetLevelString(ENUM_LOG_LEVEL level)
|
||||
{
|
||||
switch(level)
|
||||
{
|
||||
case LOG_LEVEL_ERROR: return "[ERROR]";
|
||||
case LOG_LEVEL_WARNING: return "[WARN] ";
|
||||
case LOG_LEVEL_INFO: return "[INFO] ";
|
||||
case LOG_LEVEL_DEBUG: return "[DEBUG]";
|
||||
default: return "[????] ";
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file if enabled
|
||||
void WriteToFile(string message)
|
||||
{
|
||||
if(!m_logToFile || m_fileHandle == INVALID_HANDLE)
|
||||
return;
|
||||
|
||||
FileWriteString(m_fileHandle, message + "\n");
|
||||
FileFlush(m_fileHandle);
|
||||
}
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
CLogger()
|
||||
{
|
||||
m_logLevel = LOG_LEVEL_INFO;
|
||||
m_logToFile = false;
|
||||
m_fileHandle = INVALID_HANDLE;
|
||||
m_logFileName = "";
|
||||
m_isInitialized = false;
|
||||
}
|
||||
|
||||
// Destructor
|
||||
~CLogger()
|
||||
{
|
||||
Deinitialize();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize logger |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(ENUM_LOG_LEVEL level, bool logToFile = false)
|
||||
{
|
||||
m_logLevel = level;
|
||||
m_logToFile = logToFile;
|
||||
|
||||
if(logToFile)
|
||||
{
|
||||
// Create log file with timestamp
|
||||
m_logFileName = LOG_FILE_PREFIX + TimeToString(TimeCurrent(), TIME_DATE) + ".log";
|
||||
StringReplace(m_logFileName, ".", "_");
|
||||
StringReplace(m_logFileName, ":", "_");
|
||||
m_logFileName = m_logFileName + ".log";
|
||||
|
||||
m_fileHandle = FileOpen(m_logFileName, FILE_WRITE | FILE_READ | FILE_TXT | FILE_SHARE_READ);
|
||||
|
||||
if(m_fileHandle == INVALID_HANDLE)
|
||||
{
|
||||
Print("Logger: Failed to open log file: ", m_logFileName, " Error: ", GetLastError());
|
||||
m_logToFile = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Move to end of file for appending
|
||||
FileSeek(m_fileHandle, 0, SEEK_END);
|
||||
}
|
||||
}
|
||||
|
||||
m_isInitialized = true;
|
||||
Info("Logger initialized - Level: " + EnumToString(level) + ", File: " + (logToFile ? m_logFileName : "Disabled"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Deinitialize logger |
|
||||
//+------------------------------------------------------------------+
|
||||
void Deinitialize()
|
||||
{
|
||||
if(m_fileHandle != INVALID_HANDLE)
|
||||
{
|
||||
FileClose(m_fileHandle);
|
||||
m_fileHandle = INVALID_HANDLE;
|
||||
}
|
||||
m_isInitialized = false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Set log level |
|
||||
//+------------------------------------------------------------------+
|
||||
void SetLogLevel(ENUM_LOG_LEVEL level)
|
||||
{
|
||||
m_logLevel = level;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Core logging function |
|
||||
//+------------------------------------------------------------------+
|
||||
void Log(ENUM_LOG_LEVEL level, string message)
|
||||
{
|
||||
// Check if this level should be logged
|
||||
if(level > m_logLevel)
|
||||
return;
|
||||
|
||||
// Format message
|
||||
string timestamp = FormatTimestamp(TimeCurrent());
|
||||
string levelStr = GetLevelString(level);
|
||||
string fullMessage = timestamp + " " + levelStr + " " + message;
|
||||
|
||||
// Output to terminal
|
||||
Print(fullMessage);
|
||||
|
||||
// Output to file if enabled
|
||||
WriteToFile(fullMessage);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Convenience methods |
|
||||
//+------------------------------------------------------------------+
|
||||
void Error(string message)
|
||||
{
|
||||
Log(LOG_LEVEL_ERROR, message);
|
||||
}
|
||||
|
||||
void Warning(string message)
|
||||
{
|
||||
Log(LOG_LEVEL_WARNING, message);
|
||||
}
|
||||
|
||||
void Info(string message)
|
||||
{
|
||||
Log(LOG_LEVEL_INFO, message);
|
||||
}
|
||||
|
||||
void Debug(string message)
|
||||
{
|
||||
Log(LOG_LEVEL_DEBUG, message);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Log with formatting (variadic-like using overloads) |
|
||||
//+------------------------------------------------------------------+
|
||||
void ErrorF(string format, string arg1)
|
||||
{
|
||||
string msg = format;
|
||||
StringReplace(msg, "%s", arg1);
|
||||
Error(msg);
|
||||
}
|
||||
|
||||
void ErrorF(string format, string arg1, string arg2)
|
||||
{
|
||||
string msg = format;
|
||||
StringReplace(msg, "%s", arg1);
|
||||
StringReplace(msg, "%s", arg2);
|
||||
Error(msg);
|
||||
}
|
||||
|
||||
void InfoF(string format, string arg1)
|
||||
{
|
||||
string msg = format;
|
||||
StringReplace(msg, "%s", arg1);
|
||||
Info(msg);
|
||||
}
|
||||
|
||||
void InfoF(string format, double value)
|
||||
{
|
||||
string msg = format;
|
||||
StringReplace(msg, "%.2f", DoubleToString(value, 2));
|
||||
StringReplace(msg, "%.4f", DoubleToString(value, 4));
|
||||
StringReplace(msg, "%.5f", DoubleToString(value, 5));
|
||||
StringReplace(msg, "%f", DoubleToString(value, 5));
|
||||
Info(msg);
|
||||
}
|
||||
|
||||
void DebugF(string format, string arg1)
|
||||
{
|
||||
string msg = format;
|
||||
StringReplace(msg, "%s", arg1);
|
||||
Debug(msg);
|
||||
}
|
||||
|
||||
void DebugF(string format, double value)
|
||||
{
|
||||
string msg = format;
|
||||
StringReplace(msg, "%.2f", DoubleToString(value, 2));
|
||||
StringReplace(msg, "%.4f", DoubleToString(value, 4));
|
||||
StringReplace(msg, "%.5f", DoubleToString(value, 5));
|
||||
StringReplace(msg, "%f", DoubleToString(value, 5));
|
||||
Debug(msg);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Log trade error with context |
|
||||
//+------------------------------------------------------------------+
|
||||
void TradeError(string operation, string symbol, int errorCode)
|
||||
{
|
||||
string errorDesc = ErrorDescription(errorCode);
|
||||
Error("Trade Error - Op: " + operation +
|
||||
", Symbol: " + symbol +
|
||||
", Code: " + IntegerToString(errorCode) +
|
||||
", Desc: " + errorDesc);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Log basket state |
|
||||
//+------------------------------------------------------------------+
|
||||
void LogBasketOpen(int basketID, ENUM_BASKET_SIGNAL direction, double zScore, double correlation)
|
||||
{
|
||||
string dirStr = (direction == SIGNAL_LONG_BASKET) ? "LONG" : "SHORT";
|
||||
Info("Basket #" + IntegerToString(basketID) + " OPENED - " +
|
||||
"Direction: " + dirStr +
|
||||
", Z-Score: " + DoubleToString(zScore, 2) +
|
||||
", Correlation: " + DoubleToString(correlation, 4));
|
||||
}
|
||||
|
||||
void LogBasketClose(int basketID, ENUM_EXIT_REASON reason, double pl, int holdBars)
|
||||
{
|
||||
string reasonStr;
|
||||
switch(reason)
|
||||
{
|
||||
case EXIT_MEAN_REVERSION: reasonStr = "Mean Reversion"; break;
|
||||
case EXIT_TAKE_PROFIT: reasonStr = "Take Profit"; break;
|
||||
case EXIT_STOP_LOSS: reasonStr = "Stop Loss"; break;
|
||||
case EXIT_MAX_TIME: reasonStr = "Max Time"; break;
|
||||
case EXIT_CORRELATION_BREAK: reasonStr = "Correlation Break"; break;
|
||||
case EXIT_RISK_LIMIT: reasonStr = "Risk Limit"; break;
|
||||
case EXIT_EMERGENCY: reasonStr = "Emergency"; break;
|
||||
default: reasonStr = "Manual"; break;
|
||||
}
|
||||
|
||||
Info("Basket #" + IntegerToString(basketID) + " CLOSED - " +
|
||||
"Reason: " + reasonStr +
|
||||
", P/L: " + DoubleToString(pl, 2) +
|
||||
", Bars Held: " + IntegerToString(holdBars));
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Convert error code to description |
|
||||
//+------------------------------------------------------------------+
|
||||
string ErrorDescription(int errorCode)
|
||||
{
|
||||
switch(errorCode)
|
||||
{
|
||||
case 0: return "No error";
|
||||
case 10004: return "Requote";
|
||||
case 10006: return "Request rejected";
|
||||
case 10007: return "Request canceled by trader";
|
||||
case 10010: return "Only part of request completed";
|
||||
case 10011: return "Request processing error";
|
||||
case 10012: return "Request canceled by timeout";
|
||||
case 10013: return "Invalid request";
|
||||
case 10014: return "Invalid volume";
|
||||
case 10015: return "Invalid price";
|
||||
case 10016: return "Invalid stops";
|
||||
case 10017: return "Trade disabled";
|
||||
case 10018: return "Market closed";
|
||||
case 10019: return "Insufficient funds";
|
||||
case 10020: return "Prices changed";
|
||||
case 10021: return "No quotes";
|
||||
case 10022: return "Invalid order expiration";
|
||||
case 10023: return "Order state changed";
|
||||
case 10024: return "Too many requests";
|
||||
case 10025: return "No changes in request";
|
||||
case 10026: return "Autotrading disabled by server";
|
||||
case 10027: return "Autotrading disabled by client";
|
||||
case 10028: return "Request locked for processing";
|
||||
case 10029: return "Order or position frozen";
|
||||
case 10030: return "Invalid fill type";
|
||||
case 10031: return "No connection with trade server";
|
||||
case 10032: return "Operation allowed only for live accounts";
|
||||
case 10033: return "Pending orders limit reached";
|
||||
case 10034: return "Order or position volume limit reached";
|
||||
case 10035: return "Invalid or prohibited order type";
|
||||
case 10036: return "Position with specified POSITION_IDENTIFIER already closed";
|
||||
case 10038: return "Close volume exceeds current position volume";
|
||||
case 10039: return "Close order already exists";
|
||||
case 10040: return "Limit of pending orders reached";
|
||||
case 10041: return "Order or position modification rejected";
|
||||
case 10042: return "Request rejected by trade context busy";
|
||||
case 10043: return "Only part of positions closed";
|
||||
case 10044: return "Position limit reached";
|
||||
default: return "Unknown error (" + IntegerToString(errorCode) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Log initialization summary |
|
||||
//+------------------------------------------------------------------+
|
||||
void LogInitSummary(string eaName, string version, double balance, int leverage, string server)
|
||||
{
|
||||
Info("==================================================");
|
||||
Info("= " + eaName + " v" + version);
|
||||
Info("==================================================");
|
||||
Info("Account Balance: " + DoubleToString(balance, 2));
|
||||
Info("Leverage: 1:" + IntegerToString(leverage));
|
||||
Info("Server: " + server);
|
||||
Info("==================================================");
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Log correlation data |
|
||||
//+------------------------------------------------------------------+
|
||||
void LogCorrelationData(const CorrelationData &data)
|
||||
{
|
||||
if(m_logLevel < LOG_LEVEL_DEBUG)
|
||||
return;
|
||||
|
||||
Debug("Correlation Data - " +
|
||||
"Corr: " + DoubleToString(data.corrAUDCAD_NZDCAD, 4) +
|
||||
", Ratio: " + DoubleToString(data.syntheticRatio, 5) +
|
||||
", Actual: " + DoubleToString(data.actualAUDNZD, 5) +
|
||||
", Spread: " + DoubleToString(data.spreadValue, 5) +
|
||||
", Z: " + DoubleToString(data.spreadZScore, 2) +
|
||||
", Valid: " + (data.isValid ? "Yes" : "No"));
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Global logger instance |
|
||||
//+------------------------------------------------------------------+
|
||||
CLogger Logger;
|
||||
|
||||
#endif // DBASKET_LOGGER_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,571 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_PositionManager.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Basket Position Management |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_POSITIONMANAGER_MQH
|
||||
#define DBASKET_POSITIONMANAGER_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
#include "DBasket_Structures.mqh"
|
||||
#include "DBasket_Logger.mqh"
|
||||
#include "DBasket_TradeWrapper.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Position Manager Class |
|
||||
//| Manages coordinated 3-leg basket positions |
|
||||
//+------------------------------------------------------------------+
|
||||
class CPositionManager
|
||||
{
|
||||
private:
|
||||
// Configuration
|
||||
string m_symbols[NUM_SYMBOLS];
|
||||
double m_baseLotSize;
|
||||
double m_riskPercentPerBasket;
|
||||
ENUM_SIZING_MODE m_sizingMode;
|
||||
int m_maxOpenBaskets;
|
||||
int m_maxHoldingHours;
|
||||
double m_takeProfitAmount;
|
||||
double m_stopLossAmount;
|
||||
|
||||
// References
|
||||
CTradeWrapper* m_tradeWrapper;
|
||||
|
||||
// State
|
||||
BasketState m_activeBasket;
|
||||
int m_basketCounter;
|
||||
bool m_isInitialized;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate lot size based on sizing mode |
|
||||
//+------------------------------------------------------------------+
|
||||
double CalculateLotSize(string symbol)
|
||||
{
|
||||
double lots = m_baseLotSize;
|
||||
|
||||
if(m_sizingMode == SIZING_RISK_BASED)
|
||||
{
|
||||
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
double riskAmount = equity * (m_riskPercentPerBasket / 100.0);
|
||||
|
||||
// Divide by 3 for basket (each leg gets 1/3)
|
||||
double riskPerLeg = riskAmount / 3.0;
|
||||
|
||||
// Use fixed pip stop assumption (e.g., 50 pips worst case)
|
||||
double pipValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE) * 10; // Approximate
|
||||
double stopPips = 50.0;
|
||||
|
||||
if(pipValue > 0)
|
||||
{
|
||||
lots = riskPerLeg / (stopPips * pipValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize lots to broker requirements
|
||||
return m_tradeWrapper.NormalizeLots(symbol, lots);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get order types for basket direction |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetBasketOrderTypes(ENUM_BASKET_SIGNAL direction, ENUM_ORDER_TYPE &types[])
|
||||
{
|
||||
if(ArrayResize(types, NUM_SYMBOLS) != NUM_SYMBOLS)
|
||||
return;
|
||||
|
||||
if(direction == SIGNAL_LONG_BASKET)
|
||||
{
|
||||
// Long basket: expect AUDNZD to rise
|
||||
// Trade: Long AUDNZD, Short AUDCAD, Long NZDCAD
|
||||
types[SYMBOL_AUDCAD] = ORDER_TYPE_SELL; // Short AUDCAD
|
||||
types[SYMBOL_NZDCAD] = ORDER_TYPE_BUY; // Long NZDCAD
|
||||
types[SYMBOL_AUDNZD] = ORDER_TYPE_BUY; // Long AUDNZD
|
||||
}
|
||||
else if(direction == SIGNAL_SHORT_BASKET)
|
||||
{
|
||||
// Short basket: expect AUDNZD to fall
|
||||
// Trade: Short AUDNZD, Long AUDCAD, Short NZDCAD
|
||||
types[SYMBOL_AUDCAD] = ORDER_TYPE_BUY; // Long AUDCAD
|
||||
types[SYMBOL_NZDCAD] = ORDER_TYPE_SELL; // Short NZDCAD
|
||||
types[SYMBOL_AUDNZD] = ORDER_TYPE_SELL; // Short AUDNZD
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Close specific legs (for rollback) |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CloseLegs(int upToIndex)
|
||||
{
|
||||
bool allClosed = true;
|
||||
|
||||
for(int i = 0; i <= upToIndex; i++)
|
||||
{
|
||||
if(m_activeBasket.positions[i].isOpen)
|
||||
{
|
||||
string errorMsg;
|
||||
if(!m_tradeWrapper.ClosePosition(m_activeBasket.positions[i].ticket, errorMsg))
|
||||
{
|
||||
Logger.Error("Failed to close leg " + IntegerToString(i) + ": " + errorMsg);
|
||||
allClosed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_activeBasket.positions[i].isOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allClosed;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Update position P&L for a single position |
|
||||
//+------------------------------------------------------------------+
|
||||
void UpdatePositionPL(int index)
|
||||
{
|
||||
if(!m_activeBasket.positions[index].isOpen)
|
||||
return;
|
||||
|
||||
ulong ticket = m_activeBasket.positions[index].ticket;
|
||||
|
||||
if(PositionSelectByTicket(ticket))
|
||||
{
|
||||
m_activeBasket.positions[index].unrealizedPL =
|
||||
PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
|
||||
m_activeBasket.positions[index].currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
|
||||
m_activeBasket.positions[index].swap = PositionGetDouble(POSITION_SWAP);
|
||||
// Note: POSITION_COMMISSION is deprecated in MQL5 and returns 0
|
||||
// Commission is now tracked via deal history (DEAL_COMMISSION)
|
||||
// For live P&L, commission is already factored into POSITION_PROFIT by most brokers
|
||||
m_activeBasket.positions[index].commission = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Position no longer exists
|
||||
m_activeBasket.positions[index].isOpen = false;
|
||||
Logger.Warning("Position " + IntegerToString(ticket) + " no longer exists");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
//+------------------------------------------------------------------+
|
||||
//| Constructor |
|
||||
//+------------------------------------------------------------------+
|
||||
CPositionManager()
|
||||
{
|
||||
m_baseLotSize = 0.01;
|
||||
m_riskPercentPerBasket = 1.0;
|
||||
m_sizingMode = SIZING_FIXED;
|
||||
m_maxOpenBaskets = 1;
|
||||
m_maxHoldingHours = DEFAULT_MAX_HOLDING_HOURS;
|
||||
m_takeProfitAmount = 0;
|
||||
m_stopLossAmount = 0;
|
||||
m_tradeWrapper = NULL;
|
||||
m_basketCounter = 0;
|
||||
m_isInitialized = false;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
m_symbols[i] = "";
|
||||
|
||||
m_activeBasket.Reset();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize position manager |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(const EAConfig &config, CTradeWrapper *tradeWrapper)
|
||||
{
|
||||
if(tradeWrapper == NULL)
|
||||
{
|
||||
Logger.Error("Trade wrapper is NULL");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_tradeWrapper = tradeWrapper;
|
||||
|
||||
// Copy configuration
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
m_symbols[i] = config.symbols[i];
|
||||
|
||||
m_baseLotSize = config.baseLotSize;
|
||||
m_riskPercentPerBasket = config.riskPercentPerBasket;
|
||||
m_sizingMode = config.sizingMode;
|
||||
m_maxOpenBaskets = config.maxOpenBaskets;
|
||||
m_maxHoldingHours = config.maxHoldingHours;
|
||||
|
||||
// Calculate TP/SL amounts (e.g., 2x average expected profit)
|
||||
m_takeProfitAmount = 10.0; // Default $10, can be made configurable
|
||||
m_stopLossAmount = 15.0; // Default $15, can be made configurable
|
||||
|
||||
m_isInitialized = true;
|
||||
m_activeBasket.Reset();
|
||||
|
||||
Logger.Info("Position Manager initialized - Base Lot: " + DoubleToString(m_baseLotSize, 2) +
|
||||
", Sizing: " + EnumToString(m_sizingMode));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Open a new basket (coordinated 3-leg entry) |
|
||||
//+------------------------------------------------------------------+
|
||||
bool OpenBasket(ENUM_BASKET_SIGNAL direction, double zScore, double correlation)
|
||||
{
|
||||
if(!m_isInitialized || m_tradeWrapper == NULL)
|
||||
{
|
||||
Logger.Error("Position Manager not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(HasOpenBasket())
|
||||
{
|
||||
Logger.Warning("Cannot open new basket - basket already open");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(direction != SIGNAL_LONG_BASKET && direction != SIGNAL_SHORT_BASKET)
|
||||
{
|
||||
Logger.Error("Invalid basket direction");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset basket state
|
||||
m_activeBasket.Reset();
|
||||
m_basketCounter++;
|
||||
m_activeBasket.basketID = m_basketCounter;
|
||||
m_activeBasket.direction = direction;
|
||||
m_activeBasket.openTime = TimeCurrent();
|
||||
m_activeBasket.entryZScore = zScore;
|
||||
m_activeBasket.entryCorrelation = correlation;
|
||||
m_activeBasket.state = BASKET_ENTRY_PENDING;
|
||||
|
||||
// Get order types for each leg
|
||||
ENUM_ORDER_TYPE orderTypes[];
|
||||
GetBasketOrderTypes(direction, orderTypes);
|
||||
|
||||
// Calculate lot sizes
|
||||
double lotSizes[];
|
||||
ArrayResize(lotSizes, NUM_SYMBOLS);
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
lotSizes[i] = CalculateLotSize(m_symbols[i]);
|
||||
}
|
||||
|
||||
Logger.Info("Opening basket #" + IntegerToString(m_activeBasket.basketID) +
|
||||
" - Direction: " + (direction == SIGNAL_LONG_BASKET ? "LONG" : "SHORT"));
|
||||
|
||||
// Execute legs sequentially (AUDNZD first as reference)
|
||||
int executionOrder[] = {SYMBOL_AUDNZD, SYMBOL_AUDCAD, SYMBOL_NZDCAD};
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
int legIndex = executionOrder[i];
|
||||
string symbol = m_symbols[legIndex];
|
||||
ENUM_ORDER_TYPE orderType = orderTypes[legIndex];
|
||||
double lots = lotSizes[legIndex];
|
||||
|
||||
string comment = BASKET_COMMENT_PREFIX + IntegerToString(m_activeBasket.basketID);
|
||||
ulong ticket = 0;
|
||||
string errorMsg;
|
||||
|
||||
bool success = m_tradeWrapper.OpenPosition(symbol, orderType, lots, comment, ticket, errorMsg);
|
||||
|
||||
if(success)
|
||||
{
|
||||
// Record position
|
||||
m_activeBasket.positions[legIndex].ticket = ticket;
|
||||
m_activeBasket.positions[legIndex].symbol = symbol;
|
||||
m_activeBasket.positions[legIndex].symbolIndex = legIndex;
|
||||
m_activeBasket.positions[legIndex].type = (ENUM_POSITION_TYPE)orderType;
|
||||
m_activeBasket.positions[legIndex].lots = lots;
|
||||
m_activeBasket.positions[legIndex].openPrice = SymbolInfoDouble(symbol,
|
||||
orderType == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID);
|
||||
m_activeBasket.positions[legIndex].openTime = TimeCurrent();
|
||||
m_activeBasket.positions[legIndex].isOpen = true;
|
||||
m_activeBasket.positions[legIndex].comment = comment;
|
||||
|
||||
Logger.Debug("Leg " + IntegerToString(legIndex) + " opened - " + symbol +
|
||||
" " + (orderType == ORDER_TYPE_BUY ? "BUY" : "SELL") +
|
||||
" " + DoubleToString(lots, 2) + " lots");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to open leg " + IntegerToString(legIndex) + " (" + symbol + "): " + errorMsg);
|
||||
|
||||
// Rollback: close any legs that were opened
|
||||
if(i > 0)
|
||||
{
|
||||
Logger.Warning("Rolling back partial basket - closing opened legs");
|
||||
for(int j = 0; j < i; j++)
|
||||
{
|
||||
int rollbackIndex = executionOrder[j];
|
||||
if(m_activeBasket.positions[rollbackIndex].isOpen)
|
||||
{
|
||||
string closeError;
|
||||
m_tradeWrapper.ClosePosition(m_activeBasket.positions[rollbackIndex].ticket, closeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_activeBasket.Reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// All legs opened successfully
|
||||
m_activeBasket.state = BASKET_OPEN;
|
||||
m_activeBasket.lastUpdateTime = TimeCurrent();
|
||||
|
||||
Logger.LogBasketOpen(m_activeBasket.basketID, direction, zScore, correlation);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Close the active basket |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CloseBasket(ENUM_EXIT_REASON reason)
|
||||
{
|
||||
if(!HasOpenBasket())
|
||||
{
|
||||
Logger.Debug("No basket to close");
|
||||
return true;
|
||||
}
|
||||
|
||||
m_activeBasket.state = BASKET_EXIT_PENDING;
|
||||
m_activeBasket.exitReason = reason;
|
||||
|
||||
Logger.Info("Closing basket #" + IntegerToString(m_activeBasket.basketID) +
|
||||
" - Reason: " + EnumToString(reason));
|
||||
|
||||
bool allClosed = true;
|
||||
double totalPL = 0;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(!m_activeBasket.positions[i].isOpen)
|
||||
continue;
|
||||
|
||||
ulong ticket = m_activeBasket.positions[i].ticket;
|
||||
|
||||
// Get P&L before closing
|
||||
double pl = m_tradeWrapper.GetPositionProfit(ticket);
|
||||
totalPL += pl;
|
||||
|
||||
string errorMsg;
|
||||
if(m_tradeWrapper.ClosePosition(ticket, errorMsg))
|
||||
{
|
||||
m_activeBasket.positions[i].isOpen = false;
|
||||
m_activeBasket.positions[i].unrealizedPL = pl;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to close position " + IntegerToString(ticket) + ": " + errorMsg);
|
||||
allClosed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(allClosed)
|
||||
{
|
||||
m_activeBasket.realizedPL = totalPL;
|
||||
m_activeBasket.state = BASKET_CLOSED;
|
||||
|
||||
Logger.LogBasketClose(m_activeBasket.basketID, reason, totalPL, m_activeBasket.barsHeld);
|
||||
|
||||
// Reset basket state for next trade
|
||||
m_activeBasket.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_activeBasket.state = BASKET_PARTIAL;
|
||||
Logger.Error("Basket partially closed - manual intervention may be required");
|
||||
}
|
||||
|
||||
return allClosed;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Update basket state and P&L |
|
||||
//+------------------------------------------------------------------+
|
||||
void UpdateBasketState()
|
||||
{
|
||||
if(!HasOpenBasket())
|
||||
return;
|
||||
|
||||
double totalPL = 0;
|
||||
int openCount = 0;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(m_activeBasket.positions[i].isOpen)
|
||||
{
|
||||
UpdatePositionPL(i);
|
||||
|
||||
if(m_activeBasket.positions[i].isOpen) // Check again after update
|
||||
{
|
||||
totalPL += m_activeBasket.positions[i].unrealizedPL;
|
||||
openCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_activeBasket.unrealizedPL = totalPL;
|
||||
m_activeBasket.lastUpdateTime = TimeCurrent();
|
||||
|
||||
// Update basket state based on open positions
|
||||
if(openCount == 0)
|
||||
{
|
||||
Logger.Warning("All positions closed externally - resetting basket");
|
||||
m_activeBasket.Reset();
|
||||
}
|
||||
else if(openCount < NUM_SYMBOLS && m_activeBasket.state == BASKET_OPEN)
|
||||
{
|
||||
Logger.Warning("Basket is now partial - " + IntegerToString(openCount) + " legs open");
|
||||
m_activeBasket.state = BASKET_PARTIAL;
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if basket is open |
|
||||
//+------------------------------------------------------------------+
|
||||
bool HasOpenBasket()
|
||||
{
|
||||
return m_activeBasket.IsActive();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current basket state |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetBasketState(BasketState &state)
|
||||
{
|
||||
state = m_activeBasket;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get basket unrealized P&L |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetBasketPL()
|
||||
{
|
||||
return m_activeBasket.unrealizedPL;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current basket direction |
|
||||
//+------------------------------------------------------------------+
|
||||
ENUM_BASKET_SIGNAL GetBasketDirection()
|
||||
{
|
||||
return m_activeBasket.direction;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get take profit amount |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetTakeProfitAmount()
|
||||
{
|
||||
return m_takeProfitAmount;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get stop loss amount |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetStopLossAmount()
|
||||
{
|
||||
return m_stopLossAmount;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get max holding hours |
|
||||
//+------------------------------------------------------------------+
|
||||
int GetMaxHoldingHours()
|
||||
{
|
||||
return m_maxHoldingHours;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Set TP/SL amounts |
|
||||
//+------------------------------------------------------------------+
|
||||
void SetTPSL(double takeProfitAmount, double stopLossAmount)
|
||||
{
|
||||
m_takeProfitAmount = takeProfitAmount;
|
||||
m_stopLossAmount = stopLossAmount;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Recover basket state from open positions |
|
||||
//+------------------------------------------------------------------+
|
||||
void RecoverFromOpenPositions()
|
||||
{
|
||||
int magicNumber = m_tradeWrapper.GetMagicNumber();
|
||||
int positionsFound = 0;
|
||||
|
||||
int total = PositionsTotal();
|
||||
for(int i = 0; i < total; i++)
|
||||
{
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
if(ticket == 0)
|
||||
continue;
|
||||
|
||||
if(PositionGetInteger(POSITION_MAGIC) != magicNumber)
|
||||
continue;
|
||||
|
||||
string symbol = PositionGetString(POSITION_SYMBOL);
|
||||
|
||||
// Find symbol index
|
||||
int symbolIndex = -1;
|
||||
for(int j = 0; j < NUM_SYMBOLS; j++)
|
||||
{
|
||||
if(symbol == m_symbols[j])
|
||||
{
|
||||
symbolIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(symbolIndex >= 0)
|
||||
{
|
||||
m_activeBasket.positions[symbolIndex].ticket = ticket;
|
||||
m_activeBasket.positions[symbolIndex].symbol = symbol;
|
||||
m_activeBasket.positions[symbolIndex].symbolIndex = symbolIndex;
|
||||
m_activeBasket.positions[symbolIndex].type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
||||
m_activeBasket.positions[symbolIndex].lots = PositionGetDouble(POSITION_VOLUME);
|
||||
m_activeBasket.positions[symbolIndex].openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
m_activeBasket.positions[symbolIndex].openTime = (datetime)PositionGetInteger(POSITION_TIME);
|
||||
m_activeBasket.positions[symbolIndex].isOpen = true;
|
||||
m_activeBasket.positions[symbolIndex].comment = PositionGetString(POSITION_COMMENT);
|
||||
|
||||
positionsFound++;
|
||||
}
|
||||
}
|
||||
|
||||
if(positionsFound > 0)
|
||||
{
|
||||
Logger.Info("Recovered " + IntegerToString(positionsFound) + " positions from previous session");
|
||||
|
||||
if(positionsFound == NUM_SYMBOLS)
|
||||
{
|
||||
m_activeBasket.state = BASKET_OPEN;
|
||||
// Try to determine direction from position types
|
||||
if(m_activeBasket.positions[SYMBOL_AUDNZD].type == POSITION_TYPE_BUY)
|
||||
m_activeBasket.direction = SIGNAL_LONG_BASKET;
|
||||
else
|
||||
m_activeBasket.direction = SIGNAL_SHORT_BASKET;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_activeBasket.state = BASKET_PARTIAL;
|
||||
Logger.Warning("Incomplete basket recovered - may need manual intervention");
|
||||
}
|
||||
|
||||
m_activeBasket.basketID = ++m_basketCounter;
|
||||
m_activeBasket.openTime = m_activeBasket.positions[0].openTime;
|
||||
m_activeBasket.lastUpdateTime = TimeCurrent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_POSITIONMANAGER_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,452 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_RiskManager.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Risk Management Module |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_RISKMANAGER_MQH
|
||||
#define DBASKET_RISKMANAGER_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
#include "DBasket_Structures.mqh"
|
||||
#include "DBasket_Logger.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Risk Manager Class |
|
||||
//| Monitors and enforces risk limits with circuit breaker |
|
||||
//+------------------------------------------------------------------+
|
||||
class CRiskManager
|
||||
{
|
||||
private:
|
||||
// Configuration
|
||||
double m_maxDrawdownPercent;
|
||||
double m_warningDrawdownPercent;
|
||||
double m_maxDailyLossAmount;
|
||||
double m_maxDailyLossPercent;
|
||||
double m_minMarginLevel;
|
||||
double m_warningMarginLevel;
|
||||
int m_maxConsecutiveLosses;
|
||||
|
||||
// State tracking
|
||||
PerformanceMetrics m_metrics;
|
||||
ENUM_CIRCUIT_BREAKER_STATE m_cbState;
|
||||
string m_cbTripReason;
|
||||
datetime m_cbTripTime;
|
||||
|
||||
// Daily tracking
|
||||
datetime m_lastDailyReset;
|
||||
double m_dailyStartEquity;
|
||||
double m_dailyRealizedPL;
|
||||
int m_consecutiveLosses;
|
||||
|
||||
// Historical high
|
||||
double m_peakEquity;
|
||||
double m_startingBalance;
|
||||
|
||||
bool m_isInitialized;
|
||||
|
||||
public:
|
||||
//+------------------------------------------------------------------+
|
||||
//| Constructor |
|
||||
//+------------------------------------------------------------------+
|
||||
CRiskManager()
|
||||
{
|
||||
m_maxDrawdownPercent = DEFAULT_MAX_DRAWDOWN_PERCENT;
|
||||
m_warningDrawdownPercent = CB_WARNING_DRAWDOWN_PERCENT;
|
||||
m_maxDailyLossAmount = DEFAULT_DAILY_LOSS_LIMIT;
|
||||
m_maxDailyLossPercent = 5.0;
|
||||
m_minMarginLevel = DEFAULT_MIN_MARGIN_LEVEL;
|
||||
m_warningMarginLevel = DEFAULT_WARNING_MARGIN_LEVEL;
|
||||
m_maxConsecutiveLosses = CB_MAX_CONSECUTIVE_LOSSES;
|
||||
|
||||
m_cbState = CB_NORMAL;
|
||||
m_cbTripReason = "";
|
||||
m_cbTripTime = 0;
|
||||
|
||||
m_lastDailyReset = 0;
|
||||
m_dailyStartEquity = 0;
|
||||
m_dailyRealizedPL = 0;
|
||||
m_consecutiveLosses = 0;
|
||||
|
||||
m_peakEquity = 0;
|
||||
m_startingBalance = 0;
|
||||
|
||||
m_isInitialized = false;
|
||||
m_metrics.Reset();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize risk manager |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(const EAConfig &config)
|
||||
{
|
||||
m_maxDrawdownPercent = config.maxDrawdownPercent;
|
||||
m_warningDrawdownPercent = m_maxDrawdownPercent * 0.6; // 60% of max
|
||||
m_maxDailyLossAmount = config.maxDailyLossAmount;
|
||||
m_maxDailyLossPercent = config.maxDailyLossPercent;
|
||||
|
||||
// Initialize tracking
|
||||
m_startingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
||||
m_peakEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
m_dailyStartEquity = m_peakEquity;
|
||||
m_lastDailyReset = TimeCurrent();
|
||||
|
||||
m_metrics.Reset();
|
||||
m_metrics.startingBalance = m_startingBalance;
|
||||
m_metrics.peakEquity = m_peakEquity;
|
||||
m_metrics.metricsStartTime = TimeCurrent();
|
||||
m_metrics.dailyStartEquity = m_dailyStartEquity;
|
||||
m_metrics.dailyResetTime = m_lastDailyReset;
|
||||
|
||||
m_cbState = CB_NORMAL;
|
||||
m_isInitialized = true;
|
||||
|
||||
Logger.Info("Risk Manager initialized - Max DD: " + DoubleToString(m_maxDrawdownPercent, 1) +
|
||||
"%, Daily Limit: $" + DoubleToString(m_maxDailyLossAmount, 2));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Update metrics (call every tick or periodically) |
|
||||
//+------------------------------------------------------------------+
|
||||
void UpdateMetrics()
|
||||
{
|
||||
if(!m_isInitialized)
|
||||
return;
|
||||
|
||||
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
||||
|
||||
// Update peak equity
|
||||
if(currentEquity > m_peakEquity)
|
||||
m_peakEquity = currentEquity;
|
||||
|
||||
// Calculate current drawdown
|
||||
double drawdown = 0;
|
||||
if(m_peakEquity > 0)
|
||||
drawdown = ((m_peakEquity - currentEquity) / m_peakEquity) * 100;
|
||||
|
||||
// Check for daily reset
|
||||
CheckDailyReset();
|
||||
|
||||
// Update metrics structure
|
||||
m_metrics.currentEquity = currentEquity;
|
||||
m_metrics.currentBalance = currentBalance;
|
||||
m_metrics.peakEquity = m_peakEquity;
|
||||
m_metrics.currentDrawdownPercent = drawdown;
|
||||
m_metrics.dailyPnL = currentEquity - m_dailyStartEquity;
|
||||
m_metrics.uptimeSeconds = (int)(TimeCurrent() - m_metrics.metricsStartTime);
|
||||
|
||||
if(drawdown > m_metrics.maxDrawdownPercent)
|
||||
{
|
||||
m_metrics.maxDrawdownPercent = drawdown;
|
||||
m_metrics.maxDrawdownValue = m_peakEquity - currentEquity;
|
||||
}
|
||||
|
||||
// Update win rate
|
||||
if(m_metrics.closedBaskets > 0)
|
||||
m_metrics.winRate = (double)m_metrics.winningBaskets / m_metrics.closedBaskets;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check for daily reset |
|
||||
//+------------------------------------------------------------------+
|
||||
void CheckDailyReset()
|
||||
{
|
||||
MqlDateTime dtNow, dtLast;
|
||||
TimeToStruct(TimeCurrent(), dtNow);
|
||||
TimeToStruct(m_lastDailyReset, dtLast);
|
||||
|
||||
// Check if day changed
|
||||
if(dtNow.day != dtLast.day || dtNow.mon != dtLast.mon || dtNow.year != dtLast.year)
|
||||
{
|
||||
// New trading day
|
||||
m_dailyStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
m_dailyRealizedPL = 0;
|
||||
m_lastDailyReset = TimeCurrent();
|
||||
|
||||
m_metrics.dailyStartEquity = m_dailyStartEquity;
|
||||
m_metrics.dailyResetTime = m_lastDailyReset;
|
||||
m_metrics.dailyPnL = 0;
|
||||
|
||||
Logger.Info("Daily reset - New equity baseline: " + DoubleToString(m_dailyStartEquity, 2));
|
||||
|
||||
// Reset circuit breaker if tripped due to daily limits or consecutive losses
|
||||
if(m_cbState == CB_TRIPPED)
|
||||
{
|
||||
if(m_cbTripReason == "Daily loss limit exceeded" ||
|
||||
StringFind(m_cbTripReason, "Maximum consecutive losses") >= 0)
|
||||
{
|
||||
Logger.Info("Resetting circuit breaker after daily reset");
|
||||
ResetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check all risk limits |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckRiskLimits(string &failReason)
|
||||
{
|
||||
failReason = "";
|
||||
|
||||
if(!m_isInitialized)
|
||||
return true;
|
||||
|
||||
// Update metrics first
|
||||
UpdateMetrics();
|
||||
|
||||
// If already tripped, stay tripped
|
||||
if(m_cbState == CB_TRIPPED)
|
||||
{
|
||||
failReason = "Circuit breaker tripped: " + m_cbTripReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
ENUM_CIRCUIT_BREAKER_STATE newState = CB_NORMAL;
|
||||
string reason = "";
|
||||
|
||||
// Check 1: Drawdown limit
|
||||
if(m_metrics.currentDrawdownPercent >= m_maxDrawdownPercent)
|
||||
{
|
||||
reason = "Maximum drawdown exceeded: " + DoubleToString(m_metrics.currentDrawdownPercent, 2) + "%";
|
||||
newState = CB_TRIPPED;
|
||||
}
|
||||
else if(m_metrics.currentDrawdownPercent >= m_warningDrawdownPercent)
|
||||
{
|
||||
reason = "Approaching drawdown limit: " + DoubleToString(m_metrics.currentDrawdownPercent, 2) + "%";
|
||||
if(newState < CB_WARNING)
|
||||
newState = CB_WARNING;
|
||||
}
|
||||
|
||||
// Check 2: Daily loss limit
|
||||
if(m_metrics.dailyPnL <= -m_maxDailyLossAmount)
|
||||
{
|
||||
reason = "Daily loss limit exceeded: $" + DoubleToString(MathAbs(m_metrics.dailyPnL), 2);
|
||||
newState = CB_TRIPPED;
|
||||
}
|
||||
else if(m_maxDailyLossPercent > 0)
|
||||
{
|
||||
double dailyLossPercent = MathAbs(m_metrics.dailyPnL) / m_dailyStartEquity * 100;
|
||||
if(m_metrics.dailyPnL < 0 && dailyLossPercent >= m_maxDailyLossPercent)
|
||||
{
|
||||
reason = "Daily loss % exceeded: " + DoubleToString(dailyLossPercent, 2) + "%";
|
||||
newState = CB_TRIPPED;
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: Margin level
|
||||
double marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
||||
if(marginLevel > 0) // 0 means no positions
|
||||
{
|
||||
if(marginLevel < m_minMarginLevel)
|
||||
{
|
||||
reason = "Margin level critical: " + DoubleToString(marginLevel, 0) + "%";
|
||||
newState = CB_TRIPPED;
|
||||
}
|
||||
else if(marginLevel < m_warningMarginLevel)
|
||||
{
|
||||
if(reason == "")
|
||||
reason = "Margin level warning: " + DoubleToString(marginLevel, 0) + "%";
|
||||
if(newState < CB_WARNING)
|
||||
newState = CB_WARNING;
|
||||
}
|
||||
}
|
||||
|
||||
// Check 4: Consecutive losses
|
||||
if(m_consecutiveLosses >= m_maxConsecutiveLosses)
|
||||
{
|
||||
reason = "Maximum consecutive losses: " + IntegerToString(m_consecutiveLosses);
|
||||
newState = CB_TRIPPED;
|
||||
}
|
||||
|
||||
// Apply state change
|
||||
if(newState > m_cbState)
|
||||
{
|
||||
m_cbState = newState;
|
||||
|
||||
if(newState == CB_TRIPPED)
|
||||
{
|
||||
m_cbTripReason = reason;
|
||||
m_cbTripTime = TimeCurrent();
|
||||
Logger.Error("CIRCUIT BREAKER TRIPPED: " + reason);
|
||||
}
|
||||
else if(newState == CB_WARNING)
|
||||
{
|
||||
Logger.Warning("RISK WARNING: " + reason);
|
||||
}
|
||||
}
|
||||
|
||||
failReason = reason;
|
||||
return (m_cbState != CB_TRIPPED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Record a basket close result |
|
||||
//+------------------------------------------------------------------+
|
||||
void RecordBasketClose(double pl, bool isWin)
|
||||
{
|
||||
m_metrics.closedBaskets++;
|
||||
m_dailyRealizedPL += pl;
|
||||
m_metrics.realizedPL += pl;
|
||||
|
||||
if(isWin)
|
||||
{
|
||||
m_metrics.winningBaskets++;
|
||||
m_consecutiveLosses = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_metrics.losingBaskets++;
|
||||
m_consecutiveLosses++;
|
||||
|
||||
if(m_consecutiveLosses > m_metrics.maxConsecutiveLosses)
|
||||
m_metrics.maxConsecutiveLosses = m_consecutiveLosses;
|
||||
}
|
||||
|
||||
m_metrics.consecutiveLosses = m_consecutiveLosses;
|
||||
|
||||
// Update win rate
|
||||
if(m_metrics.closedBaskets > 0)
|
||||
m_metrics.winRate = (double)m_metrics.winningBaskets / m_metrics.closedBaskets;
|
||||
|
||||
// Calculate profit factor
|
||||
double totalWins = 0, totalLosses = 0;
|
||||
// (Would need to track these separately for accurate profit factor)
|
||||
|
||||
Logger.Debug("Basket recorded - P/L: $" + DoubleToString(pl, 2) +
|
||||
", Win Rate: " + DoubleToString(m_metrics.winRate * 100, 1) + "%" +
|
||||
", Consecutive Losses: " + IntegerToString(m_consecutiveLosses));
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Record basket open |
|
||||
//+------------------------------------------------------------------+
|
||||
void RecordBasketOpen()
|
||||
{
|
||||
m_metrics.totalBaskets++;
|
||||
m_metrics.executedSignals++;
|
||||
m_metrics.lastTradeTime = TimeCurrent();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Record signal generation |
|
||||
//+------------------------------------------------------------------+
|
||||
void RecordSignal(bool executed)
|
||||
{
|
||||
m_metrics.totalSignals++;
|
||||
if(!executed)
|
||||
m_metrics.filteredSignals++;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if trading is allowed |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsTradingAllowed()
|
||||
{
|
||||
return (m_cbState != CB_TRIPPED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get circuit breaker state |
|
||||
//+------------------------------------------------------------------+
|
||||
ENUM_CIRCUIT_BREAKER_STATE GetCircuitBreakerState()
|
||||
{
|
||||
return m_cbState;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Reset circuit breaker (manual reset) |
|
||||
//+------------------------------------------------------------------+
|
||||
void ResetCircuitBreaker()
|
||||
{
|
||||
if(m_cbState == CB_TRIPPED)
|
||||
{
|
||||
Logger.Info("Circuit breaker reset - Previous reason: " + m_cbTripReason);
|
||||
m_cbState = CB_NORMAL;
|
||||
m_cbTripReason = "";
|
||||
m_cbTripTime = 0;
|
||||
m_consecutiveLosses = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get performance metrics |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetMetrics(PerformanceMetrics &metrics)
|
||||
{
|
||||
metrics = m_metrics;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current drawdown |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetCurrentDrawdown()
|
||||
{
|
||||
return m_metrics.currentDrawdownPercent;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get daily P&L |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetDailyPnL()
|
||||
{
|
||||
return m_metrics.dailyPnL;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check emergency exit conditions |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckEmergencyExit(string &reason)
|
||||
{
|
||||
reason = "";
|
||||
|
||||
// Check margin level emergency
|
||||
double marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
||||
if(marginLevel > 0 && marginLevel < 150) // Very critical
|
||||
{
|
||||
reason = "Emergency: Margin call imminent (" + DoubleToString(marginLevel, 0) + "%)";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Display metrics on chart |
|
||||
//+------------------------------------------------------------------+
|
||||
void DisplayMetricsOnChart()
|
||||
{
|
||||
string status = (m_cbState == CB_TRIPPED) ? "HALTED" :
|
||||
(m_cbState == CB_WARNING) ? "WARNING" : "NORMAL";
|
||||
|
||||
string display = StringFormat(
|
||||
"=== D-Basket EA Risk Monitor ===\n" +
|
||||
"Status: %s\n" +
|
||||
"Net P/L: $%.2f (%.1f%%)\n" +
|
||||
"Daily P/L: $%.2f\n" +
|
||||
"Drawdown: %.2f%% (Max: %.2f%%)\n" +
|
||||
"Baskets: %d | Win Rate: %.1f%%\n" +
|
||||
"Consecutive Losses: %d",
|
||||
status,
|
||||
m_metrics.currentEquity - m_startingBalance,
|
||||
((m_metrics.currentEquity - m_startingBalance) / m_startingBalance) * 100,
|
||||
m_metrics.dailyPnL,
|
||||
m_metrics.currentDrawdownPercent,
|
||||
m_metrics.maxDrawdownPercent,
|
||||
m_metrics.closedBaskets,
|
||||
m_metrics.winRate * 100,
|
||||
m_consecutiveLosses
|
||||
);
|
||||
|
||||
Comment(display);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_RISKMANAGER_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,431 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_SignalEngine.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Signal Generation Module |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_SIGNALENGINE_MQH
|
||||
#define DBASKET_SIGNALENGINE_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
#include "DBasket_Structures.mqh"
|
||||
#include "DBasket_Logger.mqh"
|
||||
#include "DBasket_CorrelationEngine.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Signal Engine Class |
|
||||
//| Generates entry/exit signals with multi-stage filtering |
|
||||
//+------------------------------------------------------------------+
|
||||
class CSignalEngine
|
||||
{
|
||||
private:
|
||||
// Configuration
|
||||
string m_symbols[NUM_SYMBOLS];
|
||||
double m_zScoreEntry; // Entry threshold
|
||||
double m_zScoreExit; // Exit threshold
|
||||
double m_minCorrelation; // Minimum correlation
|
||||
double m_maxSpreadPips; // Maximum spread (pips)
|
||||
double m_maxATRMultiple; // Volatility filter
|
||||
|
||||
// Trading hours
|
||||
int m_startHour;
|
||||
int m_startMinute;
|
||||
int m_endHour;
|
||||
int m_endMinute;
|
||||
bool m_avoidRollover;
|
||||
|
||||
// ATR handles for volatility calculation
|
||||
int m_atrHandles[NUM_SYMBOLS];
|
||||
|
||||
// State
|
||||
bool m_isInitialized;
|
||||
int m_signalPersistCount; // For signal persistence filter
|
||||
ENUM_BASKET_SIGNAL m_lastSignal; // Last detected signal
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current spread in pips for symbol |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetSpreadPips(string symbol)
|
||||
{
|
||||
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
||||
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
||||
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
||||
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
||||
|
||||
double spread = ask - bid;
|
||||
|
||||
// Convert to pips (handle 5-digit and 3-digit brokers)
|
||||
double pipSize = (digits == 3 || digits == 5) ? point * 10 : point;
|
||||
|
||||
return spread / pipSize;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if within trading hours |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsWithinTradingHours()
|
||||
{
|
||||
datetime serverTime = TimeCurrent();
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(serverTime, dt);
|
||||
|
||||
int currentMinutes = dt.hour * 60 + dt.min;
|
||||
int startMinutes = m_startHour * 60 + m_startMinute;
|
||||
int endMinutes = m_endHour * 60 + m_endMinute;
|
||||
|
||||
// Handle case where trading window crosses midnight
|
||||
if(startMinutes <= endMinutes)
|
||||
{
|
||||
return (currentMinutes >= startMinutes && currentMinutes <= endMinutes);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (currentMinutes >= startMinutes || currentMinutes <= endMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if in rollover period (21:00-23:59 typically) |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsRolloverPeriod()
|
||||
{
|
||||
if(!m_avoidRollover)
|
||||
return false;
|
||||
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(TimeCurrent(), dt);
|
||||
|
||||
// Rollover typically 21:00-00:10 broker time
|
||||
return (dt.hour >= 21 || (dt.hour == 0 && dt.min <= 10));
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check spread filter for all symbols |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckSpreadFilter(string &failReason)
|
||||
{
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
double spreadPips = GetSpreadPips(m_symbols[i]);
|
||||
|
||||
if(spreadPips > m_maxSpreadPips)
|
||||
{
|
||||
failReason = "Spread too high on " + m_symbols[i] +
|
||||
": " + DoubleToString(spreadPips, 2) + " pips";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check volatility filter using ATR |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckVolatilityFilter(string &failReason)
|
||||
{
|
||||
// Use AUDNZD as reference for volatility check
|
||||
if(m_atrHandles[SYMBOL_AUDNZD] == INVALID_HANDLE)
|
||||
return true; // Skip if ATR not available
|
||||
|
||||
double atrBuffer[];
|
||||
ArraySetAsSeries(atrBuffer, true);
|
||||
|
||||
// Get current and average ATR
|
||||
if(CopyBuffer(m_atrHandles[SYMBOL_AUDNZD], 0, 0, 20, atrBuffer) < 20)
|
||||
return true; // Skip if insufficient data
|
||||
|
||||
double currentATR = atrBuffer[0];
|
||||
double avgATR = 0;
|
||||
for(int i = 1; i < 20; i++)
|
||||
avgATR += atrBuffer[i];
|
||||
avgATR /= 19;
|
||||
|
||||
if(avgATR > 0 && currentATR > avgATR * m_maxATRMultiple)
|
||||
{
|
||||
failReason = "Volatility spike detected: ATR " +
|
||||
DoubleToString(currentATR / avgATR, 2) + "x average";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
//+------------------------------------------------------------------+
|
||||
//| Constructor |
|
||||
//+------------------------------------------------------------------+
|
||||
CSignalEngine()
|
||||
{
|
||||
m_zScoreEntry = 2.5;
|
||||
m_zScoreExit = 0.5;
|
||||
m_minCorrelation = 0.75;
|
||||
m_maxSpreadPips = 3.0;
|
||||
m_maxATRMultiple = 2.0;
|
||||
m_startHour = 0;
|
||||
m_startMinute = 0;
|
||||
m_endHour = 23;
|
||||
m_endMinute = 59;
|
||||
m_avoidRollover = true;
|
||||
m_isInitialized = false;
|
||||
m_signalPersistCount = 0;
|
||||
m_lastSignal = SIGNAL_NONE;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
m_symbols[i] = "";
|
||||
m_atrHandles[i] = INVALID_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Destructor |
|
||||
//+------------------------------------------------------------------+
|
||||
~CSignalEngine()
|
||||
{
|
||||
// Release ATR handles
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(m_atrHandles[i] != INVALID_HANDLE)
|
||||
{
|
||||
IndicatorRelease(m_atrHandles[i]);
|
||||
m_atrHandles[i] = INVALID_HANDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize signal engine |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(const EAConfig &config)
|
||||
{
|
||||
// Copy configuration
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
m_symbols[i] = config.symbols[i];
|
||||
|
||||
m_zScoreEntry = config.zScoreEntryThreshold;
|
||||
m_zScoreExit = config.zScoreExitThreshold;
|
||||
m_minCorrelation = config.minCorrelation;
|
||||
m_maxSpreadPips = config.maxSpreadPips;
|
||||
|
||||
m_startHour = config.tradingStartHour;
|
||||
m_startMinute = config.tradingStartMinute;
|
||||
m_endHour = config.tradingEndHour;
|
||||
m_endMinute = config.tradingEndMinute;
|
||||
m_avoidRollover = config.avoidRollover;
|
||||
|
||||
// Create ATR handles for volatility filtering
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
m_atrHandles[i] = iATR(m_symbols[i], config.timeframe, 14);
|
||||
if(m_atrHandles[i] == INVALID_HANDLE)
|
||||
{
|
||||
Logger.Warning("Failed to create ATR handle for " + m_symbols[i]);
|
||||
}
|
||||
}
|
||||
|
||||
m_isInitialized = true;
|
||||
|
||||
Logger.Info("Signal Engine initialized - Entry Z: " + DoubleToString(m_zScoreEntry, 2) +
|
||||
", Exit Z: " + DoubleToString(m_zScoreExit, 2) +
|
||||
", Min Corr: " + DoubleToString(m_minCorrelation, 2));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check for entry signal with all filters |
|
||||
//+------------------------------------------------------------------+
|
||||
ENUM_BASKET_SIGNAL CheckEntrySignal(const CorrelationData &corrData, bool basketOpen, string &failReason)
|
||||
{
|
||||
failReason = "";
|
||||
|
||||
// Stage 1: Data validity
|
||||
if(!corrData.isValid)
|
||||
{
|
||||
failReason = "Correlation data invalid: " + corrData.invalidReason;
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 2: Check if basket already open
|
||||
if(basketOpen)
|
||||
{
|
||||
failReason = "Basket already open";
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 3: Trading hours filter
|
||||
if(!IsWithinTradingHours())
|
||||
{
|
||||
failReason = "Outside trading hours";
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 4: Rollover filter
|
||||
if(IsRolloverPeriod())
|
||||
{
|
||||
failReason = "Rollover period";
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 5: Spread filter
|
||||
if(!CheckSpreadFilter(failReason))
|
||||
{
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 6: Correlation stability filter
|
||||
if(corrData.corrAUDCAD_NZDCAD < m_minCorrelation)
|
||||
{
|
||||
failReason = "Correlation too low: " + DoubleToString(corrData.corrAUDCAD_NZDCAD, 4);
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 7: Volatility filter
|
||||
if(!CheckVolatilityFilter(failReason))
|
||||
{
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Stage 8: Z-score threshold check
|
||||
double zScore = corrData.spreadZScore;
|
||||
|
||||
if(MathAbs(zScore) <= m_zScoreEntry)
|
||||
{
|
||||
failReason = "Z-score below threshold: " + DoubleToString(zScore, 2);
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Determine direction
|
||||
ENUM_BASKET_SIGNAL signal = SIGNAL_NONE;
|
||||
|
||||
if(zScore < -m_zScoreEntry)
|
||||
{
|
||||
// Negative z-score: AUDNZD underpriced relative to ratio
|
||||
// Expect AUDNZD to rise (or ratio to fall)
|
||||
signal = SIGNAL_LONG_BASKET;
|
||||
Logger.Info("LONG basket signal generated - Z-Score: " + DoubleToString(zScore, 2));
|
||||
}
|
||||
else if(zScore > m_zScoreEntry)
|
||||
{
|
||||
// Positive z-score: AUDNZD overpriced relative to ratio
|
||||
// Expect AUDNZD to fall (or ratio to rise)
|
||||
signal = SIGNAL_SHORT_BASKET;
|
||||
Logger.Info("SHORT basket signal generated - Z-Score: " + DoubleToString(zScore, 2));
|
||||
}
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check for exit signal |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckExitSignal(const CorrelationData &corrData, const BasketState &basket,
|
||||
double takeProfitAmount, double stopLossAmount,
|
||||
int maxHoldingHours, ENUM_EXIT_REASON &exitReason)
|
||||
{
|
||||
exitReason = EXIT_MANUAL;
|
||||
|
||||
// Check if basket is active
|
||||
if(!basket.IsActive())
|
||||
return false;
|
||||
|
||||
// Exit 1: Mean reversion (z-score returned to near zero)
|
||||
if(corrData.isValid)
|
||||
{
|
||||
double currentZ = corrData.spreadZScore;
|
||||
|
||||
// For long basket, we entered when z < -entry, exit when z > -exit
|
||||
// For short basket, we entered when z > +entry, exit when z < +exit
|
||||
if(basket.direction == SIGNAL_LONG_BASKET && currentZ > -m_zScoreExit)
|
||||
{
|
||||
exitReason = EXIT_MEAN_REVERSION;
|
||||
Logger.Info("Exit signal: Mean reversion (Z: " + DoubleToString(currentZ, 2) + ")");
|
||||
return true;
|
||||
}
|
||||
else if(basket.direction == SIGNAL_SHORT_BASKET && currentZ < m_zScoreExit)
|
||||
{
|
||||
exitReason = EXIT_MEAN_REVERSION;
|
||||
Logger.Info("Exit signal: Mean reversion (Z: " + DoubleToString(currentZ, 2) + ")");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit 2: Take profit
|
||||
if(takeProfitAmount > 0 && basket.unrealizedPL >= takeProfitAmount)
|
||||
{
|
||||
exitReason = EXIT_TAKE_PROFIT;
|
||||
Logger.Info("Exit signal: Take profit reached (" + DoubleToString(basket.unrealizedPL, 2) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exit 3: Stop loss
|
||||
if(stopLossAmount > 0 && basket.unrealizedPL <= -stopLossAmount)
|
||||
{
|
||||
exitReason = EXIT_STOP_LOSS;
|
||||
Logger.Warning("Exit signal: Stop loss triggered (" + DoubleToString(basket.unrealizedPL, 2) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exit 4: Maximum holding time
|
||||
if(maxHoldingHours > 0)
|
||||
{
|
||||
int holdingSeconds = (int)(TimeCurrent() - basket.openTime);
|
||||
int holdingHours = holdingSeconds / 3600;
|
||||
|
||||
if(holdingHours >= maxHoldingHours)
|
||||
{
|
||||
exitReason = EXIT_MAX_TIME;
|
||||
Logger.Info("Exit signal: Max holding time (" + IntegerToString(holdingHours) + " hours)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit 5: Correlation breakdown
|
||||
if(corrData.isValid && corrData.corrAUDCAD_NZDCAD < 0.5)
|
||||
{
|
||||
exitReason = EXIT_CORRELATION_BREAK;
|
||||
Logger.Warning("Exit signal: Correlation breakdown (" + DoubleToString(corrData.corrAUDCAD_NZDCAD, 4) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get current spread summary |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetTotalSpreadPips()
|
||||
{
|
||||
double total = 0;
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
total += GetSpreadPips(m_symbols[i]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if trading is allowed (time and conditions) |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsTradingAllowed()
|
||||
{
|
||||
return IsWithinTradingHours() && !IsRolloverPeriod();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Update configuration |
|
||||
//+------------------------------------------------------------------+
|
||||
void UpdateConfig(double zScoreEntry, double zScoreExit, double minCorrelation, double maxSpread)
|
||||
{
|
||||
m_zScoreEntry = zScoreEntry;
|
||||
m_zScoreExit = zScoreExit;
|
||||
m_minCorrelation = minCorrelation;
|
||||
m_maxSpreadPips = maxSpread;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_SIGNALENGINE_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,524 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_Structures.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Core Data Structures |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_STRUCTURES_MQH
|
||||
#define DBASKET_STRUCTURES_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Correlation Data Structure |
|
||||
//| Encapsulates all correlation engine outputs |
|
||||
//+------------------------------------------------------------------+
|
||||
struct CorrelationData
|
||||
{
|
||||
// Primary correlation coefficients
|
||||
double corrAUDCAD_NZDCAD; // Main correlation: AUDCAD vs NZDCAD
|
||||
double corrAUDCAD_AUDNZD; // Validation: AUDCAD vs AUDNZD
|
||||
double corrNZDCAD_AUDNZD; // Validation: NZDCAD vs AUDNZD
|
||||
|
||||
// Spread and divergence metrics
|
||||
double syntheticRatio; // AUDCAD / NZDCAD
|
||||
double actualAUDNZD; // Current AUDNZD close price
|
||||
double spreadValue; // syntheticRatio - actualAUDNZD
|
||||
double spreadZScore; // Z-score of current spread
|
||||
|
||||
// Statistical parameters
|
||||
double spreadMean; // Historical mean of spread
|
||||
double spreadStdDev; // Historical standard deviation
|
||||
|
||||
// Metadata
|
||||
datetime calculationTime; // Timestamp of last calculation
|
||||
bool isValid; // False if insufficient data or error
|
||||
int lookbackPeriod; // Number of bars used
|
||||
string invalidReason; // Description if isValid == false
|
||||
|
||||
// Constructor
|
||||
void CorrelationData()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
// Reset to default values
|
||||
void Reset()
|
||||
{
|
||||
corrAUDCAD_NZDCAD = 0;
|
||||
corrAUDCAD_AUDNZD = 0;
|
||||
corrNZDCAD_AUDNZD = 0;
|
||||
syntheticRatio = 0;
|
||||
actualAUDNZD = 0;
|
||||
spreadValue = 0;
|
||||
spreadZScore = 0;
|
||||
spreadMean = 0;
|
||||
spreadStdDev = 0;
|
||||
calculationTime = 0;
|
||||
isValid = false;
|
||||
lookbackPeriod = 0;
|
||||
invalidReason = "";
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Position State Structure |
|
||||
//| Track individual position within a basket |
|
||||
//+------------------------------------------------------------------+
|
||||
struct PositionState
|
||||
{
|
||||
// Position identification
|
||||
ulong ticket; // MT5 position ticket
|
||||
string symbol; // Symbol name
|
||||
int symbolIndex; // 0=AUDCAD, 1=NZDCAD, 2=AUDNZD
|
||||
|
||||
// Position parameters
|
||||
ENUM_POSITION_TYPE type; // POSITION_TYPE_BUY or SELL
|
||||
double lots; // Position volume
|
||||
double openPrice; // Entry price
|
||||
datetime openTime; // Position open timestamp
|
||||
|
||||
// Risk management
|
||||
double stopLoss; // SL price (0 if none)
|
||||
double takeProfit; // TP price (0 if none)
|
||||
|
||||
// P&L tracking
|
||||
double currentPrice; // Last known price
|
||||
double unrealizedPL; // Floating profit/loss
|
||||
double swap; // Accumulated swap
|
||||
double commission; // Commission paid
|
||||
|
||||
// State flags
|
||||
bool isOpen; // True if position exists
|
||||
string comment; // Position comment
|
||||
|
||||
// Constructor
|
||||
void PositionState()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
// Reset to default values
|
||||
void Reset()
|
||||
{
|
||||
ticket = 0;
|
||||
symbol = "";
|
||||
symbolIndex = -1;
|
||||
type = POSITION_TYPE_BUY;
|
||||
lots = 0;
|
||||
openPrice = 0;
|
||||
openTime = 0;
|
||||
stopLoss = 0;
|
||||
takeProfit = 0;
|
||||
currentPrice = 0;
|
||||
unrealizedPL = 0;
|
||||
swap = 0;
|
||||
commission = 0;
|
||||
isOpen = false;
|
||||
comment = "";
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Basket State Structure |
|
||||
//| Tracks a complete 3-leg basket |
|
||||
//+------------------------------------------------------------------+
|
||||
struct BasketState
|
||||
{
|
||||
// Basket identification
|
||||
int basketID; // Unique basket identifier
|
||||
ENUM_BASKET_STATE state; // Current basket state
|
||||
ENUM_BASKET_SIGNAL direction; // LONG or SHORT basket
|
||||
|
||||
// Timing
|
||||
datetime openTime; // Basket creation timestamp
|
||||
datetime lastUpdateTime; // Last state update
|
||||
int barsHeld; // Number of bars position held
|
||||
|
||||
// Position tracking for each leg
|
||||
PositionState positions[NUM_SYMBOLS]; // All three legs
|
||||
|
||||
// Entry conditions snapshot
|
||||
double entryZScore; // Z-score at entry
|
||||
double entryCorrelation; // Primary correlation at entry
|
||||
double entrySpread; // Spread value at entry
|
||||
|
||||
// P&L tracking
|
||||
double unrealizedPL; // Total floating P&L
|
||||
double realizedPL; // Realized P&L (if partially closed)
|
||||
|
||||
// Exit tracking
|
||||
ENUM_EXIT_REASON exitReason; // Reason for exit (when closed)
|
||||
|
||||
// Constructor
|
||||
void BasketState()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
// Reset to default values
|
||||
void Reset()
|
||||
{
|
||||
basketID = 0;
|
||||
state = BASKET_NONE;
|
||||
direction = SIGNAL_NONE;
|
||||
openTime = 0;
|
||||
lastUpdateTime = 0;
|
||||
barsHeld = 0;
|
||||
entryZScore = 0;
|
||||
entryCorrelation = 0;
|
||||
entrySpread = 0;
|
||||
unrealizedPL = 0;
|
||||
realizedPL = 0;
|
||||
exitReason = EXIT_MANUAL;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
positions[i].Reset();
|
||||
}
|
||||
|
||||
// Check if basket is active (has open positions)
|
||||
bool IsActive() const
|
||||
{
|
||||
return (state == BASKET_OPEN || state == BASKET_PARTIAL);
|
||||
}
|
||||
|
||||
// Get total lots across all legs
|
||||
double GetTotalLots() const
|
||||
{
|
||||
double total = 0;
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
if(positions[i].isOpen)
|
||||
total += positions[i].lots;
|
||||
return total;
|
||||
}
|
||||
|
||||
// Count open legs
|
||||
int CountOpenLegs() const
|
||||
{
|
||||
int count = 0;
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
if(positions[i].isOpen)
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Performance Metrics Structure |
|
||||
//| Track EA performance in real-time |
|
||||
//+------------------------------------------------------------------+
|
||||
struct PerformanceMetrics
|
||||
{
|
||||
// Account metrics
|
||||
double startingBalance; // Initial balance at EA start
|
||||
double currentBalance; // Current balance
|
||||
double currentEquity; // Current equity
|
||||
double peakEquity; // Highest equity reached
|
||||
|
||||
// P&L tracking
|
||||
double realizedPL; // Total closed P&L
|
||||
double unrealizedPL; // Total floating P&L
|
||||
double netPL; // realizedPL + unrealizedPL
|
||||
|
||||
// Trade statistics
|
||||
int totalBaskets; // Total baskets opened
|
||||
int closedBaskets; // Total baskets closed
|
||||
int winningBaskets; // Profitable closes
|
||||
int losingBaskets; // Loss closes
|
||||
double winRate; // winningBaskets / closedBaskets
|
||||
double avgWin; // Average winning basket P&L
|
||||
double avgLoss; // Average losing basket P&L
|
||||
double profitFactor; // Sum(wins) / abs(Sum(losses))
|
||||
|
||||
// Risk metrics
|
||||
double currentDrawdownPercent; // Current drawdown from peak
|
||||
double maxDrawdownPercent; // Maximum drawdown
|
||||
double maxDrawdownValue; // Max drawdown in currency
|
||||
|
||||
// Daily tracking
|
||||
double dailyPnL; // Today's P&L
|
||||
double dailyStartEquity; // Equity at day start
|
||||
datetime dailyResetTime; // Last daily reset timestamp
|
||||
int consecutiveLosses; // Current losing streak
|
||||
int maxConsecutiveLosses; // Worst losing streak
|
||||
|
||||
// Operational metrics
|
||||
int totalSignals; // Signals generated
|
||||
int executedSignals; // Signals that became trades
|
||||
int filteredSignals; // Signals blocked by filters
|
||||
int erroredTrades; // Trade execution errors
|
||||
datetime lastTradeTime; // Last basket open/close
|
||||
|
||||
// Timing
|
||||
datetime metricsStartTime; // When tracking started
|
||||
int uptimeSeconds; // Seconds since start
|
||||
|
||||
// Constructor
|
||||
void PerformanceMetrics()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
// Reset to default values
|
||||
void Reset()
|
||||
{
|
||||
startingBalance = 0;
|
||||
currentBalance = 0;
|
||||
currentEquity = 0;
|
||||
peakEquity = 0;
|
||||
realizedPL = 0;
|
||||
unrealizedPL = 0;
|
||||
netPL = 0;
|
||||
totalBaskets = 0;
|
||||
closedBaskets = 0;
|
||||
winningBaskets = 0;
|
||||
losingBaskets = 0;
|
||||
winRate = 0;
|
||||
avgWin = 0;
|
||||
avgLoss = 0;
|
||||
profitFactor = 0;
|
||||
currentDrawdownPercent = 0;
|
||||
maxDrawdownPercent = 0;
|
||||
maxDrawdownValue = 0;
|
||||
dailyPnL = 0;
|
||||
dailyStartEquity = 0;
|
||||
dailyResetTime = 0;
|
||||
consecutiveLosses = 0;
|
||||
maxConsecutiveLosses = 0;
|
||||
totalSignals = 0;
|
||||
executedSignals = 0;
|
||||
filteredSignals = 0;
|
||||
erroredTrades = 0;
|
||||
lastTradeTime = 0;
|
||||
metricsStartTime = 0;
|
||||
uptimeSeconds = 0;
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Trade Log Entry Structure |
|
||||
//| Record of trade operations for audit |
|
||||
//+------------------------------------------------------------------+
|
||||
struct TradeLogEntry
|
||||
{
|
||||
// Trade identification
|
||||
int entryID; // Sequential log entry number
|
||||
datetime timestamp; // Operation timestamp
|
||||
string operation; // Operation type description
|
||||
|
||||
// Trade details
|
||||
int basketID; // Basket identifier (-1 if N/A)
|
||||
string symbol; // Symbol traded
|
||||
ulong ticket; // Position ticket
|
||||
ENUM_ORDER_TYPE orderType; // Buy/sell
|
||||
double lots; // Volume
|
||||
double price; // Execution price
|
||||
|
||||
// Outcome
|
||||
bool success; // Operation succeeded
|
||||
int errorCode; // MT5 error code
|
||||
string errorDescription; // Error message
|
||||
double pl; // P&L (for closes)
|
||||
|
||||
// Context
|
||||
double accountBalance; // Balance at operation
|
||||
double accountEquity; // Equity at operation
|
||||
double zScore; // Z-score at operation
|
||||
double correlation; // Correlation at operation
|
||||
|
||||
// Constructor
|
||||
void TradeLogEntry()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
// Reset
|
||||
void Reset()
|
||||
{
|
||||
entryID = 0;
|
||||
timestamp = 0;
|
||||
operation = "";
|
||||
basketID = -1;
|
||||
symbol = "";
|
||||
ticket = 0;
|
||||
orderType = ORDER_TYPE_BUY;
|
||||
lots = 0;
|
||||
price = 0;
|
||||
success = false;
|
||||
errorCode = 0;
|
||||
errorDescription = "";
|
||||
pl = 0;
|
||||
accountBalance = 0;
|
||||
accountEquity = 0;
|
||||
zScore = 0;
|
||||
correlation = 0;
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Price History Buffer Structure |
|
||||
//| Maintains rolling window for correlation calculations |
|
||||
//+------------------------------------------------------------------+
|
||||
struct PriceHistoryBuffer
|
||||
{
|
||||
double prices[]; // Price data array
|
||||
int size; // Current buffer size
|
||||
int head; // Current write position (newest)
|
||||
datetime lastUpdateTime; // Last update timestamp
|
||||
bool isWarmedUp; // True when fully populated
|
||||
|
||||
// Constructor
|
||||
void PriceHistoryBuffer()
|
||||
{
|
||||
size = 0;
|
||||
head = 0;
|
||||
lastUpdateTime = 0;
|
||||
isWarmedUp = false;
|
||||
}
|
||||
|
||||
// Initialize buffer with specific size
|
||||
bool Initialize(int bufferSize)
|
||||
{
|
||||
if(bufferSize <= 0 || bufferSize > MAX_LOOKBACK_PERIOD)
|
||||
return false;
|
||||
|
||||
if(ArrayResize(prices, bufferSize) != bufferSize)
|
||||
return false;
|
||||
|
||||
ArrayInitialize(prices, 0);
|
||||
size = bufferSize;
|
||||
head = 0;
|
||||
lastUpdateTime = 0;
|
||||
isWarmedUp = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add new price (circular buffer pattern)
|
||||
void AddPrice(double price, datetime time)
|
||||
{
|
||||
if(size <= 0)
|
||||
return;
|
||||
|
||||
head = (head + 1) % size;
|
||||
prices[head] = price;
|
||||
lastUpdateTime = time;
|
||||
|
||||
// Check if warmed up (simple check - all positions written at least once)
|
||||
if(!isWarmedUp && head == size - 1)
|
||||
isWarmedUp = true;
|
||||
}
|
||||
|
||||
// Get price at offset from newest (0 = newest, 1 = second newest, etc.)
|
||||
double GetPrice(int offset) const
|
||||
{
|
||||
if(offset < 0 || offset >= size)
|
||||
return 0;
|
||||
|
||||
int realIndex = (head - offset + size) % size;
|
||||
return prices[realIndex];
|
||||
}
|
||||
|
||||
// Get all prices in chronological order (oldest first)
|
||||
bool GetPricesOrdered(double &output[]) const
|
||||
{
|
||||
if(ArrayResize(output, size) != size)
|
||||
return false;
|
||||
|
||||
for(int i = 0; i < size; i++)
|
||||
{
|
||||
int srcIndex = (head - size + 1 + i + size) % size;
|
||||
output[i] = prices[srcIndex];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| EA Configuration Structure |
|
||||
//| Groups all user-configurable settings |
|
||||
//+------------------------------------------------------------------+
|
||||
struct EAConfig
|
||||
{
|
||||
// Symbol configuration
|
||||
string symbols[NUM_SYMBOLS]; // Full symbol names with suffix
|
||||
ENUM_TIMEFRAMES timeframe; // Timeframe for calculations
|
||||
|
||||
// Correlation engine parameters
|
||||
int lookbackPeriod; // Rolling window size
|
||||
int updateIntervalSeconds; // Cache update frequency
|
||||
|
||||
// Signal generation parameters
|
||||
double zScoreEntryThreshold; // Minimum |z-score| for entry
|
||||
double zScoreExitThreshold; // Maximum |z-score| for exit
|
||||
double minCorrelation; // Minimum acceptable correlation
|
||||
double maxSpreadPips; // Maximum spread per symbol
|
||||
|
||||
// Risk management parameters
|
||||
double baseLotSize; // Base lot size per leg
|
||||
double riskPercentPerBasket; // Risk % per basket
|
||||
double maxDrawdownPercent; // Circuit breaker threshold
|
||||
double maxDailyLossPercent; // Daily loss limit %
|
||||
double maxDailyLossAmount; // Daily loss limit amount
|
||||
int maxOpenBaskets; // Maximum concurrent baskets
|
||||
int maxHoldingHours; // Maximum basket hold time
|
||||
|
||||
// Trading hours
|
||||
int tradingStartHour; // Start hour (broker time)
|
||||
int tradingStartMinute; // Start minute
|
||||
int tradingEndHour; // End hour (broker time)
|
||||
int tradingEndMinute; // End minute
|
||||
bool avoidRollover; // Skip rollover period
|
||||
|
||||
// Technical settings
|
||||
int magicNumber; // EA magic number
|
||||
int slippagePoints; // Maximum slippage
|
||||
int maxRetries; // Trade retry limit
|
||||
ENUM_LOG_LEVEL logLevel; // Logging verbosity
|
||||
bool logToFile; // Enable file logging
|
||||
ENUM_SIZING_MODE sizingMode; // Position sizing mode
|
||||
|
||||
// Constructor
|
||||
void EAConfig()
|
||||
{
|
||||
SetDefaults();
|
||||
}
|
||||
|
||||
// Set default values
|
||||
void SetDefaults()
|
||||
{
|
||||
symbols[0] = DEFAULT_SYMBOL_AUDCAD;
|
||||
symbols[1] = DEFAULT_SYMBOL_NZDCAD;
|
||||
symbols[2] = DEFAULT_SYMBOL_AUDNZD;
|
||||
timeframe = PERIOD_M15;
|
||||
lookbackPeriod = 250;
|
||||
updateIntervalSeconds = DEFAULT_CACHE_UPDATE_INTERVAL;
|
||||
zScoreEntryThreshold = 2.5;
|
||||
zScoreExitThreshold = 0.5;
|
||||
minCorrelation = 0.75;
|
||||
maxSpreadPips = 3.0;
|
||||
baseLotSize = 0.01;
|
||||
riskPercentPerBasket = 1.0;
|
||||
maxDrawdownPercent = DEFAULT_MAX_DRAWDOWN_PERCENT;
|
||||
maxDailyLossPercent = 5.0;
|
||||
maxDailyLossAmount = DEFAULT_DAILY_LOSS_LIMIT;
|
||||
maxOpenBaskets = 1;
|
||||
maxHoldingHours = DEFAULT_MAX_HOLDING_HOURS;
|
||||
tradingStartHour = 0;
|
||||
tradingStartMinute = 0;
|
||||
tradingEndHour = 23;
|
||||
tradingEndMinute = 59;
|
||||
avoidRollover = true;
|
||||
magicNumber = 100000;
|
||||
slippagePoints = DEFAULT_SLIPPAGE_POINTS;
|
||||
maxRetries = MAX_RETRY_ATTEMPTS;
|
||||
logLevel = LOG_LEVEL_INFO;
|
||||
logToFile = false;
|
||||
sizingMode = SIZING_FIXED;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_STRUCTURES_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,398 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_TradeWrapper.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| Trade Execution Abstraction |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_TRADEWRAPPER_MQH
|
||||
#define DBASKET_TRADEWRAPPER_MQH
|
||||
|
||||
#include <Trade\Trade.mqh>
|
||||
#include "DBasket_Defines.mqh"
|
||||
#include "DBasket_Structures.mqh"
|
||||
#include "DBasket_Logger.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Trade Wrapper Class |
|
||||
//| Centralized trade execution with error handling and retry logic |
|
||||
//+------------------------------------------------------------------+
|
||||
class CTradeWrapper
|
||||
{
|
||||
private:
|
||||
CTrade m_trade; // MQL5 trade object
|
||||
int m_magicNumber; // EA magic number
|
||||
int m_slippagePoints; // Maximum slippage
|
||||
int m_maxRetries; // Maximum retry attempts
|
||||
bool m_isInitialized;
|
||||
|
||||
// Statistics
|
||||
int m_totalOrders;
|
||||
int m_successfulOrders;
|
||||
int m_failedOrders;
|
||||
int m_retriedOrders;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if error is retriable |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsRetriableError(uint retcode)
|
||||
{
|
||||
switch(retcode)
|
||||
{
|
||||
case TRADE_RETCODE_REQUOTE:
|
||||
case TRADE_RETCODE_PRICE_OFF:
|
||||
case TRADE_RETCODE_PRICE_CHANGED:
|
||||
case TRADE_RETCODE_TIMEOUT:
|
||||
case TRADE_RETCODE_CONNECTION:
|
||||
case TRADE_RETCODE_SERVER_DISABLES_AT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Wait between retries |
|
||||
//+------------------------------------------------------------------+
|
||||
void WaitForRetry(int attempt)
|
||||
{
|
||||
int waitMs = RETRY_DELAY_MS * (attempt + 1); // Exponential backoff
|
||||
Sleep(waitMs);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check pre-trade conditions |
|
||||
//+------------------------------------------------------------------+
|
||||
bool PreTradeCheck(string symbol, double lots, string &failReason)
|
||||
{
|
||||
// Check symbol tradability
|
||||
ENUM_SYMBOL_TRADE_MODE tradeMode = (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
|
||||
if(tradeMode != SYMBOL_TRADE_MODE_FULL)
|
||||
{
|
||||
failReason = "Symbol " + symbol + " is not fully tradeable. Mode: " + EnumToString(tradeMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check volume constraints
|
||||
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
||||
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
||||
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
|
||||
if(lots < minLot)
|
||||
{
|
||||
failReason = "Lot size " + DoubleToString(lots, 2) + " below minimum " + DoubleToString(minLot, 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(lots > maxLot)
|
||||
{
|
||||
failReason = "Lot size " + DoubleToString(lots, 2) + " exceeds maximum " + DoubleToString(maxLot, 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check margin
|
||||
double marginRequired;
|
||||
double price = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
||||
|
||||
if(!OrderCalcMargin(ORDER_TYPE_BUY, symbol, lots, price, marginRequired))
|
||||
{
|
||||
failReason = "Failed to calculate margin requirement";
|
||||
return false;
|
||||
}
|
||||
|
||||
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
||||
|
||||
if(freeMargin < marginRequired * 1.5) // 50% buffer
|
||||
{
|
||||
failReason = "Insufficient margin. Required: " + DoubleToString(marginRequired, 2) +
|
||||
", Available: " + DoubleToString(freeMargin, 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
//+------------------------------------------------------------------+
|
||||
//| Constructor |
|
||||
//+------------------------------------------------------------------+
|
||||
CTradeWrapper()
|
||||
{
|
||||
m_magicNumber = 100000;
|
||||
m_slippagePoints = DEFAULT_SLIPPAGE_POINTS;
|
||||
m_maxRetries = MAX_RETRY_ATTEMPTS;
|
||||
m_isInitialized = false;
|
||||
m_totalOrders = 0;
|
||||
m_successfulOrders = 0;
|
||||
m_failedOrders = 0;
|
||||
m_retriedOrders = 0;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize trade wrapper |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(int magicNumber, int slippagePoints = DEFAULT_SLIPPAGE_POINTS, int maxRetries = MAX_RETRY_ATTEMPTS)
|
||||
{
|
||||
m_magicNumber = magicNumber;
|
||||
m_slippagePoints = slippagePoints;
|
||||
m_maxRetries = maxRetries;
|
||||
|
||||
// Configure CTrade
|
||||
m_trade.SetExpertMagicNumber(m_magicNumber);
|
||||
m_trade.SetDeviationInPoints(m_slippagePoints);
|
||||
m_trade.SetTypeFilling(ORDER_FILLING_FOK);
|
||||
m_trade.SetAsyncMode(false); // Synchronous mode for reliable basket execution
|
||||
|
||||
m_isInitialized = true;
|
||||
|
||||
Logger.Info("Trade Wrapper initialized - Magic: " + IntegerToString(m_magicNumber) +
|
||||
", Slippage: " + IntegerToString(m_slippagePoints) + " points");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Open a position with retry logic |
|
||||
//+------------------------------------------------------------------+
|
||||
bool OpenPosition(string symbol, ENUM_ORDER_TYPE orderType, double lots,
|
||||
string comment, ulong &ticket, string &errorMsg)
|
||||
{
|
||||
ticket = 0;
|
||||
errorMsg = "";
|
||||
m_totalOrders++;
|
||||
|
||||
// Pre-trade validation
|
||||
if(!PreTradeCheck(symbol, lots, errorMsg))
|
||||
{
|
||||
Logger.Error("Pre-trade check failed: " + errorMsg);
|
||||
m_failedOrders++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current price
|
||||
double price = (orderType == ORDER_TYPE_BUY) ?
|
||||
SymbolInfoDouble(symbol, SYMBOL_ASK) :
|
||||
SymbolInfoDouble(symbol, SYMBOL_BID);
|
||||
|
||||
if(price == 0)
|
||||
{
|
||||
errorMsg = "Invalid price for " + symbol;
|
||||
m_failedOrders++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute with retry logic
|
||||
for(int attempt = 0; attempt < m_maxRetries; attempt++)
|
||||
{
|
||||
// Refresh price on retry
|
||||
if(attempt > 0)
|
||||
{
|
||||
WaitForRetry(attempt);
|
||||
price = (orderType == ORDER_TYPE_BUY) ?
|
||||
SymbolInfoDouble(symbol, SYMBOL_ASK) :
|
||||
SymbolInfoDouble(symbol, SYMBOL_BID);
|
||||
m_retriedOrders++;
|
||||
Logger.Debug("Retrying order - Attempt " + IntegerToString(attempt + 1));
|
||||
}
|
||||
|
||||
// Attempt to open position
|
||||
bool result = m_trade.PositionOpen(symbol, orderType, lots, price, 0, 0, comment);
|
||||
|
||||
uint retcode = m_trade.ResultRetcode();
|
||||
|
||||
if(result && retcode == TRADE_RETCODE_DONE)
|
||||
{
|
||||
ticket = m_trade.ResultOrder();
|
||||
m_successfulOrders++;
|
||||
|
||||
Logger.Info("Position opened - Symbol: " + symbol +
|
||||
", Type: " + (orderType == ORDER_TYPE_BUY ? "BUY" : "SELL") +
|
||||
", Lots: " + DoubleToString(lots, 2) +
|
||||
", Price: " + DoubleToString(m_trade.ResultPrice(), 5) +
|
||||
", Ticket: " + IntegerToString(ticket));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if error is retriable
|
||||
if(!IsRetriableError(retcode))
|
||||
{
|
||||
errorMsg = Logger.ErrorDescription((int)retcode);
|
||||
Logger.TradeError("OpenPosition", symbol, (int)retcode);
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Debug("Retriable error: " + Logger.ErrorDescription((int)retcode));
|
||||
}
|
||||
|
||||
m_failedOrders++;
|
||||
|
||||
if(errorMsg == "")
|
||||
errorMsg = "Max retries exceeded";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Close a position by ticket |
|
||||
//+------------------------------------------------------------------+
|
||||
bool ClosePosition(ulong ticket, string &errorMsg)
|
||||
{
|
||||
errorMsg = "";
|
||||
|
||||
// Select position
|
||||
if(!PositionSelectByTicket(ticket))
|
||||
{
|
||||
errorMsg = "Position not found: " + IntegerToString(ticket);
|
||||
return false;
|
||||
}
|
||||
|
||||
string symbol = PositionGetString(POSITION_SYMBOL);
|
||||
double lots = PositionGetDouble(POSITION_VOLUME);
|
||||
|
||||
// Execute with retry logic
|
||||
for(int attempt = 0; attempt < m_maxRetries; attempt++)
|
||||
{
|
||||
if(attempt > 0)
|
||||
{
|
||||
WaitForRetry(attempt);
|
||||
m_retriedOrders++;
|
||||
}
|
||||
|
||||
bool result = m_trade.PositionClose(ticket);
|
||||
uint retcode = m_trade.ResultRetcode();
|
||||
|
||||
if(result && retcode == TRADE_RETCODE_DONE)
|
||||
{
|
||||
Logger.Info("Position closed - Ticket: " + IntegerToString(ticket) +
|
||||
", Symbol: " + symbol +
|
||||
", Lots: " + DoubleToString(lots, 2));
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!IsRetriableError(retcode))
|
||||
{
|
||||
errorMsg = Logger.ErrorDescription((int)retcode);
|
||||
Logger.TradeError("ClosePosition", symbol, (int)retcode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(errorMsg == "")
|
||||
errorMsg = "Max retries exceeded";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Close all positions by magic number |
|
||||
//+------------------------------------------------------------------+
|
||||
int CloseAllPositions(string &errorMsg)
|
||||
{
|
||||
int closed = 0;
|
||||
int total = PositionsTotal();
|
||||
|
||||
// Close from end to avoid index shifting
|
||||
for(int i = total - 1; i >= 0; i--)
|
||||
{
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
if(ticket == 0)
|
||||
continue;
|
||||
|
||||
if(PositionGetInteger(POSITION_MAGIC) != m_magicNumber)
|
||||
continue;
|
||||
|
||||
string closeError;
|
||||
if(ClosePosition(ticket, closeError))
|
||||
closed++;
|
||||
else
|
||||
Logger.Error("Failed to close position " + IntegerToString(ticket) + ": " + closeError);
|
||||
}
|
||||
|
||||
if(closed < total)
|
||||
errorMsg = "Closed " + IntegerToString(closed) + " of " + IntegerToString(total) + " positions";
|
||||
|
||||
return closed;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get position P&L by ticket |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetPositionProfit(ulong ticket)
|
||||
{
|
||||
if(!PositionSelectByTicket(ticket))
|
||||
return 0;
|
||||
|
||||
return PositionGetDouble(POSITION_PROFIT) +
|
||||
PositionGetDouble(POSITION_SWAP);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if position exists |
|
||||
//+------------------------------------------------------------------+
|
||||
bool PositionExists(ulong ticket)
|
||||
{
|
||||
return PositionSelectByTicket(ticket);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Normalize lot size to broker requirements |
|
||||
//+------------------------------------------------------------------+
|
||||
double NormalizeLots(string symbol, double lots)
|
||||
{
|
||||
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
||||
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
||||
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
|
||||
lots = MathMax(minLot, lots);
|
||||
lots = MathMin(maxLot, lots);
|
||||
lots = MathFloor(lots / lotStep) * lotStep;
|
||||
|
||||
return NormalizeDouble(lots, 2);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get execution statistics |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetStatistics(int &total, int &successful, int &failed, int &retried)
|
||||
{
|
||||
total = m_totalOrders;
|
||||
successful = m_successfulOrders;
|
||||
failed = m_failedOrders;
|
||||
retried = m_retriedOrders;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get magic number |
|
||||
//+------------------------------------------------------------------+
|
||||
int GetMagicNumber() const
|
||||
{
|
||||
return m_magicNumber;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Count positions by magic number |
|
||||
//+------------------------------------------------------------------+
|
||||
int CountPositions()
|
||||
{
|
||||
int count = 0;
|
||||
int total = PositionsTotal();
|
||||
|
||||
for(int i = 0; i < total; i++)
|
||||
{
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
if(ticket == 0)
|
||||
continue;
|
||||
|
||||
if(PositionGetInteger(POSITION_MAGIC) == m_magicNumber)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_TRADEWRAPPER_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,332 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DBasket_VolatilityBalancer.mqh |
|
||||
//| D-Basket Correlation Hedging EA |
|
||||
//| ATR-Based Position Sizing |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "D-Basket EA"
|
||||
#property version "2.00"
|
||||
#property strict
|
||||
|
||||
#ifndef DBASKET_VOLATILITYBALANCER_MQH
|
||||
#define DBASKET_VOLATILITYBALANCER_MQH
|
||||
|
||||
#include "DBasket_Defines.mqh"
|
||||
#include "DBasket_Structures.mqh"
|
||||
#include "DBasket_Logger.mqh"
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Volatility Data Structure |
|
||||
//+------------------------------------------------------------------+
|
||||
struct VolatilityData
|
||||
{
|
||||
double atr[NUM_SYMBOLS]; // ATR values for each symbol
|
||||
double weights[NUM_SYMBOLS]; // Inverse volatility weights
|
||||
double adjustedLots[NUM_SYMBOLS]; // Final lot sizes
|
||||
datetime lastUpdateTime; // Timestamp of last calculation
|
||||
bool isValid; // True if calculation succeeded
|
||||
|
||||
void Reset()
|
||||
{
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
atr[i] = 0;
|
||||
weights[i] = 0.333333; // Default equal weight
|
||||
adjustedLots[i] = 0.01;
|
||||
}
|
||||
lastUpdateTime = 0;
|
||||
isValid = false;
|
||||
}
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Volatility Balancer Class |
|
||||
//| Risk Parity Position Sizing via ATR |
|
||||
//+------------------------------------------------------------------+
|
||||
class CVolatilityBalancer
|
||||
{
|
||||
private:
|
||||
// Configuration
|
||||
string m_symbols[NUM_SYMBOLS];
|
||||
int m_atrPeriod; // ATR lookback period
|
||||
int m_atrHandles[NUM_SYMBOLS]; // ATR indicator handles
|
||||
double m_minWeight; // Minimum weight per symbol
|
||||
double m_maxWeight; // Maximum weight per symbol
|
||||
bool m_enabled; // ATR sizing enabled
|
||||
|
||||
// State
|
||||
VolatilityData m_cache;
|
||||
int m_barsSinceUpdate;
|
||||
bool m_isInitialized;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Normalize lot size to broker requirements |
|
||||
//+------------------------------------------------------------------+
|
||||
double NormalizeLots(string symbol, double lots)
|
||||
{
|
||||
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
||||
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
||||
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
|
||||
if(lotStep == 0) lotStep = 0.01;
|
||||
if(minLot == 0) minLot = 0.01;
|
||||
if(maxLot == 0) maxLot = 100.0;
|
||||
|
||||
// Round to lot step
|
||||
lots = MathFloor(lots / lotStep) * lotStep;
|
||||
|
||||
// Clamp to min/max
|
||||
lots = MathMax(minLot, MathMin(lots, maxLot));
|
||||
|
||||
return NormalizeDouble(lots, 2);
|
||||
}
|
||||
|
||||
public:
|
||||
//+------------------------------------------------------------------+
|
||||
//| Constructor |
|
||||
//+------------------------------------------------------------------+
|
||||
CVolatilityBalancer()
|
||||
{
|
||||
m_atrPeriod = 14;
|
||||
m_minWeight = 0.15;
|
||||
m_maxWeight = 0.50;
|
||||
m_enabled = true;
|
||||
m_barsSinceUpdate = 999;
|
||||
m_isInitialized = false;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
m_symbols[i] = "";
|
||||
m_atrHandles[i] = INVALID_HANDLE;
|
||||
}
|
||||
|
||||
m_cache.Reset();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Destructor - Release indicator handles |
|
||||
//+------------------------------------------------------------------+
|
||||
~CVolatilityBalancer()
|
||||
{
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
if(m_atrHandles[i] != INVALID_HANDLE)
|
||||
{
|
||||
IndicatorRelease(m_atrHandles[i]);
|
||||
m_atrHandles[i] = INVALID_HANDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Initialize volatility balancer |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Initialize(const string &symbols[], int atrPeriod,
|
||||
double minWeight, double maxWeight, bool enabled)
|
||||
{
|
||||
m_atrPeriod = atrPeriod;
|
||||
m_minWeight = minWeight;
|
||||
m_maxWeight = maxWeight;
|
||||
m_enabled = enabled;
|
||||
|
||||
// Copy symbols
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
m_symbols[i] = symbols[i];
|
||||
|
||||
// Create ATR indicator handles
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
m_atrHandles[i] = iATR(m_symbols[i], PERIOD_CURRENT, m_atrPeriod);
|
||||
if(m_atrHandles[i] == INVALID_HANDLE)
|
||||
{
|
||||
Logger.Error("Failed to create ATR handle for " + m_symbols[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_isInitialized = true;
|
||||
m_cache.Reset();
|
||||
m_barsSinceUpdate = 999;
|
||||
|
||||
Logger.Info("Volatility Balancer initialized - ATR Period: " + IntegerToString(m_atrPeriod) +
|
||||
", Enabled: " + (m_enabled ? "Yes" : "No"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Update ATR values and calculate weights |
|
||||
//+------------------------------------------------------------------+
|
||||
bool Update(bool forceUpdate = false)
|
||||
{
|
||||
if(!m_isInitialized)
|
||||
{
|
||||
Logger.Error("Volatility Balancer not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if update needed
|
||||
m_barsSinceUpdate++;
|
||||
if(!forceUpdate && m_barsSinceUpdate < 1 && m_cache.isValid)
|
||||
{
|
||||
return true; // Use cached values
|
||||
}
|
||||
|
||||
m_barsSinceUpdate = 0;
|
||||
|
||||
// Get ATR values for each symbol
|
||||
double totalATR = 0;
|
||||
double totalInvATR = 0;
|
||||
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
double buffer[1];
|
||||
if(CopyBuffer(m_atrHandles[i], 0, 0, 1, buffer) != 1)
|
||||
{
|
||||
Logger.Warning("Failed to get ATR for " + m_symbols[i] + ", using cached value");
|
||||
if(m_cache.atr[i] <= 0)
|
||||
{
|
||||
m_cache.isValid = false;
|
||||
return false;
|
||||
}
|
||||
// Use cached ATR
|
||||
}
|
||||
else
|
||||
{
|
||||
m_cache.atr[i] = buffer[0];
|
||||
}
|
||||
|
||||
if(m_cache.atr[i] <= 0)
|
||||
{
|
||||
Logger.Error("Invalid ATR value for " + m_symbols[i]);
|
||||
m_cache.isValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
totalATR += m_cache.atr[i];
|
||||
totalInvATR += 1.0 / m_cache.atr[i];
|
||||
}
|
||||
|
||||
// Calculate inverse volatility weights
|
||||
// Higher volatility = smaller weight
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
double rawWeight = (1.0 / m_cache.atr[i]) / totalInvATR;
|
||||
|
||||
// Apply min/max constraints
|
||||
rawWeight = MathMax(m_minWeight, MathMin(rawWeight, m_maxWeight));
|
||||
|
||||
m_cache.weights[i] = rawWeight;
|
||||
}
|
||||
|
||||
// Renormalize weights to sum to 1.0
|
||||
double totalWeight = 0;
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
totalWeight += m_cache.weights[i];
|
||||
|
||||
if(totalWeight > 0)
|
||||
{
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
m_cache.weights[i] /= totalWeight;
|
||||
}
|
||||
|
||||
m_cache.lastUpdateTime = TimeCurrent();
|
||||
m_cache.isValid = true;
|
||||
|
||||
Logger.Debug("ATR Weights updated: AUDCAD=" + DoubleToString(m_cache.weights[SYMBOL_AUDCAD], 3) +
|
||||
", NZDCAD=" + DoubleToString(m_cache.weights[SYMBOL_NZDCAD], 3) +
|
||||
", AUDNZD=" + DoubleToString(m_cache.weights[SYMBOL_AUDNZD], 3));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Calculate weighted lot sizes |
|
||||
//| baseLots: total lot budget |
|
||||
//| lots[]: output array with adjusted lot sizes |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CalculateWeightedLots(double baseLots, double &lots[])
|
||||
{
|
||||
if(ArraySize(lots) < NUM_SYMBOLS)
|
||||
ArrayResize(lots, NUM_SYMBOLS);
|
||||
|
||||
if(!m_enabled || !m_cache.isValid)
|
||||
{
|
||||
// Fallback to equal sizing
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
lots[i] = NormalizeLots(m_symbols[i], baseLots);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Apply weights to base lots
|
||||
// Multiply by 3 because weights sum to 1.0 but we want 3 positions
|
||||
for(int i = 0; i < NUM_SYMBOLS; i++)
|
||||
{
|
||||
double rawLots = baseLots * m_cache.weights[i] * 3.0;
|
||||
lots[i] = NormalizeLots(m_symbols[i], rawLots);
|
||||
m_cache.adjustedLots[i] = lots[i];
|
||||
}
|
||||
|
||||
Logger.Debug("Weighted lots: AUDCAD=" + DoubleToString(lots[SYMBOL_AUDCAD], 2) +
|
||||
", NZDCAD=" + DoubleToString(lots[SYMBOL_NZDCAD], 2) +
|
||||
", AUDNZD=" + DoubleToString(lots[SYMBOL_AUDNZD], 2));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get weight for a specific symbol |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetWeight(int symbolIndex)
|
||||
{
|
||||
if(symbolIndex < 0 || symbolIndex >= NUM_SYMBOLS)
|
||||
return 0.333333;
|
||||
return m_cache.weights[symbolIndex];
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get ATR for a specific symbol |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetATR(int symbolIndex)
|
||||
{
|
||||
if(symbolIndex < 0 || symbolIndex >= NUM_SYMBOLS)
|
||||
return 0;
|
||||
return m_cache.atr[symbolIndex];
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Get cached volatility data |
|
||||
//+------------------------------------------------------------------+
|
||||
void GetData(VolatilityData &data)
|
||||
{
|
||||
data = m_cache;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Is enabled |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsEnabled()
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Is cache valid |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsValid()
|
||||
{
|
||||
return m_cache.isValid;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Enable/disable volatility balancing |
|
||||
//+------------------------------------------------------------------+
|
||||
void SetEnabled(bool enabled)
|
||||
{
|
||||
m_enabled = enabled;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DBASKET_VOLATILITYBALANCER_MQH
|
||||
//+------------------------------------------------------------------+
|
||||
Reference in New Issue
Block a user