//+------------------------------------------------------------------+ //| RiskManager.mqh | //| Risk Management Module 2.1 | //| Fixed: Lot size calculation accuracy | //+------------------------------------------------------------------+ #ifndef RISK_MANAGER_MQH #define RISK_MANAGER_MQH #include "DataTypes.mqh" #include #include //+------------------------------------------------------------------+ //| Risk Manager Class | //+------------------------------------------------------------------+ class CRiskManager { private: //--- Configuration RiskManagerConfig m_config; //--- Market info CSymbolInfo m_symbol; MarketConditions m_market_conditions; //--- Risk tracking double m_daily_loss; double m_daily_profit; datetime m_last_reset; double m_current_exposure; double m_max_daily_loss; //--- Kelly Criterion tracking double m_kelly_wins[100]; double m_kelly_losses[100]; int m_kelly_win_count; int m_kelly_loss_count; double m_kelly_optimal; //--- Helper methods double CalculateKellyFraction(); double GetAccountRiskAmount(); double CalculateVolatilityAdjustment(); bool ValidateRiskLimits(double risk_amount); double GetMinStopDistance(); double GetSymbolMinLot(); double GetSymbolMaxLot(); double GetSymbolLotStep(); public: CRiskManager(); ~CRiskManager(); //--- Initialization bool Initialize(const RiskManagerConfig &config); //--- Position sizing double CalculatePositionSize(double stop_distance, double risk_override = 0); double CalculateLotSize(double risk_amount, double stop_distance_price); //--- Risk validation bool ValidateNewPosition(double lot_size, double stop_distance); bool ValidateAndAdjustRisk(ManagedTrade &trade); bool CheckDailyDrawdown(); bool CheckMaxExposure(); //--- External trade management void EnforceRiskRules(ManagedTrade &trade, double default_risk_percent, bool close_excessive); double CalculateTradeRisk(const ManagedTrade &trade); //--- Risk metrics double GetCurrentExposure(); double GetDailyPnL(); ENUM_RISK_LEVEL GetCurrentRiskLevel(); double GetRiskCapacity(); //--- Updates void UpdateMarketConditions(const MarketConditions &conditions); void UpdateKellyData(double profit); void ResetDailyMetrics(); //--- Getters double GetOptimalKelly() { return m_kelly_optimal; } double GetMaxDailyLoss() { return m_max_daily_loss; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CRiskManager::CRiskManager() { m_daily_loss = 0; m_daily_profit = 0; m_last_reset = 0; m_current_exposure = 0; m_max_daily_loss = 0; m_kelly_win_count = 0; m_kelly_loss_count = 0; m_kelly_optimal = 0; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CRiskManager::~CRiskManager() { } //+------------------------------------------------------------------+ //| Initialize risk manager | //+------------------------------------------------------------------+ bool CRiskManager::Initialize(const RiskManagerConfig &config) { m_config = config; //--- Initialize symbol info if(!m_symbol.Name(_Symbol)) { Print("RiskManager: Failed to initialize symbol info"); return false; } //--- Calculate max daily loss in account currency double balance = AccountInfoDouble(ACCOUNT_BALANCE); m_max_daily_loss = balance * (m_config.max_daily_dd / 100.0); //--- Reset daily metrics ResetDailyMetrics(); Print("RiskManager initialized: Risk=", m_config.risk_percent, "%, MaxDD=", m_config.max_daily_dd, "%"); return true; } //+---------------------------------------------------------------------------------+ //| Calculate position size based on stop distance | //Fix 2: Position Sizing Issues (RiskManager.mqh) | //Fix 3: Improve Lot Size Calculation (RiskManager.mqh) | //Let's add better error handling and logging to the position sizing calculation | //Let's also improve the lot size calculation to handle edge cases better | | //+---------------------------------------------------------------------------------+ double CRiskManager::CalculatePositionSize(double stop_distance, double risk_override) { //--- Check daily drawdown limit if(!CheckDailyDrawdown()) { Print("RiskManager: Daily drawdown limit reached"); return 0; } //--- Validate stop distance if(stop_distance <= 0) { Print("RiskManager: Invalid stop distance: ", stop_distance); Print("RiskManager: Stop distance must be greater than 0"); return 0; } //--- Get risk amount double risk_percent = (risk_override > 0) ? risk_override : m_config.risk_percent; double account_balance = GetAccountRiskAmount(); double risk_amount = account_balance * (risk_percent / 100.0); Print("RiskManager: Balance=", account_balance, ", Risk%=", risk_percent, ", RiskAmount=", risk_amount, ", StopDistance=", stop_distance/_Point, " points"); //--- Apply Kelly Criterion if enabled if(m_config.use_kelly) { double kelly_fraction = CalculateKellyFraction(); risk_amount *= kelly_fraction; Print("RiskManager: Kelly fraction = ", kelly_fraction); } //--- Apply volatility adjustment if(m_config.sizing_mode == PS_ATR_BASED) { double vol_adjustment = CalculateVolatilityAdjustment(); risk_amount *= vol_adjustment; Print("RiskManager: Volatility adjustment = ", vol_adjustment); } //--- Validate risk limits if(!ValidateRiskLimits(risk_amount)) { risk_amount = account_balance * (m_config.max_risk_percent / 100.0); Print("RiskManager: Risk amount adjusted to max allowed: ", risk_amount); } //--- Calculate lot size double lot_size = CalculateLotSize(risk_amount, stop_distance); Print("RiskManager: Raw lot size calculated: ", lot_size); //--- Safety check for unreasonable lot sizes if(lot_size > 1000.0) { Print("RiskManager: ERROR - Calculated lot size unreasonably high: ", lot_size); Print("RiskManager: Check your broker's contract specifications"); return 0; } //--- Get symbol limits double min_lot = GetSymbolMinLot(); double max_lot = GetSymbolMaxLot(); double lot_step = GetSymbolLotStep(); Print("RiskManager: Symbol limits - MinLot=", min_lot, ", MaxLot=", max_lot, ", LotStep=", lot_step); //--- Check if calculated lot is too small if(lot_size < min_lot) { Print("RiskManager: Calculated lot ", lot_size, " below minimum ", min_lot); // Calculate required balance for minimum lot double required_balance = (min_lot * stop_distance * m_symbol.TickValue() * m_symbol.Point()) / (risk_percent / 100.0); Print("RiskManager: Required balance for min lot: ", required_balance); // If account is too small, return 0 if(account_balance < required_balance * 0.5) // 50% safety margin { Print("RiskManager: Account too small for minimum position size"); return 0; } // Otherwise use minimum lot lot_size = min_lot; } //--- Apply maximum limit from configuration lot_size = MathMin(lot_size, m_config.max_lot_size); //--- Apply symbol maximum limit lot_size = MathMin(lot_size, max_lot); //--- Round to lot step lot_size = MathFloor(lot_size / lot_step) * lot_step; lot_size = NormalizeDouble(lot_size, 2); //--- Final check against configured limits if(lot_size < m_config.min_lot_size) { Print("RiskManager: Lot size ", lot_size, " below configured minimum ", m_config.min_lot_size); return 0; } //--- ✅ FIXED: Return the calculated lot size Print("RiskManager: Final lot size: ", lot_size); return lot_size; } //+------------------------------------------------------------------+ //| Calculate lot size from risk amount and stop distance | //+------------------------------------------------------------------+ double CRiskManager::CalculateLotSize(double risk_amount, double stop_distance_price) { if(stop_distance_price <= 0) return 0; // Method 1: Using tick calculations (more accurate) double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double point_size = SymbolInfoDouble(_Symbol, SYMBOL_POINT); // Convert stop distance from price units to points double stop_points = stop_distance_price / point_size; // Convert points to ticks double point_to_tick_ratio = point_size / tick_size; double stop_ticks = stop_points * point_to_tick_ratio; // Calculate the monetary value of the stop loss for 1 lot double stop_value_per_lot = stop_ticks * tick_value; // Calculate required lot size double lot_size = risk_amount / stop_value_per_lot; // Method 2: Simplified calculation (for verification) // This uses contract specifications directly double contract_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); string profit_currency = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT); // For forex pairs where profit currency is the quote currency double lot_size_simple = 0; if(profit_currency == AccountInfoString(ACCOUNT_CURRENCY)) { // Direct calculation for pairs like EURUSD (USD account) lot_size_simple = risk_amount / (stop_distance_price * contract_size); } else { // For cross pairs, we'd need currency conversion // For now, use the tick-based calculation lot_size_simple = lot_size; } // Debug output Print("CalculateLotSize Debug:"); Print(" Risk Amount: ", risk_amount); Print(" Stop Distance (price): ", stop_distance_price); Print(" Stop Distance (points): ", stop_points); Print(" Point Size: ", point_size); Print(" Tick Size: ", tick_size); Print(" Tick Value: ", tick_value); Print(" Contract Size: ", contract_size); Print(" Stop Ticks: ", stop_ticks); Print(" Stop Value per Lot: ", stop_value_per_lot); Print(" Calculated Lot Size (tick method): ", lot_size); Print(" Calculated Lot Size (simple method): ", lot_size_simple); // Use the tick-based calculation as it's more universal return lot_size; } //+------------------------------------------------------------------+ //| Validate new position against risk limits | //+------------------------------------------------------------------+ bool CRiskManager::ValidateNewPosition(double lot_size, double stop_distance) { //--- Check daily drawdown if(!CheckDailyDrawdown()) return false; //--- Calculate position risk using the same method as CalculateLotSize double tick_value = m_symbol.TickValue(); double tick_size = m_symbol.TickSize(); double stop_ticks = stop_distance / tick_size; double position_risk = lot_size * stop_ticks * tick_value; //--- Check against max risk percent double account_balance = AccountInfoDouble(ACCOUNT_BALANCE); double risk_percent = (position_risk / account_balance) * 100.0; if(risk_percent > m_config.max_risk_percent) { Print("RiskManager: Position risk ", risk_percent, "% exceeds maximum ", m_config.max_risk_percent, "%"); return false; } //--- Check total exposure double new_exposure = m_current_exposure + risk_percent; if(new_exposure > m_config.max_exposure) { Print("RiskManager: Total exposure would be ", new_exposure, "% exceeding limit ", m_config.max_exposure, "%"); return false; } return true; } //+------------------------------------------------------------------+ //| Validate and adjust risk for existing trade | //+------------------------------------------------------------------+ bool CRiskManager::ValidateAndAdjustRisk(ManagedTrade &trade) { //--- Calculate current risk trade.risk_amount = CalculateTradeRisk(trade); trade.risk_percent = (trade.risk_amount / AccountInfoDouble(ACCOUNT_BALANCE)) * 100.0; //--- Calculate R-multiple if(trade.risk_amount > 0) { double total_pnl = trade.profit + trade.commission + trade.swap; trade.r_multiple = total_pnl / trade.risk_amount; } //--- Update MAE/MFE double current_pnl = trade.profit + trade.commission + trade.swap; if(current_pnl > trade.mfe) trade.mfe = current_pnl; if(current_pnl < trade.mae) trade.mae = current_pnl; return true; } //+------------------------------------------------------------------+ //| Enforce risk rules on external trades | //+------------------------------------------------------------------+ void CRiskManager::EnforceRiskRules(ManagedTrade &trade, double default_risk_percent, bool close_excessive) { CTrade trade_ops; //--- Calculate actual risk double actual_risk = CalculateTradeRisk(trade); double risk_percent = (actual_risk / AccountInfoDouble(ACCOUNT_BALANCE)) * 100.0; //--- Check if risk exceeds maximum if(risk_percent > m_config.max_risk_percent) { Print("RiskManager: External trade #", trade.ticket, " risk ", risk_percent, "% exceeds max ", m_config.max_risk_percent, "%"); if(close_excessive) { //--- Close the trade trade_ops.PositionClose(trade.ticket); Print("RiskManager: Closed excessive risk trade #", trade.ticket); return; } } //--- If no stop loss, calculate and set one if(trade.sl == 0) { double atr = m_market_conditions.volatility; double stop_distance = atr * 2.0; // 2 ATR default if(trade.type == POSITION_TYPE_BUY) trade.sl = trade.open_price - stop_distance; else trade.sl = trade.open_price + stop_distance; //--- Ensure minimum stop distance double min_stop = GetMinStopDistance(); double current_price = (trade.type == POSITION_TYPE_BUY) ? m_symbol.Bid() : m_symbol.Ask(); double actual_distance = MathAbs(current_price - trade.sl); if(actual_distance < min_stop) { if(trade.type == POSITION_TYPE_BUY) trade.sl = current_price - min_stop; else trade.sl = current_price + min_stop; } //--- Modify position trade_ops.PositionModify(trade.ticket, trade.sl, trade.tp); Print("RiskManager: Set stop loss for trade #", trade.ticket, " at ", trade.sl); } //--- Update trade risk values trade.risk_amount = actual_risk; trade.risk_percent = risk_percent; } //+------------------------------------------------------------------+ //| Calculate trade risk amount | //+------------------------------------------------------------------+ double CRiskManager::CalculateTradeRisk(const ManagedTrade &trade) { if(trade.sl == 0) return 0; double stop_distance = MathAbs(trade.open_price - trade.sl); double tick_value = m_symbol.TickValue(); double tick_size = m_symbol.TickSize(); // Convert stop distance to ticks double stop_ticks = stop_distance / tick_size; // Calculate risk amount double risk_amount = trade.volume * stop_ticks * tick_value; return risk_amount; } //+------------------------------------------------------------------+ //| Check daily drawdown limit | //+------------------------------------------------------------------+ bool CRiskManager::CheckDailyDrawdown() { double daily_pnl = GetDailyPnL(); if(daily_pnl < -m_max_daily_loss) { Print("RiskManager: Daily loss limit reached: ", daily_pnl); return false; } return true; } //+------------------------------------------------------------------+ //| Check maximum exposure limit | //+------------------------------------------------------------------+ bool CRiskManager::CheckMaxExposure() { return (m_current_exposure <= m_config.max_exposure); } //+------------------------------------------------------------------+ //| Get current total exposure | //+------------------------------------------------------------------+ double CRiskManager::GetCurrentExposure() { m_current_exposure = 0; int total = PositionsTotal(); for(int i = 0; i < total; i++) { // Use PositionGetSymbol instead of PositionSelectByIndex if(PositionGetSymbol(i) == _Symbol) { if(PositionSelect(_Symbol)) { ManagedTrade temp_trade; temp_trade.ticket = PositionGetInteger(POSITION_TICKET); temp_trade.open_price = PositionGetDouble(POSITION_PRICE_OPEN); temp_trade.sl = PositionGetDouble(POSITION_SL); temp_trade.volume = PositionGetDouble(POSITION_VOLUME); temp_trade.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); double risk = CalculateTradeRisk(temp_trade); m_current_exposure += (risk / AccountInfoDouble(ACCOUNT_BALANCE)) * 100.0; } } } return m_current_exposure; } //+------------------------------------------------------------------+ //| Get daily P&L | //+------------------------------------------------------------------+ double CRiskManager::GetDailyPnL() { //--- Check for new day MqlDateTime current_time; TimeToStruct(TimeCurrent(), current_time); MqlDateTime reset_time; TimeToStruct(m_last_reset, reset_time); if(current_time.day != reset_time.day) { ResetDailyMetrics(); } return m_daily_profit - m_daily_loss; } //+------------------------------------------------------------------+ //| Get current risk level | //+------------------------------------------------------------------+ ENUM_RISK_LEVEL CRiskManager::GetCurrentRiskLevel() { double exposure = GetCurrentExposure(); double daily_loss_percent = (m_daily_loss / AccountInfoDouble(ACCOUNT_BALANCE)) * 100.0; //--- Critical risk if(daily_loss_percent > m_config.max_daily_dd * 0.8 || exposure > m_config.max_exposure * 0.9) return RISK_CRITICAL; //--- Extreme risk if(daily_loss_percent > m_config.max_daily_dd * 0.6 || exposure > m_config.max_exposure * 0.7) return RISK_EXTREME; //--- High risk if(daily_loss_percent > m_config.max_daily_dd * 0.4 || exposure > m_config.max_exposure * 0.5) return RISK_HIGH; //--- Medium risk if(daily_loss_percent > m_config.max_daily_dd * 0.2 || exposure > m_config.max_exposure * 0.3) return RISK_MEDIUM; return RISK_LOW; } //+------------------------------------------------------------------+ //| Get risk capacity (how much more risk can be taken) | //+------------------------------------------------------------------+ double CRiskManager::GetRiskCapacity() { double daily_remaining = m_max_daily_loss - MathAbs(MathMin(GetDailyPnL(), 0)); double exposure_remaining = m_config.max_exposure - GetCurrentExposure(); //--- Return the more restrictive limit double daily_capacity = (daily_remaining / AccountInfoDouble(ACCOUNT_BALANCE)) * 100.0; return MathMin(daily_capacity, exposure_remaining); } //+------------------------------------------------------------------+ //| Update market conditions | //+------------------------------------------------------------------+ void CRiskManager::UpdateMarketConditions(const MarketConditions &conditions) { m_market_conditions = conditions; } //+------------------------------------------------------------------+ //| Update Kelly criterion data | //+------------------------------------------------------------------+ void CRiskManager::UpdateKellyData(double profit) { if(profit > 0) { if(m_kelly_win_count < 100) { m_kelly_wins[m_kelly_win_count] = profit; m_kelly_win_count++; } } else if(profit < 0) { if(m_kelly_loss_count < 100) { m_kelly_losses[m_kelly_loss_count] = MathAbs(profit); m_kelly_loss_count++; } } //--- Recalculate Kelly fraction m_kelly_optimal = CalculateKellyFraction(); } //+------------------------------------------------------------------+ //| Calculate Kelly fraction | //+------------------------------------------------------------------+ double CRiskManager::CalculateKellyFraction() { if(m_kelly_win_count < 10 || m_kelly_loss_count < 10) return m_config.kelly_fraction; // Use default until enough data //--- Calculate win probability double win_prob = (double)m_kelly_win_count / (m_kelly_win_count + m_kelly_loss_count); //--- Calculate average win/loss double avg_win = 0, avg_loss = 0; for(int i = 0; i < m_kelly_win_count; i++) avg_win += m_kelly_wins[i]; avg_win /= m_kelly_win_count; for(int i = 0; i < m_kelly_loss_count; i++) avg_loss += m_kelly_losses[i]; avg_loss /= m_kelly_loss_count; //--- Kelly formula: f = (p*b - q) / b //--- where p = win prob, q = loss prob, b = win/loss ratio double b = avg_win / avg_loss; double q = 1 - win_prob; double kelly = (win_prob * b - q) / b; //--- Apply safety factor kelly *= m_config.kelly_fraction; //--- Ensure reasonable bounds kelly = MathMax(kelly, 0.01); // Minimum 1% kelly = MathMin(kelly, 0.25); // Maximum 25% return kelly; } //+------------------------------------------------------------------+ //| Calculate volatility adjustment factor | //+------------------------------------------------------------------+ double CRiskManager::CalculateVolatilityAdjustment() { if(m_market_conditions.volatility <= 0) return 1.0; //--- Get historical average volatility (simplified) double avg_volatility = iATR(_Symbol, PERIOD_D1, 20); if(avg_volatility <= 0) return 1.0; //--- Reduce size in high volatility, increase in low volatility double vol_ratio = avg_volatility / m_market_conditions.volatility; //--- Apply bounds vol_ratio = MathMax(vol_ratio, 0.5); // Minimum 50% size vol_ratio = MathMin(vol_ratio, 1.5); // Maximum 150% size return vol_ratio; } //+------------------------------------------------------------------+ //| Get account risk amount (considers balance/equity) | //+------------------------------------------------------------------+ double CRiskManager::GetAccountRiskAmount() { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Use the lower of balance or equity for safety return MathMin(balance, equity); } //+------------------------------------------------------------------+ //| Validate risk limits | //+------------------------------------------------------------------+ bool CRiskManager::ValidateRiskLimits(double risk_amount) { double account_value = GetAccountRiskAmount(); double risk_percent = (risk_amount / account_value) * 100.0; return (risk_percent <= m_config.max_risk_percent); } //+------------------------------------------------------------------+ //| Get minimum stop distance | //+------------------------------------------------------------------+ double CRiskManager::GetMinStopDistance() { int stops_level = (int)m_symbol.StopsLevel(); double min_stop = stops_level * m_symbol.Point(); //--- Add buffer for safety return min_stop * 1.1; } //+------------------------------------------------------------------+ //| Get symbol minimum lot | //+------------------------------------------------------------------+ double CRiskManager::GetSymbolMinLot() { return m_symbol.LotsMin(); } //+------------------------------------------------------------------+ //| Get symbol maximum lot | //+------------------------------------------------------------------+ double CRiskManager::GetSymbolMaxLot() { return m_symbol.LotsMax(); } //+------------------------------------------------------------------+ //| Get symbol lot step | //+------------------------------------------------------------------+ double CRiskManager::GetSymbolLotStep() { return m_symbol.LotsStep(); } //+------------------------------------------------------------------+ //| Reset daily metrics | //+------------------------------------------------------------------+ void CRiskManager::ResetDailyMetrics() { m_daily_loss = 0; m_daily_profit = 0; m_last_reset = TimeCurrent(); //--- Recalculate max daily loss based on current balance double balance = AccountInfoDouble(ACCOUNT_BALANCE); m_max_daily_loss = balance * (m_config.max_daily_dd / 100.0); } #endif // RISK_MANAGER_MQH //+------------------------------------------------------------------+