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;
| |
}
| |
//+------------------------------------------------------------------+
| |
//+------------------------------------------------------------------+
|