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
+546
View File
@@ -0,0 +1,546 @@
//+------------------------------------------------------------------+
//| DBasketEA.mq5 |
//| D-Basket Correlation Hedging EA |
//| Three-Pair Correlation Strategy for AUDCAD/NZDCAD/AUDNZD |
//+------------------------------------------------------------------+
#property copyright "D-Basket EA"
#property version "1.00"
#property description "Three-pair correlation hedging EA exploiting temporary divergences between AUDCAD, NZDCAD, and AUDNZD"
#property strict
//+------------------------------------------------------------------+
//| Include Files |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include "..\Include\DBasket\DBasket_Defines.mqh"
#include "..\Include\DBasket\DBasket_Structures.mqh"
#include "..\Include\DBasket\DBasket_Logger.mqh"
#include "..\Include\DBasket\DBasket_CorrelationEngine.mqh"
#include "..\Include\DBasket\DBasket_SignalEngine.mqh"
#include "..\Include\DBasket\DBasket_TradeWrapper.mqh"
#include "..\Include\DBasket\DBasket_PositionManager.mqh"
#include "..\Include\DBasket\DBasket_RiskManager.mqh"
//+------------------------------------------------------------------+
//| Input Parameters |
//+------------------------------------------------------------------+
// --- Symbol Configuration ---
input group "Symbol Settings"
input string InpSymbolSuffix = ""; // Symbol suffix (e.g., ".m", "_sb")
// --- Correlation Engine ---
input group "Correlation Engine"
input int InpLookbackPeriod = 250; // Lookback period (bars)
input int InpCacheUpdateSec = 30; // Cache update interval (seconds)
// --- Signal Generation ---
input group "Signal Generation"
input double InpZScoreEntry = 2.5; // Entry Z-Score threshold
input double InpZScoreExit = 0.5; // Exit Z-Score threshold
input double InpMinCorrelation = 0.75; // Minimum correlation threshold
input double InpMaxSpreadPips = 3.0; // Maximum spread (pips per symbol)
// --- Risk Management ---
input group "Risk Management"
input double InpFixedLotSize = 0.01; // Fixed lot size per leg
input double InpRiskPercent = 1.0; // Risk % per basket (if dynamic sizing)
input bool InpUseFixedLots = true; // Use fixed lot size
input double InpMaxDrawdownPct = 15.0; // Max drawdown % before halt
input double InpDailyLossLimit = 100.0; // Daily loss limit ($)
input double InpDailyLossPct = 5.0; // Daily loss limit (%)
input int InpMaxHoldingHours = 24; // Maximum basket holding hours
input double InpTakeProfitAmount = 10.0; // Take profit per basket ($)
input double InpStopLossAmount = 15.0; // Stop loss per basket ($)
// --- Trading Hours ---
input group "Trading Hours"
input int InpTradingStartHour = 0; // Trading start hour (broker time)
input int InpTradingStartMin = 0; // Trading start minute
input int InpTradingEndHour = 23; // Trading end hour (broker time)
input int InpTradingEndMin = 59; // Trading end minute
input bool InpAvoidRollover = true; // Avoid rollover period
// --- Technical Settings ---
input group "Technical Settings"
input int InpMagicNumber = 100000; // Magic number
input int InpSlippagePoints = 10; // Maximum slippage (points)
input ENUM_LOG_LEVEL InpLogLevel = LOG_LEVEL_INFO; // Log level
input bool InpLogToFile = false; // Log to file
//+------------------------------------------------------------------+
//| Global Variables |
//+------------------------------------------------------------------+
// Configuration
EAConfig g_config;
// Module instances
CCorrelationEngine g_correlationEngine;
CSignalEngine g_signalEngine;
CTradeWrapper g_tradeWrapper;
CPositionManager g_positionManager;
CRiskManager g_riskManager;
// State
bool g_isInitialized = false;
bool g_tradingEnabled = true;
datetime g_lastTickProcessed = 0;
int g_tickCount = 0;
//+------------------------------------------------------------------+
//| Build configuration from inputs |
//+------------------------------------------------------------------+
void BuildConfiguration()
{
g_config.SetDefaults();
// Symbol configuration
g_config.symbols[SYMBOL_AUDCAD] = DEFAULT_SYMBOL_AUDCAD + InpSymbolSuffix;
g_config.symbols[SYMBOL_NZDCAD] = DEFAULT_SYMBOL_NZDCAD + InpSymbolSuffix;
g_config.symbols[SYMBOL_AUDNZD] = DEFAULT_SYMBOL_AUDNZD + InpSymbolSuffix;
g_config.timeframe = Period();
// Correlation engine
g_config.lookbackPeriod = InpLookbackPeriod;
g_config.updateIntervalSeconds = InpCacheUpdateSec;
// Signal generation
g_config.zScoreEntryThreshold = InpZScoreEntry;
g_config.zScoreExitThreshold = InpZScoreExit;
g_config.minCorrelation = InpMinCorrelation;
g_config.maxSpreadPips = InpMaxSpreadPips;
// Risk management
g_config.baseLotSize = InpFixedLotSize;
g_config.riskPercentPerBasket = InpRiskPercent;
g_config.sizingMode = InpUseFixedLots ? SIZING_FIXED : SIZING_RISK_BASED;
g_config.maxDrawdownPercent = InpMaxDrawdownPct;
g_config.maxDailyLossPercent = InpDailyLossPct;
g_config.maxDailyLossAmount = InpDailyLossLimit;
g_config.maxHoldingHours = InpMaxHoldingHours;
// Trading hours
g_config.tradingStartHour = InpTradingStartHour;
g_config.tradingStartMinute = InpTradingStartMin;
g_config.tradingEndHour = InpTradingEndHour;
g_config.tradingEndMinute = InpTradingEndMin;
g_config.avoidRollover = InpAvoidRollover;
// Technical
g_config.magicNumber = InpMagicNumber;
g_config.slippagePoints = InpSlippagePoints;
g_config.logLevel = InpLogLevel;
g_config.logToFile = InpLogToFile;
}
//+------------------------------------------------------------------+
//| Validate input parameters |
//+------------------------------------------------------------------+
bool ValidateInputs()
{
// Lookback period
if(InpLookbackPeriod < MIN_LOOKBACK_PERIOD || InpLookbackPeriod > MAX_LOOKBACK_PERIOD)
{
Logger.Error("Invalid lookback period. Must be " + IntegerToString(MIN_LOOKBACK_PERIOD) +
"-" + IntegerToString(MAX_LOOKBACK_PERIOD));
return false;
}
// Z-score thresholds
if(InpZScoreEntry <= 0 || InpZScoreEntry > 5.0)
{
Logger.Error("Invalid entry Z-score. Must be 0-5.0");
return false;
}
if(InpZScoreExit < 0 || InpZScoreExit >= InpZScoreEntry)
{
Logger.Error("Invalid exit Z-score. Must be 0 to less than entry threshold");
return false;
}
// Correlation
if(InpMinCorrelation < 0.5 || InpMinCorrelation > 0.95)
{
Logger.Error("Invalid minimum correlation. Must be 0.5-0.95");
return false;
}
// Risk parameters
if(InpRiskPercent < 0.1 || InpRiskPercent > 10.0)
{
Logger.Error("Invalid risk percent. Must be 0.1-10.0");
return false;
}
if(InpMaxDrawdownPct < 5.0 || InpMaxDrawdownPct > 50.0)
{
Logger.Error("Invalid max drawdown. Must be 5-50%");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate trading environment |
//+------------------------------------------------------------------+
bool ValidateEnvironment()
{
// Check account type (must be hedging)
ENUM_ACCOUNT_MARGIN_MODE marginMode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
if(marginMode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
{
Logger.Error("FATAL: Hedging account required. Current mode: " + EnumToString(marginMode));
Logger.Error("This EA requires a hedging account to open opposite positions.");
return false;
}
// Check if trading allowed
if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
{
Logger.Error("Trading is not allowed in terminal settings");
return false;
}
if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
{
Logger.Error("Automated trading is not allowed for this EA");
return false;
}
// Check connection
if(!TerminalInfoInteger(TERMINAL_CONNECTED))
{
Logger.Warning("Terminal is not connected to server");
}
return true;
}
//+------------------------------------------------------------------+
//| Validate symbols |
//+------------------------------------------------------------------+
bool ValidateSymbols()
{
for(int i = 0; i < NUM_SYMBOLS; i++)
{
string symbol = g_config.symbols[i];
// Try to select symbol
if(!SymbolSelect(symbol, true))
{
Logger.Error("Symbol not available: " + symbol);
return false;
}
// Check trade mode
ENUM_SYMBOL_TRADE_MODE tradeMode = (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
if(tradeMode != SYMBOL_TRADE_MODE_FULL)
{
Logger.Error("Trading not fully allowed on " + symbol + ": " + EnumToString(tradeMode));
return false;
}
// Log symbol info
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double spread = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * SymbolInfoDouble(symbol, SYMBOL_POINT);
Logger.Info("Symbol " + symbol + " - MinLot: " + DoubleToString(minLot, 2) +
", Step: " + DoubleToString(lotStep, 2) +
", Spread: " + DoubleToString(spread * 10000, 1) + " pips");
}
return true;
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Initialize logger first
Logger.Initialize(InpLogLevel, InpLogToFile);
// Log startup
Logger.LogInitSummary(EA_NAME, EA_VERSION,
AccountInfoDouble(ACCOUNT_BALANCE),
(int)AccountInfoInteger(ACCOUNT_LEVERAGE),
AccountInfoString(ACCOUNT_SERVER));
// Build configuration from inputs
BuildConfiguration();
// Validate inputs
if(!ValidateInputs())
{
return INIT_PARAMETERS_INCORRECT;
}
// Validate environment
if(!ValidateEnvironment())
{
return INIT_FAILED;
}
// Validate symbols
if(!ValidateSymbols())
{
return INIT_FAILED;
}
// Initialize modules
Logger.Info("Initializing EA modules...");
// Trade wrapper
if(!g_tradeWrapper.Initialize(g_config.magicNumber, g_config.slippagePoints))
{
Logger.Error("Failed to initialize Trade Wrapper");
return INIT_FAILED;
}
// Correlation engine
if(!g_correlationEngine.Initialize(g_config.symbols, g_config.lookbackPeriod,
g_config.timeframe, g_config.updateIntervalSeconds))
{
Logger.Error("Failed to initialize Correlation Engine");
return INIT_FAILED;
}
// Signal engine
if(!g_signalEngine.Initialize(g_config))
{
Logger.Error("Failed to initialize Signal Engine");
return INIT_FAILED;
}
// Position manager
if(!g_positionManager.Initialize(g_config, &g_tradeWrapper))
{
Logger.Error("Failed to initialize Position Manager");
return INIT_FAILED;
}
// Set TP/SL
g_positionManager.SetTPSL(InpTakeProfitAmount, InpStopLossAmount);
// Risk manager
if(!g_riskManager.Initialize(g_config))
{
Logger.Error("Failed to initialize Risk Manager");
return INIT_FAILED;
}
// Recover any existing positions from previous session
g_positionManager.RecoverFromOpenPositions();
g_isInitialized = true;
g_tradingEnabled = true;
Logger.Info("EA initialization complete. Ready for trading.");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
string reasonStr;
switch(reason)
{
case REASON_PROGRAM: reasonStr = "Program"; break;
case REASON_REMOVE: reasonStr = "Remove"; break;
case REASON_RECOMPILE: reasonStr = "Recompile"; break;
case REASON_CHARTCHANGE: reasonStr = "Chart changed"; break;
case REASON_CHARTCLOSE: reasonStr = "Chart closed"; break;
case REASON_PARAMETERS: reasonStr = "Parameters changed"; break;
case REASON_ACCOUNT: reasonStr = "Account changed"; break;
case REASON_TEMPLATE: reasonStr = "Template applied"; break;
case REASON_INITFAILED: reasonStr = "Init failed"; break;
case REASON_CLOSE: reasonStr = "Terminal closed"; break;
default: reasonStr = "Unknown (" + IntegerToString(reason) + ")";
}
Logger.Info("EA shutdown - Reason: " + reasonStr);
// Log final statistics
PerformanceMetrics metrics;
g_riskManager.GetMetrics(metrics);
Logger.Info("Final Statistics:");
Logger.Info(" Total Baskets: " + IntegerToString(metrics.totalBaskets));
Logger.Info(" Closed: " + IntegerToString(metrics.closedBaskets) +
" (Win: " + IntegerToString(metrics.winningBaskets) +
", Loss: " + IntegerToString(metrics.losingBaskets) + ")");
Logger.Info(" Win Rate: " + DoubleToString(metrics.winRate * 100, 1) + "%");
Logger.Info(" Realized P/L: $" + DoubleToString(metrics.realizedPL, 2));
Logger.Info(" Max Drawdown: " + DoubleToString(metrics.maxDrawdownPercent, 2) + "%");
// Clear chart
Comment("");
// Deinitialize logger
Logger.Deinitialize();
g_isInitialized = false;
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(!g_isInitialized)
return;
g_tickCount++;
// === Phase 1: Risk Management Check ===
string riskReason;
if(!g_riskManager.CheckRiskLimits(riskReason))
{
// Check for emergency exit
string emergencyReason;
if(g_riskManager.CheckEmergencyExit(emergencyReason))
{
Logger.Error("EMERGENCY EXIT: " + emergencyReason);
if(g_positionManager.HasOpenBasket())
{
g_positionManager.CloseBasket(EXIT_EMERGENCY);
}
}
g_tradingEnabled = false;
// Update display
if(g_tickCount % 100 == 0)
g_riskManager.DisplayMetricsOnChart();
return;
}
g_tradingEnabled = true;
// === Phase 2: Update Price Buffers ===
g_correlationEngine.UpdatePriceBuffers();
// === Phase 3: Update Correlation Cache ===
if(!g_correlationEngine.UpdateCorrelationCache())
{
// Not ready or data invalid - skip this tick
return;
}
// Get correlation data
CorrelationData corrData;
g_correlationEngine.GetCorrelationData(corrData);
// === Phase 4: Position Management ===
if(g_positionManager.HasOpenBasket())
{
// Update basket state
g_positionManager.UpdateBasketState();
// Check exit signals
BasketState basket;
g_positionManager.GetBasketState(basket);
ENUM_EXIT_REASON exitReason;
if(g_signalEngine.CheckExitSignal(corrData, basket,
g_positionManager.GetTakeProfitAmount(),
g_positionManager.GetStopLossAmount(),
g_positionManager.GetMaxHoldingHours(),
exitReason))
{
// Close basket
double pl = g_positionManager.GetBasketPL();
g_positionManager.CloseBasket(exitReason);
// Record result
g_riskManager.RecordBasketClose(pl, pl >= 0);
}
}
else
{
// === Phase 5: Signal Generation ===
string signalFailReason;
ENUM_BASKET_SIGNAL signal = g_signalEngine.CheckEntrySignal(corrData, false, signalFailReason);
if(signal != SIGNAL_NONE)
{
g_riskManager.RecordSignal(true);
// Attempt to open basket
if(g_positionManager.OpenBasket(signal, corrData.spreadZScore, corrData.corrAUDCAD_NZDCAD))
{
g_riskManager.RecordBasketOpen();
}
}
else if(signalFailReason != "")
{
// Signal was blocked by filter
g_riskManager.RecordSignal(false);
// Log filter reason periodically (every 1000 ticks)
if(g_tickCount % 1000 == 0)
{
Logger.Debug("Signal blocked: " + signalFailReason);
}
}
}
// === Phase 6: Display Update ===
if(g_tickCount % 50 == 0)
{
g_riskManager.DisplayMetricsOnChart();
}
}
//+------------------------------------------------------------------+
//| Tester function for custom optimization criterion |
//+------------------------------------------------------------------+
double OnTester()
{
// Get statistics
double profit = TesterStatistics(STAT_PROFIT);
double maxDD = TesterStatistics(STAT_EQUITY_DD);
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
int totalTrades = (int)TesterStatistics(STAT_TRADES);
int winTrades = (int)TesterStatistics(STAT_PROFIT_TRADES);
// Calculate win rate
double winRate = totalTrades > 0 ? (double)winTrades / totalTrades : 0;
// Custom optimization criterion
// Prioritize: profit/drawdown ratio, win rate > 65%, profit factor > 1.3
if(winRate < 0.65 || profitFactor < 1.3 || totalTrades < 20)
return 0; // Reject parameters that don't meet minimum criteria
// Risk-adjusted return
double riskAdjustedReturn = maxDD > 0 ? profit / maxDD : 0;
// Combine metrics
double score = riskAdjustedReturn * profitFactor * winRate;
return score;
}
//+------------------------------------------------------------------+
//| Trade event handler |
//+------------------------------------------------------------------+
void OnTrade()
{
// Handle trade events if needed
// (Position state is already updated in OnTick via UpdateBasketState)
}
//+------------------------------------------------------------------+
//| Timer function (if using timer) |
//+------------------------------------------------------------------+
void OnTimer()
{
// Can be used for periodic tasks independent of ticks
}
//+------------------------------------------------------------------+
+735
View File
@@ -0,0 +1,735 @@
//+------------------------------------------------------------------+
//| DBasketEA_v2.mq5 |
//| D-Basket Correlation Hedging EA |
//| Version 2.0 - Advanced Optimized |
//+------------------------------------------------------------------+
#property copyright "D-Basket EA"
#property version "2.00"
#property description "Three-pair correlation hedging EA v2.0 with Cointegration, Half-Life, and ATR Balancing"
#property strict
//+------------------------------------------------------------------+
//| Include Files |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include "..\Include\DBasket\DBasket_Defines.mqh"
#include "..\Include\DBasket\DBasket_Structures.mqh"
#include "..\Include\DBasket\DBasket_Logger.mqh"
#include "..\Include\DBasket\DBasket_CorrelationEngine.mqh"
#include "..\Include\DBasket\DBasket_SignalEngine.mqh"
#include "..\Include\DBasket\DBasket_TradeWrapper.mqh"
#include "..\Include\DBasket\DBasket_PositionManager.mqh"
#include "..\Include\DBasket\DBasket_RiskManager.mqh"
// v2.0 Optimization Modules
#include "..\Include\DBasket\DBasket_CointegrationEngine.mqh"
#include "..\Include\DBasket\DBasket_HalfLifeEngine.mqh"
#include "..\Include\DBasket\DBasket_VolatilityBalancer.mqh"
//+------------------------------------------------------------------+
//| Input Parameters |
//+------------------------------------------------------------------+
// --- Symbol Configuration ---
input group "Symbol Settings"
input string InpSymbolSuffix = ""; // Symbol suffix (e.g., ".m", "_sb")
// --- Correlation Engine ---
input group "Correlation Engine"
input int InpLookbackPeriod = 250; // Lookback period (bars)
input int InpCacheUpdateSec = 30; // Cache update interval (seconds)
// --- Signal Generation ---
input group "Signal Generation"
input double InpZScoreEntry = 2.5; // Entry Z-Score threshold
input double InpZScoreExit = 0.5; // Exit Z-Score threshold
input double InpMinCorrelation = 0.75; // Minimum correlation threshold
input double InpMaxSpreadPips = 3.0; // Maximum spread (pips per symbol)
// --- Risk Management ---
input group "Risk Management"
input double InpFixedLotSize = 0.01; // Fixed lot size per leg
input double InpRiskPercent = 1.0; // Risk % per basket (if dynamic sizing)
input bool InpUseFixedLots = true; // Use fixed lot size
input double InpMaxDrawdownPct = 15.0; // Max drawdown % before halt
input double InpDailyLossLimit = 100.0; // Daily loss limit ($)
input double InpDailyLossPct = 5.0; // Daily loss limit (%)
input int InpMaxHoldingHours = 24; // Maximum basket holding hours
input double InpTakeProfitAmount = 10.0; // Take profit per basket ($)
input double InpStopLossAmount = 15.0; // Stop loss per basket ($)
// === v2.0 OPTIMIZATION SETTINGS ===
// --- Cointegration Settings ---
input group "=== Cointegration Filter (v2.0) ==="
input bool InpCointEnabled = true; // Enable Cointegration Filter?
input double InpCointPValue = 0.05; // P-Value Threshold (0.01-0.10)
input int InpCointUpdateBars = 50; // Update Interval (bars)
input int InpCointADFLags = 1; // ADF Regression Lags
// --- Half-Life Settings ---
input group "=== Half-Life Exits (v2.0) ==="
input bool InpHLEnabled = true; // Enable Half-Life Exits?
input int InpHLUpdateBars = 20; // Update Interval (bars)
input int InpHLMinValue = 10; // Minimum Half-Life (bars)
input int InpHLMaxValue = 500; // Maximum Half-Life (bars)
input double InpHLExitMultiplier = 2.0; // Max Hold = Multiplier × HalfLife
input double InpHLStopLossSigma = 1.5; // Stop-Loss Distance (sigma)
// --- ATR Position Sizing ---
input group "=== ATR Position Sizing (v2.0) ==="
input bool InpATREnabled = true; // Enable ATR Sizing?
input int InpATRPeriod = 14; // ATR Period
input double InpATRMinWeight = 0.15; // Minimum Weight per Symbol
input double InpATRMaxWeight = 0.50; // Maximum Weight per Symbol
// --- Trading Hours ---
input group "Trading Hours"
input int InpTradingStartHour = 0; // Trading start hour (broker time)
input int InpTradingStartMin = 0; // Trading start minute
input int InpTradingEndHour = 23; // Trading end hour (broker time)
input int InpTradingEndMin = 59; // Trading end minute
input bool InpAvoidRollover = true; // Avoid rollover period
// --- Technical Settings ---
input group "Technical Settings"
input int InpMagicNumber = 200000; // Magic number (v2.0)
input int InpSlippagePoints = 10; // Maximum slippage (points)
input ENUM_LOG_LEVEL InpLogLevel = LOG_LEVEL_INFO; // Log level
input bool InpLogToFile = false; // Log to file
//+------------------------------------------------------------------+
//| Global Variables |
//+------------------------------------------------------------------+
// Configuration
EAConfig g_config;
// Core modules
CCorrelationEngine g_correlationEngine;
CSignalEngine g_signalEngine;
CTradeWrapper g_tradeWrapper;
CPositionManager g_positionManager;
CRiskManager g_riskManager;
// v2.0 Optimization modules
CCointegrationEngine g_cointegrationEngine;
CHalfLifeEngine g_halfLifeEngine;
CVolatilityBalancer g_volatilityBalancer;
// State
bool g_isInitialized = false;
bool g_tradingEnabled = true;
datetime g_lastTickProcessed = 0;
int g_tickCount = 0;
int g_barCount = 0;
datetime g_lastBarTime = 0;
// v2.0 basket tracking
double g_entryZScore = 0;
int g_barsOpenCount = 0;
//+------------------------------------------------------------------+
//| Build configuration from inputs |
//+------------------------------------------------------------------+
void BuildConfiguration()
{
g_config.SetDefaults();
// Symbol configuration
g_config.symbols[SYMBOL_AUDCAD] = DEFAULT_SYMBOL_AUDCAD + InpSymbolSuffix;
g_config.symbols[SYMBOL_NZDCAD] = DEFAULT_SYMBOL_NZDCAD + InpSymbolSuffix;
g_config.symbols[SYMBOL_AUDNZD] = DEFAULT_SYMBOL_AUDNZD + InpSymbolSuffix;
g_config.timeframe = Period();
// Correlation engine
g_config.lookbackPeriod = InpLookbackPeriod;
g_config.updateIntervalSeconds = InpCacheUpdateSec;
// Signal generation
g_config.zScoreEntryThreshold = InpZScoreEntry;
g_config.zScoreExitThreshold = InpZScoreExit;
g_config.minCorrelation = InpMinCorrelation;
g_config.maxSpreadPips = InpMaxSpreadPips;
// Risk management
g_config.baseLotSize = InpFixedLotSize;
g_config.riskPercentPerBasket = InpRiskPercent;
g_config.sizingMode = InpUseFixedLots ? SIZING_FIXED : SIZING_RISK_BASED;
g_config.maxDrawdownPercent = InpMaxDrawdownPct;
g_config.maxDailyLossPercent = InpDailyLossPct;
g_config.maxDailyLossAmount = InpDailyLossLimit;
g_config.maxHoldingHours = InpMaxHoldingHours;
// Trading hours
g_config.tradingStartHour = InpTradingStartHour;
g_config.tradingStartMinute = InpTradingStartMin;
g_config.tradingEndHour = InpTradingEndHour;
g_config.tradingEndMinute = InpTradingEndMin;
g_config.avoidRollover = InpAvoidRollover;
// Technical
g_config.magicNumber = InpMagicNumber;
g_config.slippagePoints = InpSlippagePoints;
g_config.logLevel = InpLogLevel;
g_config.logToFile = InpLogToFile;
}
//+------------------------------------------------------------------+
//| Validate input parameters |
//+------------------------------------------------------------------+
bool ValidateInputs()
{
// Lookback period
if(InpLookbackPeriod < MIN_LOOKBACK_PERIOD || InpLookbackPeriod > MAX_LOOKBACK_PERIOD)
{
Logger.Error("Invalid lookback period. Must be " + IntegerToString(MIN_LOOKBACK_PERIOD) +
"-" + IntegerToString(MAX_LOOKBACK_PERIOD));
return false;
}
// Z-score thresholds
if(InpZScoreEntry <= 0 || InpZScoreEntry > 5.0)
{
Logger.Error("Invalid entry Z-score. Must be 0-5.0");
return false;
}
if(InpZScoreExit < 0 || InpZScoreExit >= InpZScoreEntry)
{
Logger.Error("Invalid exit Z-score. Must be 0 to less than entry threshold");
return false;
}
// Correlation
if(InpMinCorrelation < 0.5 || InpMinCorrelation > 0.95)
{
Logger.Error("Invalid minimum correlation. Must be 0.5-0.95");
return false;
}
// v2.0 Cointegration validation
if(InpCointPValue < 0.01 || InpCointPValue > 0.20)
{
Logger.Error("Invalid cointegration p-value. Must be 0.01-0.20");
return false;
}
// v2.0 Half-life validation
if(InpHLMinValue < 1 || InpHLMinValue > InpHLMaxValue)
{
Logger.Error("Invalid half-life range");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate trading environment |
//+------------------------------------------------------------------+
bool ValidateEnvironment()
{
// Check account type (must be hedging)
ENUM_ACCOUNT_MARGIN_MODE marginMode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
if(marginMode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
{
Logger.Error("FATAL: Hedging account required. Current mode: " + EnumToString(marginMode));
return false;
}
// Check if trading allowed
if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
{
Logger.Error("Trading is not allowed in terminal settings");
return false;
}
if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
{
Logger.Error("Automated trading is not allowed for this EA");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate symbols |
//+------------------------------------------------------------------+
bool ValidateSymbols()
{
for(int i = 0; i < NUM_SYMBOLS; i++)
{
string symbol = g_config.symbols[i];
if(!SymbolSelect(symbol, true))
{
Logger.Error("Symbol not available: " + symbol);
return false;
}
ENUM_SYMBOL_TRADE_MODE tradeMode = (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
if(tradeMode != SYMBOL_TRADE_MODE_FULL)
{
Logger.Error("Trading not fully allowed on " + symbol);
return false;
}
Logger.Info("Symbol validated: " + symbol);
}
return true;
}
//+------------------------------------------------------------------+
//| Check if new bar formed |
//+------------------------------------------------------------------+
bool IsNewBar()
{
datetime currentBarTime = iTime(g_config.symbols[0], Period(), 0);
if(currentBarTime != g_lastBarTime)
{
g_lastBarTime = currentBarTime;
g_barCount++;
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Calculate spread series for statistical tests |
//+------------------------------------------------------------------+
bool CalculateSpreadSeries(double &spreadSeries[], double &syntheticRatio[], double &audnzd[], int count)
{
// Get price data
double audcadClose[], nzdcadClose[], audnzdClose[];
if(CopyClose(g_config.symbols[SYMBOL_AUDCAD], Period(), 0, count, audcadClose) != count)
return false;
if(CopyClose(g_config.symbols[SYMBOL_NZDCAD], Period(), 0, count, nzdcadClose) != count)
return false;
if(CopyClose(g_config.symbols[SYMBOL_AUDNZD], Period(), 0, count, audnzdClose) != count)
return false;
// Resize output arrays
ArrayResize(spreadSeries, count);
ArrayResize(syntheticRatio, count);
ArrayResize(audnzd, count);
// Calculate spread and synthetic ratio
for(int i = 0; i < count; i++)
{
syntheticRatio[i] = (nzdcadClose[i] > 0) ? audcadClose[i] / nzdcadClose[i] : 0;
audnzd[i] = audnzdClose[i];
spreadSeries[i] = audnzdClose[i] - syntheticRatio[i];
}
return true;
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Initialize logger first
Logger.Initialize(InpLogLevel, InpLogToFile);
Logger.Info("=== D-Basket EA v2.0 OPTIMIZED ===");
Logger.Info("Features: Cointegration + Half-Life + ATR Balancing");
// Build configuration from inputs
BuildConfiguration();
// Validate inputs
if(!ValidateInputs())
return INIT_PARAMETERS_INCORRECT;
// Validate environment
if(!ValidateEnvironment())
return INIT_FAILED;
// Validate symbols
if(!ValidateSymbols())
return INIT_FAILED;
// Initialize core modules
Logger.Info("Initializing core modules...");
if(!g_tradeWrapper.Initialize(g_config.magicNumber, g_config.slippagePoints))
{
Logger.Error("Failed to initialize Trade Wrapper");
return INIT_FAILED;
}
if(!g_correlationEngine.Initialize(g_config.symbols, g_config.lookbackPeriod,
g_config.timeframe, g_config.updateIntervalSeconds))
{
Logger.Error("Failed to initialize Correlation Engine");
return INIT_FAILED;
}
if(!g_signalEngine.Initialize(g_config))
{
Logger.Error("Failed to initialize Signal Engine");
return INIT_FAILED;
}
if(!g_positionManager.Initialize(g_config, &g_tradeWrapper))
{
Logger.Error("Failed to initialize Position Manager");
return INIT_FAILED;
}
g_positionManager.SetTPSL(InpTakeProfitAmount, InpStopLossAmount);
if(!g_riskManager.Initialize(g_config))
{
Logger.Error("Failed to initialize Risk Manager");
return INIT_FAILED;
}
// === Initialize v2.0 Optimization Modules ===
Logger.Info("Initializing v2.0 optimization modules...");
// Cointegration Engine
if(InpCointEnabled)
{
if(!g_cointegrationEngine.Initialize(InpLookbackPeriod, InpCointPValue,
InpCointUpdateBars, InpCointADFLags))
{
Logger.Error("Failed to initialize Cointegration Engine");
return INIT_FAILED;
}
Logger.Info("Cointegration Filter ENABLED (p < " + DoubleToString(InpCointPValue, 2) + ")");
}
else
{
Logger.Info("Cointegration Filter DISABLED");
}
// Half-Life Engine
if(InpHLEnabled)
{
if(!g_halfLifeEngine.Initialize(InpLookbackPeriod, InpHLUpdateBars,
InpHLMinValue, InpHLMaxValue,
InpHLExitMultiplier, InpHLStopLossSigma))
{
Logger.Error("Failed to initialize Half-Life Engine");
return INIT_FAILED;
}
Logger.Info("Half-Life Exits ENABLED (max hold = " + DoubleToString(InpHLExitMultiplier, 1) + " × halflife)");
}
else
{
Logger.Info("Half-Life Exits DISABLED");
}
// Volatility Balancer
if(InpATREnabled)
{
if(!g_volatilityBalancer.Initialize(g_config.symbols, InpATRPeriod,
InpATRMinWeight, InpATRMaxWeight, true))
{
Logger.Error("Failed to initialize Volatility Balancer");
return INIT_FAILED;
}
Logger.Info("ATR Position Sizing ENABLED (period = " + IntegerToString(InpATRPeriod) + ")");
}
else
{
Logger.Info("ATR Position Sizing DISABLED");
}
// Recover existing positions
g_positionManager.RecoverFromOpenPositions();
g_isInitialized = true;
g_tradingEnabled = true;
g_lastBarTime = iTime(g_config.symbols[0], Period(), 0);
Logger.Info("EA v2.0 initialization complete. Ready for optimized trading.");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Logger.Info("EA v2.0 shutdown - Reason: " + IntegerToString(reason));
// Log final statistics
PerformanceMetrics metrics;
g_riskManager.GetMetrics(metrics);
Logger.Info("Final Statistics:");
Logger.Info(" Total Baskets: " + IntegerToString(metrics.totalBaskets));
Logger.Info(" Closed: " + IntegerToString(metrics.closedBaskets) +
" (Win: " + IntegerToString(metrics.winningBaskets) +
", Loss: " + IntegerToString(metrics.losingBaskets) + ")");
Logger.Info(" Win Rate: " + DoubleToString(metrics.winRate * 100, 1) + "%");
Logger.Info(" Realized P/L: $" + DoubleToString(metrics.realizedPL, 2));
Logger.Info(" Max Drawdown: " + DoubleToString(metrics.maxDrawdownPercent, 2) + "%");
Comment("");
Logger.Deinitialize();
g_isInitialized = false;
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(!g_isInitialized)
return;
g_tickCount++;
bool isNewBar = IsNewBar();
// === Phase 1: Risk Management Check ===
string riskReason;
if(!g_riskManager.CheckRiskLimits(riskReason))
{
string emergencyReason;
if(g_riskManager.CheckEmergencyExit(emergencyReason))
{
Logger.Error("EMERGENCY EXIT: " + emergencyReason);
if(g_positionManager.HasOpenBasket())
g_positionManager.CloseBasket(EXIT_EMERGENCY);
}
g_tradingEnabled = false;
if(g_tickCount % 100 == 0)
g_riskManager.DisplayMetricsOnChart();
return;
}
g_tradingEnabled = true;
// === Phase 2: Update Price Buffers ===
g_correlationEngine.UpdatePriceBuffers();
// === Phase 3: Update Correlation Cache ===
if(!g_correlationEngine.UpdateCorrelationCache())
return;
CorrelationData corrData;
g_correlationEngine.GetCorrelationData(corrData);
// === Phase 4: Update v2.0 Optimization Modules (on new bar) ===
if(isNewBar)
{
// Update basket bar counter if open
if(g_positionManager.HasOpenBasket())
g_barsOpenCount++;
// Calculate spread series for statistical tests
double spreadSeries[], syntheticRatio[], audnzd[];
int dataCount = InpLookbackPeriod;
if(CalculateSpreadSeries(spreadSeries, syntheticRatio, audnzd, dataCount))
{
// Update Cointegration
if(InpCointEnabled)
{
g_cointegrationEngine.Update(syntheticRatio, audnzd, dataCount);
}
// Update Half-Life
if(InpHLEnabled)
{
g_halfLifeEngine.Update(spreadSeries, dataCount);
}
}
// Update ATR weights
if(InpATREnabled)
{
g_volatilityBalancer.Update();
}
}
// === Phase 5: Position Management ===
if(g_positionManager.HasOpenBasket())
{
g_positionManager.UpdateBasketState();
BasketState basket;
g_positionManager.GetBasketState(basket);
ENUM_EXIT_REASON exitReason = EXIT_NONE;
// Check standard exit signals first
if(g_signalEngine.CheckExitSignal(corrData, basket,
g_positionManager.GetTakeProfitAmount(),
g_positionManager.GetStopLossAmount(),
g_positionManager.GetMaxHoldingHours(),
exitReason))
{
// Standard exit triggered
}
// v2.0: Check half-life based exits
else if(InpHLEnabled && g_halfLifeEngine.IsValid())
{
// Check time-based exit
if(g_halfLifeEngine.IsTimeExitTriggered(g_barsOpenCount))
{
exitReason = EXIT_MAX_TIME;
Logger.Info("Half-Life time exit triggered (bars: " + IntegerToString(g_barsOpenCount) +
", max: " + IntegerToString(g_halfLifeEngine.GetMaxHoldingBars()) + ")");
}
// Check variance-based stop loss
else if(g_halfLifeEngine.IsStopLossTriggered(g_entryZScore, corrData.spreadZScore))
{
exitReason = EXIT_STOP_LOSS;
Logger.Info("Half-Life variance stop triggered (entry z: " + DoubleToString(g_entryZScore, 2) +
", current z: " + DoubleToString(corrData.spreadZScore, 2) + ")");
}
}
// v2.0: Check cointegration breakdown
else if(InpCointEnabled && g_cointegrationEngine.IsValid())
{
if(!g_cointegrationEngine.IsCointegrated())
{
// Cointegration broke down - consider exiting
if(g_cointegrationEngine.GetPValue() > 0.10)
{
exitReason = EXIT_CORRELATION_BREAK;
Logger.Warning("Cointegration breakdown - p-value: " +
DoubleToString(g_cointegrationEngine.GetPValue(), 2));
}
}
}
// Execute exit if triggered
if(exitReason != EXIT_NONE)
{
double pl = g_positionManager.GetBasketPL();
g_positionManager.CloseBasket(exitReason);
g_riskManager.RecordBasketClose(pl, pl >= 0);
g_barsOpenCount = 0;
g_entryZScore = 0;
}
}
else
{
// === Phase 6: Signal Generation with v2.0 Filters ===
string signalFailReason;
ENUM_BASKET_SIGNAL signal = SIGNAL_NONE;
// v2.0 Pre-filter: Check cointegration before expensive signal calculation
bool cointValid = true;
if(InpCointEnabled)
{
if(!g_cointegrationEngine.IsCointegrated())
{
cointValid = false;
signalFailReason = "Not cointegrated (p=" +
DoubleToString(g_cointegrationEngine.GetPValue(), 2) + ")";
}
}
// v2.0 Pre-filter: Check half-life validity
bool hlValid = true;
if(InpHLEnabled)
{
if(!g_halfLifeEngine.IsHalfLifeValid())
{
hlValid = false;
if(signalFailReason == "")
signalFailReason = "Invalid half-life (" +
DoubleToString(g_halfLifeEngine.GetHalfLife(), 1) + " bars)";
}
}
// Only check entry signal if v2.0 pre-filters pass
if(cointValid && hlValid)
{
signal = g_signalEngine.CheckEntrySignal(corrData, false, signalFailReason);
}
if(signal != SIGNAL_NONE)
{
g_riskManager.RecordSignal(true);
// v2.0: Calculate ATR-weighted lot sizes
double lots[NUM_SYMBOLS];
if(InpATREnabled && g_volatilityBalancer.IsValid())
{
g_volatilityBalancer.CalculateWeightedLots(InpFixedLotSize, lots);
}
else
{
for(int i = 0; i < NUM_SYMBOLS; i++)
lots[i] = InpFixedLotSize;
}
// Attempt to open basket (using standard method for now)
if(g_positionManager.OpenBasket(signal, corrData.spreadZScore, corrData.corrAUDCAD_NZDCAD))
{
g_riskManager.RecordBasketOpen();
g_barsOpenCount = 0;
g_entryZScore = corrData.spreadZScore;
// Log v2.0 entry stats
if(InpCointEnabled)
Logger.Info("Entry cointegration p-value: " + DoubleToString(g_cointegrationEngine.GetPValue(), 3));
if(InpHLEnabled)
Logger.Info("Entry half-life: " + DoubleToString(g_halfLifeEngine.GetHalfLife(), 1) +
" bars (max hold: " + IntegerToString(g_halfLifeEngine.GetMaxHoldingBars()) + ")");
}
}
else if(signalFailReason != "" && g_tickCount % 1000 == 0)
{
g_riskManager.RecordSignal(false);
Logger.Debug("Signal blocked: " + signalFailReason);
}
}
// === Phase 7: Display Update ===
if(g_tickCount % 50 == 0)
{
g_riskManager.DisplayMetricsOnChart();
}
}
//+------------------------------------------------------------------+
//| Tester function for custom optimization criterion |
//+------------------------------------------------------------------+
double OnTester()
{
double profit = TesterStatistics(STAT_PROFIT);
double maxDD = TesterStatistics(STAT_EQUITY_DD);
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
int totalTrades = (int)TesterStatistics(STAT_TRADES);
int winTrades = (int)TesterStatistics(STAT_PROFIT_TRADES);
double winRate = totalTrades > 0 ? (double)winTrades / totalTrades : 0;
// v2.0: Stricter criteria
if(winRate < 0.70 || profitFactor < 1.5 || totalTrades < 30)
return 0;
double riskAdjustedReturn = maxDD > 0 ? profit / maxDD : 0;
double score = riskAdjustedReturn * profitFactor * winRate;
return score;
}
//+------------------------------------------------------------------+
//| Trade event handler |
//+------------------------------------------------------------------+
void OnTrade()
{
// Handle trade events if needed
}
//+------------------------------------------------------------------+
//| Timer function |
//+------------------------------------------------------------------+
void OnTimer()
{
// Can be used for periodic tasks independent of ticks
}
//+------------------------------------------------------------------+
@@ -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
//+------------------------------------------------------------------+
+221
View File
@@ -0,0 +1,221 @@
# D-Basket EA - Correlation Hedging Expert Advisor
A production-level MetaTrader 5 Expert Advisor implementing a three-pair correlation hedging strategy for AUDCAD, NZDCAD, and AUDNZD.
## Overview
This EA exploits the mathematical relationship between three currency pairs:
```
AUDNZD ≈ AUDCAD / NZDCAD
```
When the synthetic ratio (AUDCAD/NZDCAD) diverges from the actual AUDNZD price, the EA enters a hedged three-leg basket expecting mean reversion.
## Strategy Summary
- **Long Basket** (when AUDNZD appears underpriced):
- Long AUDNZD
- Short AUDCAD
- Long NZDCAD
- **Short Basket** (when AUDNZD appears overpriced):
- Short AUDNZD
- Long AUDCAD
- Short NZDCAD
## Features
- **Multi-Symbol Management**: Single EA instance manages all three symbols internally
- **Z-Score Based Signals**: Statistical divergence detection using rolling correlation and z-score
- **Multi-Stage Signal Filtering**: 8 validation stages including spread, correlation, volatility, and time filters
- **Coordinated Basket Execution**: All 3 legs executed atomically with rollback on failure
- **Circuit Breaker System**: Automatic trading halt on drawdown, daily loss, or margin warnings
- **State Recovery**: Recovers basket state after EA restart
- **Performance Tracking**: Real-time metrics with chart display
## Requirements
> âš ï¸ **CRITICAL: Hedging Account Required**
>
> This EA requires a broker account with hedging enabled. Netting accounts will NOT work.
- MetaTrader 5 platform
- Hedging account type
- All three symbols available: AUDCAD, NZDCAD, AUDNZD
- Recommended: VPS for 24/5 operation
## Installation
1. Copy `MQL5/Experts/DBasketEA.mq5` to your `MQL5/Experts/` folder
2. Copy the entire `MQL5/Include/DBasket/` folder to your `MQL5/Include/` folder
3. Open MetaEditor and compile `DBasketEA.mq5`
4. Attach EA to any chart (AUDCAD recommended)
## File Structure
```
MQL5/
├── Experts/
│ └── DBasketEA.mq5 # Main EA file
└── Include/
└── DBasket/
├── DBasket_Defines.mqh # Constants & enumerations
├── DBasket_Structures.mqh # Data structures
├── DBasket_Logger.mqh # Logging utility
├── DBasket_CorrelationEngine.mqh # Correlation & z-score calculation
├── DBasket_SignalEngine.mqh # Signal generation & filtering
├── DBasket_TradeWrapper.mqh # Trade execution wrapper
├── DBasket_PositionManager.mqh # Basket position management
└── DBasket_RiskManager.mqh # Risk management & circuit breaker
```
## Input Parameters
### Symbol Settings
| Parameter | Default | Description |
|-----------|---------|-------------|
| Symbol Suffix | "" | Broker symbol suffix (e.g., ".m", "_sb") |
### Correlation Engine
| Parameter | Default | Range | Description |
|-----------|---------|-------|-------------|
| Lookback Period | 250 | 50-1000 | Bars for correlation/z-score calculation |
| Cache Update | 30 | 10-300 | Seconds between recalculations |
### Signal Generation
| Parameter | Default | Range | Description |
|-----------|---------|-------|-------------|
| Entry Z-Score | 2.5 | 1.5-5.0 | Minimum z-score for entry |
| Exit Z-Score | 0.5 | 0.0-2.0 | Z-score for mean reversion exit |
| Min Correlation | 0.75 | 0.50-0.95 | Minimum AUDCAD-NZDCAD correlation |
| Max Spread | 3.0 | 0.5-10.0 | Maximum spread (pips) per symbol |
### Risk Management
| Parameter | Default | Range | Description |
|-----------|---------|-------|-------------|
| Fixed Lot Size | 0.01 | Min-Max | Lot size per basket leg |
| Risk % | 1.0 | 0.1-10.0 | Risk % per basket (if dynamic sizing) |
| Max Drawdown | 15.0 | 5-50 | Circuit breaker threshold (%) |
| Daily Loss Limit | $100 | 0-∞ | Daily loss limit in account currency |
| Max Holding Hours | 24 | 1-168 | Maximum basket hold time |
| Take Profit | $10 | 0-∞ | Basket take profit amount |
| Stop Loss | $15 | 0-∞ | Basket stop loss amount |
### Trading Hours
| Parameter | Default | Description |
|-----------|---------|-------------|
| Start Hour | 0 | Trading window start (broker time) |
| End Hour | 23 | Trading window end (broker time) |
| Avoid Rollover | true | Skip trading near rollover |
## Signal Logic
### Entry Conditions (all must pass)
1. ✓ Correlation data is valid
2. ✓ No basket currently open
3. ✓ Within trading hours
4. ✓ Not in rollover period
5. ✓ All symbol spreads below threshold
6. ✓ Primary correlation above minimum
7. ✓ Volatility not excessive
8. ✓ |Z-Score| exceeds entry threshold
### Exit Conditions (any triggers close)
1. Z-score returns to exit threshold (mean reversion)
2. Basket P&L exceeds take profit
3. Basket P&L exceeds stop loss
4. Maximum holding time reached
5. Correlation drops below critical level (0.5)
## Risk Management
### Circuit Breaker Triggers
| Condition | Warning | Trip (Halt Trading) |
|-----------|---------|---------------------|
| Drawdown | 60% of max | Max drawdown reached |
| Daily Loss | - | Daily limit exceeded |
| Margin Level | 500% | Below 200% |
| Consecutive Losses | - | 6 or more |
### Emergency Exit
Immediately closes all positions when:
- Margin level drops below 150%
- System detects critical error
## Backtesting
1. Open Strategy Tester in MT5
2. Select `DBasketEA.mq5`
3. Choose AUDCAD symbol (EA manages others internally)
4. Set timeframe (M15 or H1 recommended)
5. Select "Every tick based on real ticks" for accuracy
6. Set deposit and leverage matching your live account
7. Run test for minimum 1 year of data
### Optimization Tips
- The `OnTester()` function returns a custom score optimizing for:
- Risk-adjusted return (profit/drawdown)
- Win rate > 65%
- Profit factor > 1.3
- Minimum 20 trades
## Monitoring
The EA displays real-time metrics on the chart:
- Current status (NORMAL/WARNING/HALTED)
- Net P&L and percentage
- Daily P&L
- Current and maximum drawdown
- Basket count and win rate
- Consecutive losses
## Logging
Configurable log levels:
- **ERROR**: Critical errors only
- **WARNING**: Errors + warnings
- **INFO**: Normal operations (recommended)
- **DEBUG**: Full detail for troubleshooting
Enable file logging for persistent records.
## Troubleshooting
### "Hedging account required" error
Your broker account is in netting mode. Contact your broker to switch to a hedging account.
### EA not opening trades
Check:
1. Risk limits not breached (circuit breaker)
2. Current Z-score meets entry threshold
3. Spreads within limits
4. Within trading hours
5. Correlation above minimum
### Partial basket
If a basket is only partially filled, the EA will:
1. Attempt to complete the basket
2. If unable, close the opened legs
3. Log the issue for review
## Disclaimer
> âš ï¸ **Risk Warning**
>
> Trading forex involves significant risk. This EA is provided for educational purposes. Past performance does not guarantee future results. Always test thoroughly on demo accounts before live trading. Never risk more than you can afford to lose.
## License
---
## ?? License & Copyright
**Copyright © 2025 Dineth Pramodya**
**Website**: [www.dineth.lk](https://www.dineth.lk)
**All rights reserved.**
---
*Last Updated: December 28, 2025*
*Developed by: Dineth Pramodya*