//+------------------------------------------------------------------+ //| RiskManager.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define MAX_PERCENT 10 #include "../Configs/EAConfig.mqh" #include "../Market/DataManager.mqh" #include "../Events/Events/SignalEvent.mqh" #include "../Events/Events/TradingEvents.mqh" #include //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class RiskManager final: public IEventListener { private: EventBus* m_bus; EAConfig m_eaConfig; CPositions m_pos; int m_inFlight; double MoneyManagement(string pSymbol, double pFixedVol, double pPercent, int pStopPoints); double VerifyVolume(string pSymbol, double pVolume); bool FindSymbolConfig(string symbol, SymbolConfig &config) { for(int i = 0; i < m_eaConfig.tfConfigs.Size(); i++) { SymbolConfig cfg = m_eaConfig.tfConfigs.At(i); if(cfg.symbol == symbol) { config = cfg; return true; } } return false; } bool IsDirectionAllowed(int direction, const SymbolConfig &config) { if(direction == 1) return config.allowBuy; if(direction == -1) return config.allowSell; return false; } bool CheckPositionLimit(string symbol) { int actualPositions = m_pos.TotalPositions(m_eaConfig.magicNumber, symbol); int totalExposure = actualPositions + m_inFlight; /* Print("Risk: ", symbol, " actual=", actualPositions, " inFlight=", m_inFlight, " total=", totalExposure, "/", m_eaConfig.maxOpenPositions); */ if(totalExposure >= m_eaConfig.maxOpenPositions) { m_bus.Push(new RiskRejectedEvent(symbol, "Max positions reached")); return false; } return true; } bool CheckPendingLimit(string symbol) { int actualPositions = m_pos.TotalPending(m_eaConfig.magicNumber, symbol); int totalExposure = actualPositions + m_inFlight; /* Print("Risk: ", symbol, " actual=", actualPositions, " inFlight=", m_inFlight, " total=", totalExposure, "/", m_eaConfig.maxOpenPositions);*/ if(totalExposure >= m_eaConfig.maxOpenPositions) { m_bus.Push(new RiskRejectedEvent(symbol, "Too many pending orders")); return false; } return true; } double CalculateSL(const SignalEvent* signal, const SymbolConfig &config) { if(config.rmConfigs.StopLoss <= 0) return 0.0; double point = SymbolInfoDouble(signal.GetSymbolName(), SYMBOL_POINT); if(point <= 0) return 0.0; if(signal.GetDirection() == 1) return signal.GetPrice() - config.rmConfigs.StopLoss * point; if(signal.GetDirection() == -1) return signal.GetPrice() + config.rmConfigs.StopLoss * point; return 0.0; } double CalculateTP(const SignalEvent* signal, const SymbolConfig &config) { if(config.rmConfigs.TakeProfit <= 0) return 0.0; double point = SymbolInfoDouble(signal.GetSymbolName(), SYMBOL_POINT); if(point <= 0) return 0.0; if(signal.GetDirection() == 1) return signal.GetPrice() + config.rmConfigs.TakeProfit * point; if(signal.GetDirection() == -1) return signal.GetPrice() - config.rmConfigs.TakeProfit * point; return 0.0; } bool ValidatePrices(const SignalEvent* signal, double sl, double tp) { string symbol = signal.GetSymbolName(); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if(point <= 0) { m_bus.Push(new RiskRejectedEvent(symbol, "Invalid symbol point")); return false; } if(signal.GetPrice() <= 0) { m_bus.Push(new RiskRejectedEvent(symbol, "Invalid entry price")); return false; } return true; } bool m_isLaunching; public: RiskManager() : m_bus(NULL), m_isLaunching(true), m_inFlight(0) {} ~RiskManager() {} void SetLaunching(bool launching) { m_isLaunching = launching; } void Initialize(EventBus* bus, const EAConfig &config) { m_bus = bus; m_eaConfig = config; } virtual void OnEvent(AbstractEvent* event) override { if(m_isLaunching) return; if(event.GetType() == POSITION_OPENED) { if(m_inFlight > 0) m_inFlight--; return; // nothing else to do } // --- SIGNAL processing --- if(event.GetType() == SIGNAL) { // Stale check – you may want to reduce this window or remove it temporarily if(event.GetTimestamp() < TimeCurrent() - 5000) { Print("RiskManager: Rejecting stale SIGNAL"); return; } SignalEvent* signal = (SignalEvent*)event; string symbol = signal.GetSymbolName(); int direction = signal.GetDirection(); ENUM_ORDER_TYPE orderType = signal.GetOrderType(); double entryPrice = signal.GetPrice(); SymbolConfig symCfg; if(!FindSymbolConfig(symbol, symCfg)) { m_bus.Push(new RiskRejectedEvent(symbol, "Symbol configuration not found")); return; } if(signal.GetTimeFrame() != symCfg.tradeTimeframe) { m_bus.Push(new RiskRejectedEvent(symbol, "Trading TimeFrame not allowed")); return; } if(!IsDirectionAllowed(direction, symCfg)) { m_bus.Push(new RiskRejectedEvent(symbol, "Trading direction not allowed")); return; } bool isMarketOrder = (orderType == ORDER_TYPE_BUY || orderType == ORDER_TYPE_SELL); bool isPendingOrder = (orderType == ORDER_TYPE_BUY_LIMIT || orderType == ORDER_TYPE_BUY_STOP || orderType == ORDER_TYPE_BUY_STOP_LIMIT || orderType == ORDER_TYPE_SELL_LIMIT || orderType == ORDER_TYPE_SELL_STOP || orderType == ORDER_TYPE_SELL_STOP_LIMIT); if(!isMarketOrder && !isPendingOrder) { m_bus.Push(new RiskRejectedEvent(symbol, "Unsupported order type")); return; } if(isPendingOrder && entryPrice <= 0) { m_bus.Push(new RiskRejectedEvent(symbol, "Invalid pending order price")); return; } // *** Check limits (now using actual + pending) if(!CheckPositionLimit(symbol)) return; if(isPendingOrder && !CheckPendingLimit(symbol)) return; double sl = CalculateSL(signal, symCfg); double tp = CalculateTP(signal, symCfg); if(!ValidatePrices(signal, sl, tp)) return; double lotSize = MoneyManagement(symbol, symCfg.rmConfigs.FixedVolume, m_eaConfig.RiskPercent, symCfg.rmConfigs.StopLoss); lotSize = VerifyVolume(symbol, lotSize); if(lotSize <= 0) { m_bus.Push(new RiskRejectedEvent(symbol, "Invalid trade volume")); return; } if(isMarketOrder) { m_bus.Push(new ExecuteMarketOrderEvent(symbol, entryPrice, direction, lotSize, sl, tp, m_eaConfig.comment)); m_inFlight++; return; } if(isPendingOrder) { m_bus.Push(new ExecutePendingOrderEvent(symbol, orderType, lotSize, entryPrice, 0, sl, tp, 0, m_eaConfig.comment + "_Pending")); m_inFlight++; } } } }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double RiskManager::MoneyManagement(string pSymbol, double pFixedVol, double pPercent, int pStopPoints) { double tradeSize; if(pPercent > 0 && pStopPoints > 0) { if(pPercent > MAX_PERCENT) { pPercent = MAX_PERCENT; Print("Warning: Risk Percent exceeds the limit: ", MAX_PERCENT); } double margin = AccountInfoDouble(ACCOUNT_BALANCE) * (pPercent / 100); double tickSize = SymbolInfoDouble(pSymbol, SYMBOL_TRADE_TICK_VALUE); tradeSize = (margin / pStopPoints) / tickSize; } else tradeSize = pFixedVol; return VerifyVolume(pSymbol, tradeSize); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double RiskManager::VerifyVolume(string pSymbol, double pVolume) { double minVolume = SymbolInfoDouble(pSymbol, SYMBOL_VOLUME_MIN); double maxVolume = SymbolInfoDouble(pSymbol, SYMBOL_VOLUME_MAX); double stepVolume = SymbolInfoDouble(pSymbol, SYMBOL_VOLUME_STEP); double tradeSize; if(pVolume < minVolume) tradeSize = minVolume; else if(pVolume > maxVolume) tradeSize = maxVolume; else tradeSize = MathRound(pVolume / stepVolume) * stepVolume; if(stepVolume >= 0.1) tradeSize = NormalizeDouble(tradeSize, 1); else tradeSize = NormalizeDouble(tradeSize, 2); return tradeSize; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+