mql5/Experts/Advisors/Modules/RiskManager.mqh

755 lines
54 KiB
MQL5
Raw Permalink Normal View History

<EFBFBD><EFBFBD> //+------------------------------------------------------------------+
//| RiskManager.mqh |
//| Risk Management Module 2.1 |
//| Fixed: Lot size calculation accuracy |
//+------------------------------------------------------------------+
#ifndef RISK_MANAGER_MQH
#define RISK_MANAGER_MQH
#include "DataTypes.mqh"
#include <Trade/Trade.mqh>
#include <Trade/SymbolInfo.mqh>
//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+