Initial commit: D-Basket EA v2.0 Pro

This commit is contained in:
dinethlive
2025-12-28 03:37:48 +05:30
commit d096485f7c
20 changed files with 8027 additions and 0 deletions
@@ -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 &currentSpread)
{
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
//+------------------------------------------------------------------+
+149
View File
@@ -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
//+------------------------------------------------------------------+
+359
View File
@@ -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
//+------------------------------------------------------------------+
+524
View File
@@ -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
//+------------------------------------------------------------------+