//+------------------------------------------------------------------+ //| RiskManager.mqh v2.0 | //| Optimized Risk Management Module | //| Enhanced with position correlation analysis | //+------------------------------------------------------------------+ #ifndef RISK_MANAGER_MQH #define RISK_MANAGER_MQH #include "DataTypes.mqh" #include //+------------------------------------------------------------------+ //| Risk Manager Class - Optimized Version | //+------------------------------------------------------------------+ class CRiskManager { private: //--- Configuration RiskManagerConfig m_config; //--- Performance tracking for Kelly double m_win_rate; double m_avg_win_loss_ratio; int m_sample_size; //--- Daily tracking double m_daily_starting_balance; double m_daily_loss; datetime m_last_reset_day; //--- Exposure tracking struct ExposureInfo { string symbol; double long_exposure; double short_exposure; double net_exposure; int position_count; }; ExposureInfo m_exposures[]; //--- Correlation cache double m_correlation_matrix[][20]; // Max 20 symbols string m_correlation_symbols[20]; int m_correlation_count; datetime m_correlation_update_time; //--- Cache for expensive calculations double m_cached_total_exposure; datetime m_cache_time; static const int CACHE_SECONDS = 5; //--- Helper methods double CalculateKellyFraction(); double GetCurrentDrawdown(); void UpdateExposureMap(); double CalculateCorrelationRisk(string symbol, double volume, ENUM_POSITION_TYPE type); double GetSymbolCorrelation(string symbol1, string symbol2); void UpdateCorrelationMatrix(); public: CRiskManager(); ~CRiskManager(); //--- Initialization bool Initialize(const RiskManagerConfig &config); //--- Position sizing double CalculatePositionSize(double stop_distance, string symbol = NULL); double CalculateOptimalF(double win_rate, double avg_win, double avg_loss); //--- Risk validation bool ValidateNewPosition(double volume, double stop_distance, string symbol = NULL); bool CheckDailyDrawdown(); bool CheckTotalExposure(); double GetCurrentExposure(bool use_cache = true); //--- External trade management void EnforceRiskRules(ManagedTrade &trade, double default_risk, bool close_excessive); double CalculateImpliedRisk(const ManagedTrade &trade); bool ValidateExternalTrade(const ManagedTrade &trade); //--- Risk metrics double GetVaR(double confidence_level = 0.95); double GetMaxLossPerTrade() { return AccountInfoDouble(ACCOUNT_BALANCE) * m_config.max_risk_percent / 100; } double GetRemainingDailyRisk(); double GetPortfolioHeatmap(ExposureInfo &exposures[]); //--- Performance tracking void UpdatePerformanceMetrics(bool won, double profit, double risk); void ResetDailyMetrics(); //--- Market conditions void UpdateMarketConditions(const MarketConditions &conditions); //--- Multi-symbol support double CalculateSymbolRisk(string symbol, double volume, double stop_distance, ENUM_POSITION_TYPE type); bool ValidateSymbolExposure(string symbol, double additional_risk); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CRiskManager::CRiskManager() { m_win_rate = 0.5; // Default 50% m_avg_win_loss_ratio = 1.5; // Default 1.5:1 m_sample_size = 0; m_daily_starting_balance = 0; m_daily_loss = 0; m_last_reset_day = 0; m_cached_total_exposure = 0; m_cache_time = 0; m_correlation_count = 0; m_correlation_update_time = 0; } //+------------------------------------------------------------------+ //| Initialize risk manager | //+------------------------------------------------------------------+ bool CRiskManager::Initialize(const RiskManagerConfig &config) { m_config = config; //--- Validate configuration if(m_config.risk_percent <= 0 || m_config.risk_percent > 10) { Print("RiskManager: Invalid risk percent: ", m_config.risk_percent); return false; } //--- Initialize daily tracking m_daily_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE); m_daily_loss = 0; m_last_reset_day = TimeCurrent(); //--- Initialize correlation matrix ArrayInitialize(m_correlation_matrix, 0); Print("RiskManager initialized: Risk=", m_config.risk_percent, "%, MaxDD=", m_config.max_daily_dd, "%"); return true; } //+------------------------------------------------------------------+ //| Calculate position size with Kelly optimization | //+------------------------------------------------------------------+ double CRiskManager::CalculatePositionSize(double stop_distance, string symbol = NULL) { if(symbol == NULL) symbol = _Symbol; //--- Validate inputs if(stop_distance <= 0) { Print("RiskManager: Invalid stop distance: ", stop_distance); return 0; } double balance = AccountInfoDouble(ACCOUNT_BALANCE); double risk_amount = 0; //--- Check daily drawdown first if(!CheckDailyDrawdown()) { Print("RiskManager: Daily drawdown limit reached"); return 0; } //--- Apply position sizing mode switch(m_config.sizing_mode) { case PS_FIXED_LOTS: return m_config.min_lot_size; case PS_RISK_PERCENT: risk_amount = balance * m_config.risk_percent / 100.0; break; case PS_CAPITAL_PERCENT: { double capital_amount = balance * m_config.risk_percent / 100.0; // Convert capital to risk based on expected R:R risk_amount = capital_amount / m_avg_win_loss_ratio; } break; case PS_ATR_BASED: { // Dynamic risk based on volatility double atr_multiplier = MathMin(stop_distance / SymbolInfoDouble(symbol, SYMBOL_POINT) / 100, 2.0); risk_amount = balance * m_config.risk_percent * atr_multiplier / 100.0; } break; case PS_KELLY: { double kelly_fraction = CalculateKellyFraction(); risk_amount = balance * kelly_fraction; } break; } //--- Apply maximum risk limit double max_risk = balance * m_config.max_risk_percent / 100.0; risk_amount = MathMin(risk_amount, max_risk); //--- Calculate lots based on stop distance double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_value <= 0 || tick_size <= 0) { Print("RiskManager: Invalid tick parameters for ", symbol); return 0; } double lots = risk_amount / ((stop_distance / tick_size) * tick_value); //--- Normalize to symbol specifications double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); lots = MathFloor(lots / lot_step) * lot_step; lots = MathMax(lots, min_lot); lots = MathMin(lots, max_lot); //--- Apply configured limits lots = MathMax(lots, m_config.min_lot_size); lots = MathMin(lots, m_config.max_lot_size); //--- Final validation against exposure limits if(!ValidateNewPosition(lots, stop_distance, symbol)) return 0; return NormalizeDouble(lots, 2); } //+------------------------------------------------------------------+ //| Calculate Kelly fraction with safety margin | //+------------------------------------------------------------------+ double CRiskManager::CalculateKellyFraction() { //--- Need sufficient sample size if(m_sample_size < 20) return m_config.risk_percent / 100.0; // Use default until enough data //--- Kelly formula: f = (p*b - q) / b //--- Where: p = win probability, q = loss probability, b = win/loss ratio double p = m_win_rate; double q = 1 - p; double b = m_avg_win_loss_ratio; //--- Calculate raw Kelly fraction double kelly = (p * b - q) / b; //--- Apply safety factor (fractional Kelly) kelly *= m_config.kelly_fraction; //--- Ensure within reasonable bounds kelly = MathMax(kelly, 0.01); // Minimum 1% kelly = MathMin(kelly, m_config.max_risk_percent / 100.0); return kelly; } //+------------------------------------------------------------------+ //| Validate new position against all risk rules | //+------------------------------------------------------------------+ bool CRiskManager::ValidateNewPosition(double volume, double stop_distance, string symbol) { if(symbol == NULL) symbol = _Symbol; //--- Calculate position risk double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_size <= 0) return false; double position_risk = (stop_distance / tick_size) * tick_value * volume; double risk_percent = (position_risk / AccountInfoDouble(ACCOUNT_BALANCE)) * 100; //--- Check single position risk if(risk_percent > m_config.max_risk_percent) { Print("RiskManager: Position risk ", DoubleToString(risk_percent, 2), "% exceeds maximum ", m_config.max_risk_percent, "%"); return false; } //--- Check daily drawdown if(m_daily_loss + position_risk > AccountInfoDouble(ACCOUNT_BALANCE) * m_config.max_daily_dd / 100) { Print("RiskManager: Would exceed daily drawdown limit"); return false; } //--- Check total exposure double current_exposure = GetCurrentExposure(); if(current_exposure + risk_percent > m_config.max_exposure) { Print("RiskManager: Would exceed maximum exposure limit"); return false; } //--- Check correlation risk double correlation_risk = CalculateCorrelationRisk(symbol, volume, POSITION_TYPE_BUY); if(correlation_risk > m_config.max_risk_percent) { Print("RiskManager: High correlation risk detected: ", DoubleToString(correlation_risk, 2), "%"); return false; } return true; } //+------------------------------------------------------------------+ //| Get current total exposure with caching | //+------------------------------------------------------------------+ double CRiskManager::GetCurrentExposure(bool use_cache) { //--- Check cache if(use_cache && TimeCurrent() - m_cache_time < CACHE_SECONDS) return m_cached_total_exposure; //--- Update exposure map UpdateExposureMap(); double total_exposure = 0; double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Calculate total exposure across all symbols for(int i = 0; i < ArraySize(m_exposures); i++) { total_exposure += MathMax(m_exposures[i].long_exposure, m_exposures[i].short_exposure); } //--- Update cache m_cached_total_exposure = (balance > 0) ? (total_exposure / balance * 100) : 0; m_cache_time = TimeCurrent(); return m_cached_total_exposure; } //+------------------------------------------------------------------+ //| Update exposure map for all symbols | //+------------------------------------------------------------------+ void CRiskManager::UpdateExposureMap() { //--- Clear existing map ArrayResize(m_exposures, 0); //--- Temporary map for collecting exposures ExposureInfo temp_exposures[]; int exposure_count = 0; //--- Scan all positions int total = PositionsTotal(); for(int i = 0; i < total; i++) { if(PositionSelectByTicket(PositionGetTicket(i))) { string symbol = PositionGetString(POSITION_SYMBOL); double volume = PositionGetDouble(POSITION_VOLUME); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); double sl = PositionGetDouble(POSITION_SL); double open_price = PositionGetDouble(POSITION_PRICE_OPEN); //--- Calculate position risk double risk = 0; if(sl > 0) { double stop_distance = (type == POSITION_TYPE_BUY) ? (open_price - sl) : (sl - open_price); if(stop_distance > 0) { double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_size > 0) risk = (stop_distance / tick_size) * tick_value * volume; } } else { // Use default risk for positions without stop loss risk = AccountInfoDouble(ACCOUNT_BALANCE) * m_config.risk_percent / 100; } //--- Find or create symbol entry int idx = -1; for(int j = 0; j < exposure_count; j++) { if(temp_exposures[j].symbol == symbol) { idx = j; break; } } if(idx < 0) { ArrayResize(temp_exposures, exposure_count + 1); idx = exposure_count; temp_exposures[idx].symbol = symbol; temp_exposures[idx].long_exposure = 0; temp_exposures[idx].short_exposure = 0; temp_exposures[idx].position_count = 0; exposure_count++; } //--- Update exposure if(type == POSITION_TYPE_BUY) temp_exposures[idx].long_exposure += risk; else temp_exposures[idx].short_exposure += risk; temp_exposures[idx].position_count++; } } //--- Copy to member array ArrayResize(m_exposures, exposure_count); for(int i = 0; i < exposure_count; i++) { m_exposures[i] = temp_exposures[i]; m_exposures[i].net_exposure = m_exposures[i].long_exposure - m_exposures[i].short_exposure; } } //+------------------------------------------------------------------+ //| Calculate correlation risk for new position | //+------------------------------------------------------------------+ double CRiskManager::CalculateCorrelationRisk(string symbol, double volume, ENUM_POSITION_TYPE type) { //--- Update correlation matrix if needed if(TimeCurrent() - m_correlation_update_time > 3600) // Update hourly { UpdateCorrelationMatrix(); } double correlation_risk = 0; double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Check correlation with existing positions for(int i = 0; i < ArraySize(m_exposures); i++) { if(m_exposures[i].symbol == symbol) continue; double correlation = GetSymbolCorrelation(symbol, m_exposures[i].symbol); //--- High correlation threshold if(MathAbs(correlation) > 0.7) { double existing_exposure = (type == POSITION_TYPE_BUY) ? m_exposures[i].long_exposure : m_exposures[i].short_exposure; //--- Positive correlation increases risk for same direction //--- Negative correlation increases risk for opposite direction if(correlation > 0) { correlation_risk += existing_exposure * correlation / balance * 100; } else { double opposite_exposure = (type == POSITION_TYPE_BUY) ? m_exposures[i].short_exposure : m_exposures[i].long_exposure; correlation_risk += opposite_exposure * MathAbs(correlation) / balance * 100; } } } return correlation_risk; } //+------------------------------------------------------------------+ //| Update correlation matrix for monitored symbols | //+------------------------------------------------------------------+ void CRiskManager::UpdateCorrelationMatrix() { //--- Get unique symbols from positions m_correlation_count = 0; int total = PositionsTotal(); for(int i = 0; i < total && m_correlation_count < 20; i++) { if(PositionSelectByTicket(PositionGetTicket(i))) { string symbol = PositionGetString(POSITION_SYMBOL); bool found = false; for(int j = 0; j < m_correlation_count; j++) { if(m_correlation_symbols[j] == symbol) { found = true; break; } } if(!found) { m_correlation_symbols[m_correlation_count] = symbol; m_correlation_count++; } } } //--- Calculate correlations int period = 100; // 100 bar correlation for(int i = 0; i < m_correlation_count; i++) { for(int j = i; j < m_correlation_count; j++) { if(i == j) { m_correlation_matrix[i][j] = 1.0; } else { //--- Calculate correlation between symbols double prices1[], prices2[]; ArrayResize(prices1, period); ArrayResize(prices2, period); bool valid = true; for(int k = 0; k < period; k++) { prices1[k] = iClose(m_correlation_symbols[i], PERIOD_H1, k); prices2[k] = iClose(m_correlation_symbols[j], PERIOD_H1, k); if(prices1[k] <= 0 || prices2[k] <= 0) { valid = false; break; } } if(valid) { //--- Convert to returns for(int k = 0; k < period - 1; k++) { prices1[k] = (prices1[k] - prices1[k+1]) / prices1[k+1]; prices2[k] = (prices2[k] - prices2[k+1]) / prices2[k+1]; } //--- Calculate correlation coefficient double corr = MathCorrelationPearson(prices1, prices2, period - 1); m_correlation_matrix[i][j] = corr; m_correlation_matrix[j][i] = corr; } else { m_correlation_matrix[i][j] = 0; m_correlation_matrix[j][i] = 0; } } } } m_correlation_update_time = TimeCurrent(); } //+------------------------------------------------------------------+ //| Get correlation between two symbols | //+------------------------------------------------------------------+ double CRiskManager::GetSymbolCorrelation(string symbol1, string symbol2) { //--- Find indices int idx1 = -1, idx2 = -1; for(int i = 0; i < m_correlation_count; i++) { if(m_correlation_symbols[i] == symbol1) idx1 = i; if(m_correlation_symbols[i] == symbol2) idx2 = i; } //--- Return correlation if found if(idx1 >= 0 && idx2 >= 0) return m_correlation_matrix[idx1][idx2]; //--- Default no correlation return 0; } //+------------------------------------------------------------------+ //| Check daily drawdown limit | //+------------------------------------------------------------------+ bool CRiskManager::CheckDailyDrawdown() { //--- Reset daily metrics if new day MqlDateTime current_time; TimeToStruct(TimeCurrent(), current_time); MqlDateTime last_reset; TimeToStruct(m_last_reset_day, last_reset); if(current_time.day != last_reset.day) { ResetDailyMetrics(); } //--- Calculate current daily performance double current_balance = AccountInfoDouble(ACCOUNT_BALANCE); double daily_change = current_balance - m_daily_starting_balance; //--- Check if within daily drawdown limit double max_daily_loss = m_daily_starting_balance * m_config.max_daily_dd / 100; return (daily_change > -max_daily_loss); } //+------------------------------------------------------------------+ //| Reset daily metrics | //+------------------------------------------------------------------+ void CRiskManager::ResetDailyMetrics() { m_daily_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE); m_daily_loss = 0; m_last_reset_day = TimeCurrent(); Print("RiskManager: Daily metrics reset. Starting balance: ", m_daily_starting_balance); } //+------------------------------------------------------------------+ //| Enforce risk rules on external trades | //+------------------------------------------------------------------+ void CRiskManager::EnforceRiskRules(ManagedTrade &trade, double default_risk, bool close_excessive) { //--- Calculate implied risk double implied_risk = CalculateImpliedRisk(trade); //--- If no stop loss, use default risk if(trade.sl == 0) { trade.risk_percent = default_risk; trade.risk_amount = AccountInfoDouble(ACCOUNT_BALANCE) * default_risk / 100; } else { trade.risk_percent = implied_risk; } //--- Check if exceeds maximum allowed risk if(trade.risk_percent > m_config.max_risk_percent) { Print("RiskManager: External trade #", trade.ticket, " exceeds risk limit: ", DoubleToString(trade.risk_percent, 2), "%"); if(close_excessive) { //--- Mark for closure trade.close_requested = true; trade.close_reason = "Excessive risk: " + DoubleToString(trade.risk_percent, 2) + "%"; } } //--- Check correlation risk double corr_risk = CalculateCorrelationRisk(trade.symbol, trade.volume, trade.type); if(corr_risk > m_config.max_risk_percent * 0.5) // 50% of max risk for correlation { Print("RiskManager: High correlation risk for #", trade.ticket, ": ", DoubleToString(corr_risk, 2), "%"); } } //+------------------------------------------------------------------+ //| Calculate implied risk from position parameters | //+------------------------------------------------------------------+ double CRiskManager::CalculateImpliedRisk(const ManagedTrade &trade) { if(trade.sl == 0) return 0; double stop_distance; if(trade.type == POSITION_TYPE_BUY) stop_distance = trade.open_price - trade.sl; else stop_distance = trade.sl - trade.open_price; if(stop_distance <= 0) return 0; double tick_value = SymbolInfoDouble(trade.symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(trade.symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_size <= 0) return 0; double potential_loss = (stop_distance / tick_size) * tick_value * trade.volume; double balance = AccountInfoDouble(ACCOUNT_BALANCE); return (balance > 0) ? (potential_loss / balance * 100) : 0; } //+------------------------------------------------------------------+ //| Get portfolio heatmap | //+------------------------------------------------------------------+ double CRiskManager::GetPortfolioHeatmap(ExposureInfo &exposures[]) { UpdateExposureMap(); //--- Copy exposure data int size = ArraySize(m_exposures); ArrayResize(exposures, size); for(int i = 0; i < size; i++) { exposures[i] = m_exposures[i]; } return GetCurrentExposure(false); } //+------------------------------------------------------------------+ //| Update performance metrics for Kelly calculation | //+------------------------------------------------------------------+ void CRiskManager::UpdatePerformanceMetrics(bool won, double profit, double risk) { m_sample_size++; //--- Update win rate (exponential moving average) double alpha = 2.0 / (m_sample_size + 1); m_win_rate = m_win_rate * (1 - alpha) + (won ? 1.0 : 0.0) * alpha; //--- Update win/loss ratio if(won && risk > 0) { double rr_achieved = profit / risk; m_avg_win_loss_ratio = m_avg_win_loss_ratio * (1 - alpha) + rr_achieved * alpha; } //--- Update daily loss tracking if(!won) { m_daily_loss += MathAbs(profit); } } //+------------------------------------------------------------------+ //| Get Value at Risk (VaR) calculation | //+------------------------------------------------------------------+ double CRiskManager::GetVaR(double confidence_level) { //--- Simple VaR based on current exposure and volatility double exposure = GetCurrentExposure(); double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Use normal distribution assumption //--- z-score for 95% confidence = 1.645 double z_score = (confidence_level == 0.95) ? 1.645 : (confidence_level == 0.99) ? 2.326 : 1.645; //--- Estimate portfolio volatility (simplified) double portfolio_volatility = exposure / 100 * 0.02; // 2% daily volatility assumption return balance * portfolio_volatility * z_score; } //+------------------------------------------------------------------+ //| Get remaining daily risk allowance | //+------------------------------------------------------------------+ double CRiskManager::GetRemainingDailyRisk() { double max_daily_loss = m_daily_starting_balance * m_config.max_daily_dd / 100; double remaining = max_daily_loss - m_daily_loss; return MathMax(remaining, 0); } //+------------------------------------------------------------------+ //| Update market conditions | //+------------------------------------------------------------------+ void CRiskManager::UpdateMarketConditions(const MarketConditions &conditions) { //--- Adjust risk parameters based on market conditions if(conditions.condition == MARKET_VOLATILE) { //--- Reduce risk in volatile markets //--- This is just a notification, actual adjustment happens in position sizing Print("RiskManager: Volatile market detected - consider reducing position sizes"); } } #endif // RISK_MANAGER_MQH //+------------------------------------------------------------------+