//+------------------------------------------------------------------+ //| RiskManager.mqh - Risk and position sizing management | //| Calculates trade entry levels, stop loss, take profit and lot sizes| //+------------------------------------------------------------------+ #ifndef __RISKMANAGER_MQH__ #define __RISKMANAGER_MQH__ #include "Config.mqh" #include "Logger.mqh" #include "MarketData.mqh" #include "Utilities.mqh" #include "Signal.mqh" class CRiskManager { private: CMarketData *mp_market_data; CLogger *mp_logger; public: CRiskManager(CMarketData *market_data, CLogger *logger) { mp_market_data = market_data; mp_logger = logger; } double CalculateLotByRisk(double risk_distance) { string symbol = mp_market_data.GetSymbol(); if(!IsSymbolValid(symbol)) return 0.0; if(risk_distance <= 0.0) { if(mp_logger) mp_logger.Error("Risk distance must be positive to calculate lot size"); return 0.0; } double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); if(tick_size <= 0.0 || tick_value <= 0.0) { if(mp_logger) mp_logger.Error("Symbol tick size or tick value invalid for lot calculation"); return 0.0; } double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); if(account_equity <= 0.0) { if(mp_logger) mp_logger.Error("Account equity is invalid for lot calculation"); return 0.0; } double effective_risk_percent = GetEffectiveRiskPercent(); if(effective_risk_percent <= 0.0) { if(mp_logger) mp_logger.Error("Effective risk percent is invalid"); return 0.0; } double risk_amount = account_equity * (effective_risk_percent / 100.0); // Loss per single lot = (risk_distance / tick_size) * tick_value double loss_per_lot = (risk_distance / tick_size) * tick_value; if(loss_per_lot <= 0.0) { if(mp_logger) mp_logger.Error("Calculated monetary loss per lot is non-positive"); return 0.0; } double raw_lot = risk_amount / loss_per_lot; double normalized_lot = CUtilities::NormalizeLot(symbol, raw_lot); if(normalized_lot <= 0.0) { if(mp_logger) mp_logger.Warning(StringFormat("No valid lot size: raw=%.6f normalized=%.6f (min/max/step constraints)", raw_lot, normalized_lot)); return 0.0; } if(mp_logger && g_debug_mode) { mp_logger.Info(StringFormat("Tick size=%.8f tick value=%.8f risk%%=%.2f rawLot=%.6f finalLot=%.6f", tick_size, tick_value, effective_risk_percent, raw_lot, normalized_lot)); } return normalized_lot; } bool ValidateTradeSetup(const TradeSetup &setup, double &lot) { string symbol = mp_market_data.GetSymbol(); if(!IsSymbolValid(symbol)) { if(mp_logger) mp_logger.Error("Invalid symbol for trade setup validation"); return false; } if(setup.signal != SIGNAL_BUY && setup.signal != SIGNAL_SELL) { if(mp_logger && g_debug_mode) mp_logger.Info("Trade setup rejected: no valid signal"); return false; } double execution_price = (setup.signal == SIGNAL_BUY) ? mp_market_data.GetAsk() : mp_market_data.GetBid(); double setup_entry_price = setup.entryPrice; double setup_diff = 0.0; if(setup_entry_price > 0.0) { setup_diff = MathAbs(setup_entry_price - execution_price); } if(setup.stopLoss <= 0.0 || setup.takeProfit <= 0.0 || setup.riskDistance <= 0.0) { if(mp_logger) mp_logger.Warning(StringFormat("Trade setup rejected: valid initial SL and TP are required (TP-less trades need a future explicit allowNoTakeProfit field). setupEntry=%.5f execEntry=%.5f sl=%.5f tp=%.5f risk=%.5f", setup_entry_price, execution_price, setup.stopLoss, setup.takeProfit, setup.riskDistance)); return false; } bool valid_side = true; if(setup.signal == SIGNAL_BUY) { valid_side = (setup.stopLoss < execution_price && setup.takeProfit > execution_price); } else if(setup.signal == SIGNAL_SELL) { valid_side = (setup.stopLoss > execution_price && setup.takeProfit < execution_price); } if(!valid_side) { if(mp_logger) mp_logger.Warning(StringFormat("Trade setup rejected: SL/TP invalid for current market entry price (setupEntry=%.5f execEntry=%.5f sl=%.5f tp=%.5f)", setup_entry_price, execution_price, setup.stopLoss, setup.takeProfit)); return false; } // Broker minimal stop distance relative to current market execution price double point = CUtilities::GetPoint(symbol); long min_stop_points = (long)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); if(min_stop_points < 0) min_stop_points = 0; double min_stop_distance = min_stop_points * point; double actual_risk_distance = MathAbs(execution_price - setup.stopLoss); if(actual_risk_distance <= 0.0) { if(mp_logger) mp_logger.Warning(StringFormat("Trade setup rejected: non-positive risk distance after using execution price (execEntry=%.5f sl=%.5f)", execution_price, setup.stopLoss)); return false; } if(actual_risk_distance < min_stop_distance) { if(mp_logger) mp_logger.Warning(StringFormat("Trade setup rejected: SL too close to current entry price (distance %.5f < broker stop level %.5f)", actual_risk_distance, min_stop_distance)); return false; } if(mp_logger && g_debug_mode) { mp_logger.Info(StringFormat("Trade setup details: setupEntry=%.5f execEntry=%.5f diff=%.5f SL=%.5f TP=%.5f setupRisk=%.5f execRisk=%.5f reason=%s", setup_entry_price, execution_price, setup_diff, setup.stopLoss, setup.takeProfit, setup.riskDistance, actual_risk_distance, setup.reason)); } // Lot sizing: use actual execution entry-to-SL distance in price units lot = CalculateLotByRisk(actual_risk_distance); if(lot <= 0.0) { if(mp_logger) mp_logger.Warning("Trade setup rejected: lot sizing failed or below broker minimum after execution price adjustment"); return false; } if(mp_logger && g_debug_mode) { double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); mp_logger.Info(StringFormat("Trade setup validated: signal=%s setupEntry=%.5f execEntry=%.5f diff=%.5f SL=%.5f TP=%.5f execRisk=%.5f tickSize=%.8f tickValue=%.8f riskUsed=%.2f finalLot=%.6f reason=%s", setup.signal == SIGNAL_BUY ? "BUY" : "SELL", setup_entry_price, execution_price, setup_diff, setup.stopLoss, setup.takeProfit, actual_risk_distance, tick_size, tick_value, GetEffectiveRiskPercent(), lot, setup.reason)); } return true; return true; } bool IsSpreadAcceptable() { int max_spread = g_max_spread_points; string symbol = mp_market_data.GetSymbol(); if(StringFind(symbol, "XAU") >= 0 || StringFind(symbol, "GOLD") >= 0) { max_spread = MathMax(max_spread, 200); } return mp_market_data.IsSpreadAcceptable(max_spread); } bool IsTradingHourValid() { if(!g_use_trading_hours) return true; MqlDateTime time_struct; TimeToStruct(TimeCurrent(), time_struct); int current_hour = time_struct.hour; if(g_trade_start_hour <= g_trade_end_hour) { if(current_hour < g_trade_start_hour || current_hour >= g_trade_end_hour) { if(mp_logger && g_debug_mode) mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)", current_hour, g_trade_start_hour, g_trade_end_hour)); return false; } } else { if(current_hour < g_trade_start_hour && current_hour >= g_trade_end_hour) { if(mp_logger && g_debug_mode) mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)", current_hour, g_trade_start_hour, g_trade_end_hour)); return false; } } return true; } CMarketData* GetMarketData() { return mp_market_data; } private: double GetEffectiveRiskPercent() { double risk_percent = g_risk_percent; if(risk_percent > g_max_risk_percent) { if(mp_logger) mp_logger.Warning(StringFormat("RiskPercent capped from %.2f%% to %.2f%% (MaxRiskPercent)", risk_percent, g_max_risk_percent)); risk_percent = g_max_risk_percent; } return risk_percent; } double GetAtrValue(int shift) { string symbol = mp_market_data.GetSymbol(); int atr_handle = iATR(symbol, g_strategy_entry_timeframe, g_atr_period); if(atr_handle == INVALID_HANDLE) return 0.0; double atr_value[]; ArraySetAsSeries(atr_value, true); ArrayResize(atr_value, 1); if(CopyBuffer(atr_handle, 0, shift, 1, atr_value) <= 0) { IndicatorRelease(atr_handle); return 0.0; } double atr = atr_value[0]; IndicatorRelease(atr_handle); return atr; } double ValidateLotSize(double lot) { string symbol = mp_market_data.GetSymbol(); double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); if(min_lot <= 0.0 || max_lot <= 0.0 || lot_step <= 0.0 || max_lot < min_lot) { if(mp_logger) mp_logger.Error("Invalid volume step or limits for symbol"); return 0.0; } if(lot <= 0.0) return 0.0; double normalized = MathFloor(lot / lot_step) * lot_step; if(normalized < min_lot || normalized > max_lot) { if(mp_logger && g_debug_mode) mp_logger.Warning(StringFormat("Normalized lot %.2f outside allowed range [%.2f, %.2f]", normalized, min_lot, max_lot)); return 0.0; } normalized = NormalizeDouble(normalized, 2); if(mp_logger && g_debug_mode) mp_logger.Info(StringFormat("Lot size calculated: %.2f", normalized)); return normalized; } bool IsSymbolValid(const string symbol) { if(!mp_market_data.IsTradingAllowed()) { if(mp_logger) mp_logger.Error("Symbol trading is not allowed"); return false; } if(mp_market_data.GetPoint() <= 0.0) { if(mp_logger) mp_logger.Error("Symbol point size is invalid"); return false; } if(CUtilities::GetContractSize(symbol) <= 0.0) { if(mp_logger) mp_logger.Error("Symbol contract size is invalid"); return false; } return true; } }; #endif //__RISKMANAGER_MQH__