| //+------------------------------------------------------------------+
|
| //| RiskManager_Impl.mqh |
|
| //| Copyright 2025, EscapeEA |
|
| //| https://www.escapeea.com |
|
| //+------------------------------------------------------------------+
|
| #property copyright "Copyright 2025, EscapeEA"
|
| #property link "https://www.escapeea.com"
|
| #property version "2.10"
|
| #property strict
|
|
|
| // Include necessary headers
|
| #include <Trade\Trade.mqh>
|
| #include <Trade\PositionInfo.mqh>
|
| #include <Trade\DealInfo.mqh>
|
| #include <Trade\OrderInfo.mqh>
|
| #include <Trade\HistoryOrderInfo.mqh>
|
| #include <Arrays\ArrayObj.mqh>
|
| #include <Arrays\ArrayString.mqh>
|
| #include <Arrays\List.mqh>
|
| #include <Files\FileBin.mqh>
|
| #include <Files\FileTxt.mqh>
|
| #include <Indicators\Oscilators.mqh>
|
| #include <Math\Stat\Math.mqh>
|
|
|
| // Logging levels
|
| enum ENUM_LOG_LEVEL
|
| {
|
| LOG_LEVEL_NONE = 0, // No logging
|
| LOG_LEVEL_ERROR, // Only errors
|
| LOG_LEVEL_WARNING, // Errors and warnings
|
| LOG_LEVEL_INFO, // Basic information
|
| LOG_LEVEL_DEBUG, // Detailed debug information
|
| LOG_LEVEL_TRACE // Very detailed tracing
|
| };
|
|
|
| // Trade metrics structure
|
| struct STradeMetrics
|
| {
|
| datetime timestamp; // Time of the trade
|
| string symbol; // Symbol
|
| double volume; // Trade volume
|
| double entryPrice; // Entry price
|
| double exitPrice; // Exit price
|
| double pips; // Pips gained/lost
|
| double profit; // Profit/loss in account currency
|
| double drawdown; // Drawdown at time of trade
|
| double volatility; // Market volatility (ATR)
|
| double spread; // Spread at time of trade
|
| int duration; // Trade duration in seconds
|
| string comment; // Additional comments
|
|
|
| // Default constructor
|
| STradeMetrics() : timestamp(0), symbol(""), volume(0), entryPrice(0), exitPrice(0),
|
| pips(0), profit(0), drawdown(0), volatility(0), spread(0), duration(0), comment("") {}
|
| };
|
|
|
| // Performance metrics structure
|
| struct SPerformanceMetrics
|
| {
|
| int totalTrades; // Total number of trades
|
| int winningTrades; // Number of winning trades
|
| double totalProfit; // Total profit
|
| double totalLoss; // Total loss
|
| double largestWin; // Largest winning trade
|
| double largestLoss; // Largest losing trade
|
| double maxDrawdown; // Maximum drawdown
|
| double profitFactor; // Profit factor (gross profit / gross loss)
|
| double recoveryFactor; // Net profit / max drawdown
|
| double sharpeRatio; // Risk-adjusted return
|
| double sortinoRatio; // Downside risk-adjusted return
|
| double averageTrade; // Average trade result
|
| double standardDeviation; // Standard deviation of returns
|
| double winRate; // Win rate percentage
|
|
|
| // Default constructor
|
| SPerformanceMetrics() : totalTrades(0), winningTrades(0), totalProfit(0), totalLoss(0),
|
| largestWin(0), largestLoss(0), maxDrawdown(0), profitFactor(0),
|
| recoveryFactor(0), sharpeRatio(0), sortinoRatio(0), averageTrade(0),
|
| standardDeviation(0), winRate(0) {}
|
| };
|
|
|
| // Forward declarations
|
| class CSecurityManager;
|
| class CTradeRiskRecord;
|
|
|
| // Include the header file to get class declaration
|
| #include "RiskManager.mqh"
|
|
|
| // Circuit breaker thresholds
|
| #define CIRCUIT_BREAKER_THRESHOLD 5 // Number of consecutive failures before tripping
|
| #define CIRCUIT_BREAKER_TIMEOUT 300 // Timeout in seconds before reset attempt
|
| #define MAX_RETRY_ATTEMPTS 3 // Maximum number of retry attempts
|
| #define INITIAL_RETRY_DELAY 1000 // Initial retry delay in milliseconds
|
| #define MAX_RETRY_DELAY 60000 // Maximum retry delay in milliseconds
|
|
|
| // Metrics file constants
|
| #define METRICS_FILE_PREFIX "RiskMetrics_"
|
| #define MAX_METRICS_ENTRIES 10000 // Maximum number of metrics entries to keep in memory
|
|
|
| // Log file constants
|
| #define LOG_FILE_PREFIX "RiskManager_"
|
| #define MAX_LOG_FILE_SIZE 10485760 // 10 MB max log file size
|
| #define MAX_LOG_FILES 5 // Maximum number of log files to keep
|
|
|
| // Define missing ENUM_ACCOUNT_INFO_DOUBLE values if not already defined
|
| #ifndef ACCOUNT_COMMISSION
|
| #define ACCOUNT_COMMISSION 16
|
| #endif
|
|
|
| #ifndef ACCOUNT_SWAP
|
| #define ACCOUNT_SWAP 32
|
| #endif
|
|
|
| //+------------------------------------------------------------------+
|
| //| Constructor |
|
| //+------------------------------------------------------------------+
|
| CRiskManager::CRiskManager() :
|
| m_security(NULL),
|
| m_riskPerTrade(1.0),
|
| m_dailyLossLimit(2.0),
|
| m_maxOpenTrades(5),
|
| m_maxOrdersPerDay(20),
|
| m_useVolatilityAdjustment(true),
|
| m_atrPeriod(14),
|
| m_maxSpread(50.0),
|
| m_circuitBreakerActive(false),
|
| m_consecutiveFailures(0),
|
| m_circuitBreakerTime(0),
|
| m_logLevel(LOG_LEVEL_INFO),
|
| m_enableMetrics(true),
|
| m_enableDetailedLogging(false),
|
| m_lastMetricsUpdate(0),
|
| m_metricsUpdateInterval(60), // Update metrics every 60 seconds
|
| m_retryAttempts(0),
|
| m_nextRetryTime(0),
|
| m_anomalyDetection(true),
|
| m_maxLeverage(30),
|
| m_dailyPnL(0.0),
|
| m_todayOrders(0),
|
| m_lastTradeDay(0),
|
| m_equityHigh(0.0),
|
| m_maxDrawdown(0.0),
|
| m_openTrades(NULL),
|
| m_watchlist(NULL),
|
| m_lastError(""),
|
| m_lastErrorSeverity(RISK_ERROR_NONE),
|
| m_atrHandle(INVALID_HANDLE),
|
| m_rateLimitHistory(NULL),
|
| m_enableRateLimiting(true),
|
| m_maxRequestsPerMinute(60),
|
| m_ordersToday(0)
|
| {
|
| m_openTrades = new CArrayObj();
|
| m_watchlist = new CArrayString();
|
| m_security = new CSecurityManager();
|
| m_rateLimitHistory = new CArrayObj();
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Destructor |
|
| //+------------------------------------------------------------------+
|
| CRiskManager::~CRiskManager()
|
| {
|
| if(CheckPointer(m_openTrades) == POINTER_DYNAMIC)
|
| delete m_openTrades;
|
|
|
| if(CheckPointer(m_watchlist) == POINTER_DYNAMIC)
|
| delete m_watchlist;
|
|
|
| if(CheckPointer(m_security) == POINTER_DYNAMIC)
|
| delete m_security;
|
|
|
| if(CheckPointer(m_rateLimitHistory) == POINTER_DYNAMIC)
|
| delete m_rateLimitHistory;
|
|
|
| if(m_atrHandle != INVALID_HANDLE)
|
| IndicatorRelease(m_atrHandle);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Initialize the risk manager |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::Initialize()
|
| {
|
| if(m_initialized)
|
| {
|
| m_lastError = "Risk manager already initialized";
|
| return true;
|
| }
|
|
|
| // Initialize ATR indicator for volatility calculation
|
| m_atrHandle = iATR(_Symbol, PERIOD_CURRENT, m_atrPeriod);
|
| if(m_atrHandle == INVALID_HANDLE)
|
| {
|
| m_lastError = "Failed to create ATR indicator";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Initialize security manager
|
| if(m_security == NULL)
|
| {
|
| m_security = new CSecurityManager();
|
| }
|
|
|
| if(!m_security.Initialize())
|
| {
|
| m_lastError = "Failed to initialize security manager: " + m_security.GetLastError();
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Initialize state
|
| m_equityHigh = AccountInfoDouble(ACCOUNT_EQUITY);
|
| m_lastTradeDay = TimeCurrent();
|
| m_initialized = true;
|
|
|
| // Load configuration
|
| if(!LoadConfiguration())
|
| {
|
| m_lastError = "Warning: Using default configuration - " + m_lastError;
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Deinitialize the risk manager |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::Deinitialize()
|
| {
|
| if(m_atrHandle != INVALID_HANDLE)
|
| {
|
| IndicatorRelease(m_atrHandle);
|
| m_atrHandle = INVALID_HANDLE;
|
| }
|
|
|
| if(CheckPointer(m_security) == POINTER_DYNAMIC)
|
| {
|
| m_security.Deinitialize();
|
| }
|
|
|
| m_initialized = false;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Validate a trade request |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::ValidateTrade(const string symbol, double volume, double price, double sl, double tp)
|
| {
|
| if(!m_initialized)
|
| {
|
| m_lastError = "Risk manager not initialized";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Check if trading is allowed
|
| if(!IsTradingAllowed())
|
| return false;
|
|
|
| // Check if symbol is in watchlist
|
| if(m_watchlist.Total() > 0 && m_watchlist.SearchLinear(symbol) < 0)
|
| {
|
| m_lastError = "Symbol " + symbol + " not in watchlist";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| // Validate lot size
|
| if(!ValidateLotSize(symbol, volume))
|
| return false;
|
|
|
| // Validate spread
|
| if(!ValidateSpread(symbol))
|
| return false;
|
|
|
| // Check daily loss limit
|
| if(!ValidateDailyLossLimit())
|
| return false;
|
|
|
| // Check circuit breaker
|
| if(m_circuitBreakerActive)
|
| {
|
| m_lastError = "Trading suspended - circuit breaker active";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Validate lot size |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::ValidateLotSize(const string symbol, double volume)
|
| {
|
| double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
| double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
| double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
|
| if(volume < minLot || volume > maxLot)
|
| {
|
| m_lastError = StringFormat("Invalid lot size %.2f for %s (min: %.2f, max: %.2f)",
|
| volume, symbol, minLot, maxLot);
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| // Check if volume is a multiple of lot step
|
| if(MathMod(volume, lotStep) > 0.0001) // Account for floating point precision
|
| {
|
| m_lastError = StringFormat("Lot size %.2f for %s must be a multiple of %.2f",
|
| volume, symbol, lotStep);
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Validate spread |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::ValidateSpread(const string symbol)
|
| {
|
| double spread = SymbolInfoInteger(symbol, SYMBOL_SPREAD);
|
| if(spread > m_maxSpread)
|
| {
|
| m_lastError = StringFormat("Spread %.1f for %s exceeds maximum allowed (%.1f)",
|
| spread, symbol, m_maxSpread);
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Validate daily loss limit |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::ValidateDailyLossLimit()
|
| {
|
| if(m_dailyPnL < 0 && MathAbs(m_dailyPnL) >= (AccountInfoDouble(ACCOUNT_BALANCE) * m_dailyLossLimit / 100.0))
|
| {
|
| m_lastError = StringFormat("Daily loss limit reached (%.2f%% of balance)", m_dailyLossLimit);
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate position size based on risk parameters |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculatePositionSize(const string symbol, const double entryPrice, const double stopLoss, const double riskAmount)
|
| {
|
| if(entryPrice <= 0 || stopLoss <= 0 || riskAmount <= 0)
|
| {
|
| m_lastError = "Invalid parameters for position sizing";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return 0.0;
|
| }
|
|
|
| double positionSize = 0.0;
|
|
|
| // Select position sizing method
|
| switch(m_positionSizingMethod)
|
| {
|
| case POSITION_SIZING_FIXED:
|
| positionSize = m_fixedLotSize;
|
| break;
|
|
|
| case POSITION_SIZING_VOLATILITY:
|
| positionSize = CalculateVolatilityBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
| break;
|
|
|
| case POSITION_SIZING_KELLY:
|
| {
|
| double winRate = CalculateWinRate(symbol);
|
| double winLossRatio = CalculateWinLossRatio(symbol);
|
| positionSize = CalculateKellySize(symbol, entryPrice, stopLoss, winRate, winLossRatio);
|
| break;
|
| }
|
|
|
| case POSITION_SIZING_OPTIMAL_F:
|
| {
|
| double winRate = CalculateWinRate(symbol);
|
| double winLossRatio = CalculateWinLossRatio(symbol);
|
| positionSize = CalculateOptimalFSize(symbol, entryPrice, stopLoss, winRate, winLossRatio);
|
| break;
|
| }
|
|
|
| case POSITION_SIZING_HYBRID:
|
| {
|
| double winRate = CalculateWinRate(symbol);
|
| double winLossRatio = CalculateWinLossRatio(symbol);
|
| positionSize = CalculateHybridSize(symbol, entryPrice, stopLoss, riskAmount, winRate, winLossRatio);
|
| break;
|
| }
|
|
|
| case POSITION_SIZING_RISK_BASED:
|
| default:
|
| positionSize = CalculateRiskBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
| }
|
|
|
| // Apply position scaling based on account growth if enabled
|
| if(m_positionScaling != SCALING_NONE)
|
| {
|
| double initialBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
| double currentBalance = AccountInfoDouble(ACCOUNT_EQUITY);
|
| double growthFactor = (currentBalance - initialBalance) / initialBalance;
|
| positionSize = ScalePositionSize(positionSize, growthFactor);
|
| }
|
|
|
| // Apply position size limits
|
| positionSize = MathMax(m_minPositionSize, MathMin(positionSize, m_maxPositionSize));
|
|
|
| // Round to nearest step
|
| if(m_positionSizeStep > 0)
|
| {
|
| positionSize = MathRound(positionSize / m_positionSizeStep) * m_positionSizeStep;
|
| }
|
|
|
| return NormalizeLotSize(positionSize);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate position size based on volatility (ATR) |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateVolatilityBasedSize(const string symbol, const double entryPrice, const double stopLoss, const double riskAmount)
|
| {
|
| double atrValue = GetATRValue(symbol, PERIOD_CURRENT, (int)m_volatilityPeriod);
|
|
|
| if(atrValue <= 0)
|
| {
|
| // Fall back to standard risk-based if ATR is not available
|
| return CalculateRiskBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
| }
|
|
|
| double stopDistance = MathAbs(entryPrice - stopLoss);
|
| double atrMultiple = stopDistance / atrValue;
|
|
|
| // Adjust risk amount based on volatility
|
| double adjustedRisk = riskAmount * (1.0 + (1.0 - 1.0 / (1.0 + atrMultiple)) * m_volatilityMultiplier);
|
|
|
| return CalculateRiskBasedSize(symbol, entryPrice, stopLoss, adjustedRisk);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate position size using Kelly Criterion |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateKellySize(const string symbol, const double entryPrice, const double stopLoss,
|
| const double winRate, const double winLossRatio)
|
| {
|
| if(winRate <= 0 || winRate >= 1.0 || winLossRatio <= 0)
|
| {
|
| // Fall back to standard risk-based if invalid parameters
|
| return CalculateRiskBasedSize(symbol, entryPrice, stopLoss, m_riskPerTrade);
|
| }
|
|
|
| // Kelly formula: f* = (bp - q) / b
|
| // where: f* = fraction of current bankroll to wager
|
| // b = net odds received on wager (win amount / loss amount)
|
| // p = probability of winning
|
| // q = probability of losing (1 - p)
|
| double kellyFraction = (winRate * winLossRatio - (1.0 - winRate)) / winLossRatio;
|
|
|
| // Apply user-defined fraction of Kelly (0.0 to 1.0)
|
| kellyFraction = MathMax(0.0, MathMin(kellyFraction * m_kellyFraction, 1.0));
|
|
|
| // Calculate position size based on Kelly fraction
|
| double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
| double riskAmount = accountBalance * kellyFraction;
|
|
|
| return CalculateRiskBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate position size using Optimal F |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateOptimalFSize(const string symbol, const double entryPrice, const double stopLoss,
|
| const double winRate, const double winLossRatio)
|
| {
|
| if(winRate <= 0 || winRate >= 1.0 || winLossRatio <= 0)
|
| {
|
| // Fall back to standard risk-based if invalid parameters
|
| return CalculateRiskBasedSize(symbol, entryPrice, stopLoss, m_riskPerTrade);
|
| }
|
|
|
| // Simplified Optimal F calculation
|
| // Optimal f = ( (1 + winLossRatio) * winRate - 1 ) / winLossRatio
|
| double optimalF = ((1.0 + winLossRatio) * winRate - 1.0) / winLossRatio;
|
|
|
| // Apply user-defined fraction of Optimal F (0.0 to 1.0)
|
| optimalF = MathMax(0.0, MathMin(optimalF * m_optimalFFraction, 1.0));
|
|
|
| // Calculate position size based on Optimal F
|
| double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
| double riskAmount = accountBalance * optimalF;
|
|
|
| return CalculateRiskBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate position size using hybrid approach |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateHybridSize(const string symbol, const double entryPrice, const double stopLoss,
|
| const double riskAmount, const double winRate, const double winLossRatio)
|
| {
|
| // Calculate position sizes using different methods
|
| double riskBased = CalculateRiskBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
| double volatilityBased = CalculateVolatilityBasedSize(symbol, entryPrice, stopLoss, riskAmount);
|
|
|
| // If we have valid win rate and win/loss ratio, include Kelly and Optimal F
|
| if(winRate > 0 && winRate < 1.0 && winLossRatio > 0)
|
| {
|
| double kellySize = CalculateKellySize(symbol, entryPrice, stopLoss, winRate, winLossRatio);
|
| double optimalFSize = CalculateOptimalFSize(symbol, entryPrice, stopLoss, winRate, winLossRatio);
|
|
|
| // Use weighted average of all methods
|
| return (riskBased * 0.2 + volatilityBased * 0.3 + kellySize * 0.25 + optimalFSize * 0.25);
|
| }
|
|
|
| // Fall back to average of risk-based and volatility-based
|
| return (riskBased + volatilityBased) / 2.0;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Scale position size based on account growth |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::ScalePositionSize(const double size, const double accountGrowth)
|
| {
|
| if(accountGrowth <= 0 || m_positionScaling == SCALING_NONE)
|
| {
|
| return size;
|
| }
|
|
|
| double scaleFactor = 1.0;
|
|
|
| switch(m_positionScaling)
|
| {
|
| case SCALING_LINEAR:
|
| scaleFactor = 1.0 + accountGrowth;
|
| break;
|
|
|
| case SCALING_SQRT:
|
| scaleFactor = MathSqrt(1.0 + accountGrowth);
|
| break;
|
|
|
| case SCALING_LOG:
|
| scaleFactor = 1.0 + MathLog(1.0 + accountGrowth);
|
| break;
|
|
|
| case SCALING_NONE:
|
| default:
|
| return size;
|
| }
|
|
|
| return size * scaleFactor;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate position size based on risk (base method) |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateRiskBasedSize(const string symbol, const double entryPrice,
|
| const double stopLoss, const double riskAmount)
|
| {
|
| if(entryPrice <= 0 || stopLoss <= 0 || riskAmount <= 0)
|
| {
|
| m_lastError = "Invalid parameters for risk-based position sizing";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return 0.0;
|
| }
|
|
|
| double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
| double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
| double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
|
| if(tickSize <= 0 || tickValue <= 0 || lotStep <= 0)
|
| {
|
| m_lastError = "Invalid symbol parameters for position sizing";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return 0.0;
|
| }
|
|
|
| double stopDistance = MathAbs(entryPrice - stopLoss) / tickSize;
|
| double riskPerLot = stopDistance * tickValue;
|
|
|
| if(riskPerLot <= 0)
|
| {
|
| m_lastError = "Invalid risk per lot calculation";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return 0.0;
|
| }
|
|
|
| double lots = riskAmount / riskPerLot;
|
|
|
| // Normalize to lot step
|
| lots = MathFloor(lots / lotStep) * lotStep;
|
|
|
| // Check against symbol limits
|
| double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
| double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
|
| return MathMax(minLot, MathMin(lots, maxLot));
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate risk amount based on account balance and risk percent |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateRiskAmount(double accountBalance, double riskPercent)
|
| {
|
| if(accountBalance <= 0 || riskPercent <= 0)
|
| return 0.0;
|
|
|
| return accountBalance * riskPercent / 100.0;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate risk for a trade |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::CalculateTradeRisk(const string symbol, double volume, double entryPrice, double stopLoss)
|
| {
|
| if(entryPrice <= 0 || stopLoss <= 0 || volume <= 0)
|
| return 0.0;
|
|
|
| double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
| double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
| double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
| if(tickSize == 0 || tickValue == 0 || point == 0)
|
| return 0.0;
|
|
|
| double riskPoints = MathAbs(entryPrice - stopLoss) / point;
|
| return riskPoints * tickValue * volume / (tickSize / point);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Get current risk exposure |
|
| //+------------------------------------------------------------------+
|
| double CRiskManager::GetCurrentRiskExposure()
|
| {
|
| if(!m_initialized)
|
| return 0.0;
|
|
|
| double totalRisk = 0.0;
|
|
|
| // Calculate risk from open positions
|
| for(int i = 0; i < m_openTrades.Total(); i++)
|
| {
|
| CTradeRiskRecord* trade = m_openTrades.At(i);
|
| if(trade != NULL)
|
| {
|
| totalRisk += CalculateTradeRisk(trade.symbol, trade.volume, trade.entry, trade.sl);
|
| }
|
| }
|
|
|
| return totalRisk;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Check circuit breaker status |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::CheckCircuitBreaker()
|
| {
|
| if(!m_initialized)
|
| {
|
| m_lastError = "Risk manager not initialized";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
| double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
| double drawdown = 1.0 - (equity / m_equityHigh);
|
|
|
| if(drawdown >= m_circuitBreakerLevel)
|
| {
|
| m_circuitBreakerActive = true;
|
| m_circuitBreakerLevel = drawdown;
|
| m_lastError = StringFormat("Circuit breaker triggered at %.2f%% drawdown", drawdown * 100.0);
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Reset circuit breaker |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::ResetCircuitBreaker()
|
| {
|
| m_circuitBreakerActive = false;
|
| m_equityHigh = AccountInfoDouble(ACCOUNT_EQUITY);
|
| m_lastError = "Circuit breaker reset";
|
| m_lastErrorSeverity = RISK_ERROR_NONE;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Load configuration from file |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::LoadConfiguration()
|
| {
|
| string filename = "RiskManager.cfg";
|
| int handle = FileOpen(filename, FILE_READ|FILE_BIN|FILE_COMMON);
|
|
|
| if(handle == INVALID_HANDLE)
|
| {
|
| m_lastError = "No configuration file found, using defaults";
|
| return false;
|
| }
|
|
|
| // Read configuration values
|
| m_riskPerTrade = FileReadDouble(handle);
|
| m_dailyLossLimit = FileReadDouble(handle);
|
| m_maxOpenTrades = (int)FileReadInteger(handle);
|
| m_maxOrdersPerDay = (int)FileReadInteger(handle);
|
| m_useVolatilityAdjustment = FileReadInteger(handle) != 0;
|
| m_atrPeriod = (int)FileReadInteger(handle);
|
| m_maxSpread = FileReadDouble(handle);
|
| m_anomalyDetection = FileReadInteger(handle) != 0;
|
| m_maxLeverage = (int)FileReadInteger(handle);
|
| m_enableRateLimiting = FileReadInteger(handle) != 0;
|
| m_maxRequestsPerMinute = (int)FileReadInteger(handle);
|
|
|
| FileClose(handle);
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Save configuration to file |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::SaveConfiguration()
|
| {
|
| if(!m_initialized)
|
| {
|
| m_lastError = "Risk manager not initialized";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| string filename = "RiskManager.cfg";
|
| int handle = FileOpen(filename, FILE_WRITE|FILE_BIN|FILE_COMMON);
|
|
|
| if(handle == INVALID_HANDLE)
|
| {
|
| m_lastError = "Failed to open configuration file for writing";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| // Write configuration values
|
| FileWriteDouble(handle, m_riskPerTrade);
|
| FileWriteDouble(handle, m_dailyLossLimit);
|
| FileWriteInteger(handle, m_maxOpenTrades);
|
| FileWriteInteger(handle, m_maxOrdersPerDay);
|
| FileWriteInteger(handle, m_useVolatilityAdjustment ? 1 : 0);
|
| FileWriteInteger(handle, m_atrPeriod);
|
| FileWriteDouble(handle, m_maxSpread);
|
| FileWriteInteger(handle, m_anomalyDetection ? 1 : 0);
|
| FileWriteInteger(handle, m_maxLeverage);
|
| FileWriteInteger(handle, m_enableRateLimiting ? 1 : 0);
|
| FileWriteInteger(handle, m_maxRequestsPerMinute);
|
|
|
| FileClose(handle);
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Event handler for tick events |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::OnTick()
|
| {
|
| if(!m_initialized)
|
| return;
|
|
|
| // Update equity high water mark
|
| double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
| if(equity > m_equityHigh)
|
| m_equityHigh = equity;
|
|
|
| // Check for new trading day
|
| MqlDateTime dt;
|
| TimeCurrent(dt);
|
| MqlDateTime lastDt;
|
| TimeToStruct(m_lastTradeDay, lastDt);
|
|
|
| if(dt.day != lastDt.day || dt.mon != lastDt.mon || dt.year != lastDt.year)
|
| {
|
| // New trading day - reset daily counters
|
| m_todayOrders = 0;
|
| m_dailyPnL = 0;
|
| m_lastTradeDay = TimeCurrent();
|
| }
|
|
|
| // Check circuit breaker
|
| CheckCircuitBreaker();
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Event handler for trade events |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::OnTrade()
|
| {
|
| if(!m_initialized)
|
| return;
|
|
|
| // Update open trades list
|
| UpdateOpenTrades();
|
|
|
| // Update daily P&L
|
| UpdateDailyPnL();
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Update list of open trades |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::UpdateOpenTrades()
|
| {
|
| if(!m_initialized)
|
| return;
|
|
|
| m_openTrades.Clear();
|
|
|
| // Get all open positions
|
| for(int i = 0; i < PositionsTotal(); i++)
|
| {
|
| ulong ticket = PositionGetTicket(i);
|
| if(ticket > 0)
|
| {
|
| CTradeRiskRecord* trade = new CTradeRiskRecord();
|
| trade.ticket = ticket;
|
| trade.symbol = PositionGetString(POSITION_SYMBOL);
|
| trade.volume = PositionGetDouble(POSITION_VOLUME);
|
| trade.entry = PositionGetDouble(POSITION_PRICE_OPEN);
|
| trade.sl = PositionGetDouble(POSITION_SL);
|
| trade.tp = PositionGetDouble(POSITION_TP);
|
| trade.risk = CalculateTradeRisk(trade.symbol, trade.volume, trade.entry, trade.sl);
|
| trade.openTime = (datetime)PositionGetInteger(POSITION_TIME);
|
|
|
| m_openTrades.Add(trade);
|
| }
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Update daily P&L |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::UpdateDailyPnL()
|
| {
|
| if(!m_initialized)
|
| return;
|
|
|
| m_dailyPnL = 0.0;
|
|
|
| // Calculate P&L for today's closed trades
|
| HistorySelect(TimeCurrent() - 86400, TimeCurrent());
|
| int totalDeals = HistoryDealsTotal();
|
|
|
| for(int i = 0; i < totalDeals; i++)
|
| {
|
| ulong ticket = HistoryDealGetTicket(i);
|
| if(ticket > 0)
|
| {
|
| datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
|
| if(TimeDay(dealTime) == TimeDay(TimeCurrent()))
|
| {
|
| double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) +
|
| HistoryDealGetDouble(ticket, DEAL_COMMISSION) +
|
| HistoryDealGetDouble(ticket, DEAL_SWAP);
|
| m_dailyPnL += profit;
|
| }
|
| }
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Event handler for trade transactions |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::OnTradeTransaction(const MqlTradeTransaction& trans,
|
| const MqlTradeRequest& request,
|
| const MqlTradeResult& result)
|
| {
|
| if(!m_initialized)
|
| return;
|
|
|
| // Increment order counter for the day
|
| if(trans.type == TRADE_TRANSACTION_ORDER_ADD ||
|
| trans.type == TRADE_TRANSACTION_ORDER_UPDATE ||
|
| trans.type == TRADE_TRANSACTION_ORDER_DELETE)
|
| {
|
| m_todayOrders++;
|
| }
|
|
|
| // Update open trades if position was opened/closed
|
| if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
|
| {
|
| UpdateOpenTrades();
|
| UpdateDailyPnL();
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Check if trading is allowed |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::IsTradingAllowed()
|
| {
|
| if(!m_initialized)
|
| {
|
| m_lastError = "Risk manager not initialized";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Check circuit breaker
|
| if(m_circuitBreakerActive)
|
| {
|
| m_lastError = "Circuit breaker active";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Check security manager
|
| if(m_security != NULL && !m_security.IsTradingAllowed())
|
| {
|
| m_lastError = "Security restrictions: " + m_security.GetLastError();
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Check daily order limit
|
| if(m_todayOrders >= m_maxOrdersPerDay)
|
| {
|
| m_lastError = "Daily order limit reached";
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Check open trades limit
|
| if(m_openTrades.Total() >= m_maxOpenTrades)
|
| {
|
| m_lastError = "Maximum number of open trades reached";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Save risk manager state |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::Save(const int handle)
|
| {
|
| if(handle == INVALID_HANDLE)
|
| {
|
| m_lastError = "Invalid file handle";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| // Save state variables
|
| FileWriteLong(handle, m_lastTradeDay);
|
| FileWriteDouble(handle, m_equityHigh);
|
| FileWriteDouble(handle, m_dailyPnL);
|
| FileWriteInteger(handle, m_todayOrders);
|
| FileWriteInteger(handle, m_circuitBreakerActive ? 1 : 0);
|
| FileWriteDouble(handle, m_circuitBreakerLevel);
|
| FileWriteInteger(handle, m_ordersToday);
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Load risk manager state |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::Load(const int handle)
|
| {
|
| if(handle == INVALID_HANDLE)
|
| {
|
| m_lastError = "Invalid file handle";
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| // Load state variables
|
| m_lastTradeDay = (datetime)FileReadLong(handle);
|
| m_equityHigh = FileReadDouble(handle);
|
| m_dailyPnL = FileReadDouble(handle);
|
| m_todayOrders = (int)FileReadInteger(handle);
|
| m_circuitBreakerActive = FileReadInteger(handle) != 0;
|
| m_circuitBreakerLevel = FileReadDouble(handle);
|
| m_ordersToday = (int)FileReadInteger(handle);
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Update symbol cache with latest market data |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::UpdateSymbolCache(const string symbol)
|
| {
|
| // Implementation for updating symbol cache
|
| // This is a placeholder - actual implementation would depend on requirements
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Check if rate limit is exceeded |
|
| //+------------------------------------------------------------------+
|
| bool CRiskManager::CheckRateLimit()
|
| {
|
| // Thread-safe section
|
| CCriticalLock lock(m_cs);
|
|
|
| // Check if rate limiting is enabled
|
| if(!m_enableRateLimiting)
|
| {
|
| m_lastErrorCode = RISK_ERROR_NONE;
|
| return true; // Rate limiting is disabled
|
| }
|
|
|
| // Get current time once to ensure consistency
|
| datetime currentTime = TimeCurrent();
|
| MqlDateTime timeStruct;
|
| if(!TimeToStruct(currentTime, timeStruct))
|
| {
|
| m_lastError = FormatError("Failed to get time structure", RISK_ERROR_INVALID_STATE);
|
| m_lastErrorCode = RISK_ERROR_INVALID_STATE;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Validate configuration
|
| if(!ValidateConfig())
|
| {
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Validate member variables
|
| if(CheckPointer(m_rateLimitHistory) == POINTER_INVALID)
|
| {
|
| m_lastError = FormatError("Rate limit history not initialized", RISK_ERROR_INVALID_STATE);
|
| m_lastErrorCode = RISK_ERROR_INVALID_STATE;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Clean up old entries from the rate limit history
|
| for(int i = m_rateLimitHistory.Total() - 1; i >= 0; i--)
|
| {
|
| CTradeRiskRecord* record = m_rateLimitHistory.At(i);
|
| if(CheckPointer(record) == POINTER_INVALID)
|
| {
|
| m_rateLimitHistory.Delete(i);
|
| continue;
|
| }
|
|
|
| // Remove entries older than our rate limit window (1 minute)
|
| if(currentTime - record.Time() > 60) // 60 seconds window
|
| {
|
| CTradeRiskRecord* oldRecord = m_rateLimitHistory.Detach(i);
|
| if(CheckPointer(oldRecord) == POINTER_DYNAMIC)
|
| delete oldRecord;
|
| }
|
| }
|
|
|
| // Check if we've exceeded the maximum allowed requests per minute
|
| if(m_rateLimitHistory.Total() >= m_maxRequestsPerMinute)
|
| {
|
| m_lastError = FormatError("Rate limit exceeded: " +
|
| IntegerToString(m_rateLimitHistory.Total()) +
|
| " requests in the last minute",
|
| RISK_ERROR_RATE_LIMIT);
|
| m_lastErrorCode = RISK_ERROR_RATE_LIMIT;
|
| m_lastErrorSeverity = RISK_ERROR_WARNING;
|
| return false;
|
| }
|
|
|
| // Check if we've exceeded the maximum concurrent open trades
|
| int totalPositions = PositionsTotal();
|
| if(totalPositions < 0) // Handle error case
|
| {
|
| m_lastError = FormatError("Failed to get open positions count", RISK_ERROR_INVALID_STATE);
|
| m_lastErrorCode = RISK_ERROR_INVALID_STATE;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| if(totalPositions >= m_maxOpenTrades)
|
| {
|
| m_lastError = FormatError("Maximum open trades limit reached: " +
|
| IntegerToString(m_maxOpenTrades),
|
| RISK_ERROR_TRADE_LIMIT);
|
| m_lastErrorCode = RISK_ERROR_TRADE_LIMIT;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Check if we've exceeded daily order limit with boundary check
|
| if(m_ordersToday < 0)
|
| m_ordersToday = 0; // Fix corrupted counter
|
|
|
| if(m_ordersToday >= m_maxOrdersPerDay)
|
| {
|
| m_lastError = FormatError("Daily order limit reached: " +
|
| IntegerToString(m_ordersToday) + " of " +
|
| IntegerToString(m_maxOrdersPerDay),
|
| RISK_ERROR_DAILY_LIMIT);
|
| m_lastErrorCode = RISK_ERROR_DAILY_LIMIT;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Add current request to rate limit history with memory safety
|
| CTradeRiskRecord* newRecord = new CTradeRiskRecord();
|
| if(CheckPointer(newRecord) == POINTER_INVALID)
|
| {
|
| m_lastError = FormatError("Failed to allocate memory for rate limit tracking",
|
| RISK_ERROR_MEMORY);
|
| m_lastErrorCode = RISK_ERROR_MEMORY;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| newRecord.SetTime(currentTime);
|
| if(m_rateLimitHistory.Add(newRecord) < 0)
|
| {
|
| delete newRecord;
|
| m_lastError = FormatError("Failed to track rate limit", RISK_ERROR_INVALID_STATE);
|
| m_lastErrorCode = RISK_ERROR_INVALID_STATE;
|
| m_lastErrorSeverity = RISK_ERROR_CRITICAL;
|
| return false;
|
| }
|
|
|
| // Reset daily counters at the start of a new trading day
|
| if(timeStruct.day != m_lastTradeDay)
|
| {
|
| m_lastTradeDay = timeStruct.day;
|
| m_ordersToday = 0;
|
| m_todayOrders = 0;
|
| }
|
|
|
| m_lastErrorCode = RISK_ERROR_NONE;
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Adversarial testing: Simulate slippage |
|
| //+------------------------------------------------------------------+
|
| #ifdef ADVERSARIAL_TESTING
|
| void CRiskManager::SimulateSlippage()
|
| {
|
| // Implementation for slippage simulation in testing
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Adversarial testing: Simulate network latency |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::SimulateNetworkLatency()
|
| {
|
| // Implementation for network latency simulation in testing
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Adversarial testing: Inject fault |
|
| //+------------------------------------------------------------------+
|
| void CRiskManager::InjectFault(const string component, const string type)
|
| {
|
| // Implementation for fault injection in testing
|
| }
|
| #endif
|