296 lines
10 KiB
MQL5
296 lines
10 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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 <Mql5Book\TradeHedge.mqh>
| |||
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| |
| |||
//+------------------------------------------------------------------+
| |||
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;
| |||
}
| |||
//+------------------------------------------------------------------+
| |||
//+------------------------------------------------------------------+
|