mql5/Experts/Advisors/Modules_optimised/Risk_Manager_Optimised.mqh

789 lines
No EOL
27 KiB
MQL5

//+------------------------------------------------------------------+
//| RiskManager.mqh v2.0 |
//| Optimized Risk Management Module |
//| Enhanced with position correlation analysis |
//+------------------------------------------------------------------+
#ifndef RISK_MANAGER_MQH
#define RISK_MANAGER_MQH
#include "DataTypes.mqh"
#include <Math/Stat/Math.mqh>
//+------------------------------------------------------------------+
//| Risk Manager Class - Optimized Version |
//+------------------------------------------------------------------+
class CRiskManager
{
private:
//--- Configuration
RiskManagerConfig m_config;
//--- Performance tracking for Kelly
double m_win_rate;
double m_avg_win_loss_ratio;
int m_sample_size;
//--- Daily tracking
double m_daily_starting_balance;
double m_daily_loss;
datetime m_last_reset_day;
//--- Exposure tracking
struct ExposureInfo
{
string symbol;
double long_exposure;
double short_exposure;
double net_exposure;
int position_count;
};
ExposureInfo m_exposures[];
//--- Correlation cache
double m_correlation_matrix[][20]; // Max 20 symbols
string m_correlation_symbols[20];
int m_correlation_count;
datetime m_correlation_update_time;
//--- Cache for expensive calculations
double m_cached_total_exposure;
datetime m_cache_time;
static const int CACHE_SECONDS = 5;
//--- Helper methods
double CalculateKellyFraction();
double GetCurrentDrawdown();
void UpdateExposureMap();
double CalculateCorrelationRisk(string symbol, double volume, ENUM_POSITION_TYPE type);
double GetSymbolCorrelation(string symbol1, string symbol2);
void UpdateCorrelationMatrix();
public:
CRiskManager();
~CRiskManager();
//--- Initialization
bool Initialize(const RiskManagerConfig &config);
//--- Position sizing
double CalculatePositionSize(double stop_distance, string symbol = NULL);
double CalculateOptimalF(double win_rate, double avg_win, double avg_loss);
//--- Risk validation
bool ValidateNewPosition(double volume, double stop_distance, string symbol = NULL);
bool CheckDailyDrawdown();
bool CheckTotalExposure();
double GetCurrentExposure(bool use_cache = true);
//--- External trade management
void EnforceRiskRules(ManagedTrade &trade, double default_risk, bool close_excessive);
double CalculateImpliedRisk(const ManagedTrade &trade);
bool ValidateExternalTrade(const ManagedTrade &trade);
//--- Risk metrics
double GetVaR(double confidence_level = 0.95);
double GetMaxLossPerTrade() { return AccountInfoDouble(ACCOUNT_BALANCE) * m_config.max_risk_percent / 100; }
double GetRemainingDailyRisk();
double GetPortfolioHeatmap(ExposureInfo &exposures[]);
//--- Performance tracking
void UpdatePerformanceMetrics(bool won, double profit, double risk);
void ResetDailyMetrics();
//--- Market conditions
void UpdateMarketConditions(const MarketConditions &conditions);
//--- Multi-symbol support
double CalculateSymbolRisk(string symbol, double volume, double stop_distance, ENUM_POSITION_TYPE type);
bool ValidateSymbolExposure(string symbol, double additional_risk);
};
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CRiskManager::CRiskManager()
{
m_win_rate = 0.5; // Default 50%
m_avg_win_loss_ratio = 1.5; // Default 1.5:1
m_sample_size = 0;
m_daily_starting_balance = 0;
m_daily_loss = 0;
m_last_reset_day = 0;
m_cached_total_exposure = 0;
m_cache_time = 0;
m_correlation_count = 0;
m_correlation_update_time = 0;
}
//+------------------------------------------------------------------+
//| Initialize risk manager |
//+------------------------------------------------------------------+
bool CRiskManager::Initialize(const RiskManagerConfig &config)
{
m_config = config;
//--- Validate configuration
if(m_config.risk_percent <= 0 || m_config.risk_percent > 10)
{
Print("RiskManager: Invalid risk percent: ", m_config.risk_percent);
return false;
}
//--- Initialize daily tracking
m_daily_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE);
m_daily_loss = 0;
m_last_reset_day = TimeCurrent();
//--- Initialize correlation matrix
ArrayInitialize(m_correlation_matrix, 0);
Print("RiskManager initialized: Risk=", m_config.risk_percent,
"%, MaxDD=", m_config.max_daily_dd, "%");
return true;
}
//+------------------------------------------------------------------+
//| Calculate position size with Kelly optimization |
//+------------------------------------------------------------------+
double CRiskManager::CalculatePositionSize(double stop_distance, string symbol = NULL)
{
if(symbol == NULL) symbol = _Symbol;
//--- Validate inputs
if(stop_distance <= 0)
{
Print("RiskManager: Invalid stop distance: ", stop_distance);
return 0;
}
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double risk_amount = 0;
//--- Check daily drawdown first
if(!CheckDailyDrawdown())
{
Print("RiskManager: Daily drawdown limit reached");
return 0;
}
//--- Apply position sizing mode
switch(m_config.sizing_mode)
{
case PS_FIXED_LOTS:
return m_config.min_lot_size;
case PS_RISK_PERCENT:
risk_amount = balance * m_config.risk_percent / 100.0;
break;
case PS_CAPITAL_PERCENT:
{
double capital_amount = balance * m_config.risk_percent / 100.0;
// Convert capital to risk based on expected R:R
risk_amount = capital_amount / m_avg_win_loss_ratio;
}
break;
case PS_ATR_BASED:
{
// Dynamic risk based on volatility
double atr_multiplier = MathMin(stop_distance / SymbolInfoDouble(symbol, SYMBOL_POINT) / 100, 2.0);
risk_amount = balance * m_config.risk_percent * atr_multiplier / 100.0;
}
break;
case PS_KELLY:
{
double kelly_fraction = CalculateKellyFraction();
risk_amount = balance * kelly_fraction;
}
break;
}
//--- Apply maximum risk limit
double max_risk = balance * m_config.max_risk_percent / 100.0;
risk_amount = MathMin(risk_amount, max_risk);
//--- Calculate lots based on stop distance
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick_value <= 0 || tick_size <= 0)
{
Print("RiskManager: Invalid tick parameters for ", symbol);
return 0;
}
double lots = risk_amount / ((stop_distance / tick_size) * tick_value);
//--- Normalize to symbol specifications
double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
lots = MathFloor(lots / lot_step) * lot_step;
lots = MathMax(lots, min_lot);
lots = MathMin(lots, max_lot);
//--- Apply configured limits
lots = MathMax(lots, m_config.min_lot_size);
lots = MathMin(lots, m_config.max_lot_size);
//--- Final validation against exposure limits
if(!ValidateNewPosition(lots, stop_distance, symbol))
return 0;
return NormalizeDouble(lots, 2);
}
//+------------------------------------------------------------------+
//| Calculate Kelly fraction with safety margin |
//+------------------------------------------------------------------+
double CRiskManager::CalculateKellyFraction()
{
//--- Need sufficient sample size
if(m_sample_size < 20)
return m_config.risk_percent / 100.0; // Use default until enough data
//--- Kelly formula: f = (p*b - q) / b
//--- Where: p = win probability, q = loss probability, b = win/loss ratio
double p = m_win_rate;
double q = 1 - p;
double b = m_avg_win_loss_ratio;
//--- Calculate raw Kelly fraction
double kelly = (p * b - q) / b;
//--- Apply safety factor (fractional Kelly)
kelly *= m_config.kelly_fraction;
//--- Ensure within reasonable bounds
kelly = MathMax(kelly, 0.01); // Minimum 1%
kelly = MathMin(kelly, m_config.max_risk_percent / 100.0);
return kelly;
}
//+------------------------------------------------------------------+
//| Validate new position against all risk rules |
//+------------------------------------------------------------------+
bool CRiskManager::ValidateNewPosition(double volume, double stop_distance, string symbol)
{
if(symbol == NULL) symbol = _Symbol;
//--- Calculate position risk
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick_size <= 0) return false;
double position_risk = (stop_distance / tick_size) * tick_value * volume;
double risk_percent = (position_risk / AccountInfoDouble(ACCOUNT_BALANCE)) * 100;
//--- Check single position risk
if(risk_percent > m_config.max_risk_percent)
{
Print("RiskManager: Position risk ", DoubleToString(risk_percent, 2),
"% exceeds maximum ", m_config.max_risk_percent, "%");
return false;
}
//--- Check daily drawdown
if(m_daily_loss + position_risk > AccountInfoDouble(ACCOUNT_BALANCE) * m_config.max_daily_dd / 100)
{
Print("RiskManager: Would exceed daily drawdown limit");
return false;
}
//--- Check total exposure
double current_exposure = GetCurrentExposure();
if(current_exposure + risk_percent > m_config.max_exposure)
{
Print("RiskManager: Would exceed maximum exposure limit");
return false;
}
//--- Check correlation risk
double correlation_risk = CalculateCorrelationRisk(symbol, volume, POSITION_TYPE_BUY);
if(correlation_risk > m_config.max_risk_percent)
{
Print("RiskManager: High correlation risk detected: ", DoubleToString(correlation_risk, 2), "%");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Get current total exposure with caching |
//+------------------------------------------------------------------+
double CRiskManager::GetCurrentExposure(bool use_cache)
{
//--- Check cache
if(use_cache && TimeCurrent() - m_cache_time < CACHE_SECONDS)
return m_cached_total_exposure;
//--- Update exposure map
UpdateExposureMap();
double total_exposure = 0;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
//--- Calculate total exposure across all symbols
for(int i = 0; i < ArraySize(m_exposures); i++)
{
total_exposure += MathMax(m_exposures[i].long_exposure, m_exposures[i].short_exposure);
}
//--- Update cache
m_cached_total_exposure = (balance > 0) ? (total_exposure / balance * 100) : 0;
m_cache_time = TimeCurrent();
return m_cached_total_exposure;
}
//+------------------------------------------------------------------+
//| Update exposure map for all symbols |
//+------------------------------------------------------------------+
void CRiskManager::UpdateExposureMap()
{
//--- Clear existing map
ArrayResize(m_exposures, 0);
//--- Temporary map for collecting exposures
ExposureInfo temp_exposures[];
int exposure_count = 0;
//--- Scan all positions
int total = PositionsTotal();
for(int i = 0; i < total; i++)
{
if(PositionSelectByTicket(PositionGetTicket(i)))
{
string symbol = PositionGetString(POSITION_SYMBOL);
double volume = PositionGetDouble(POSITION_VOLUME);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double sl = PositionGetDouble(POSITION_SL);
double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
//--- Calculate position risk
double risk = 0;
if(sl > 0)
{
double stop_distance = (type == POSITION_TYPE_BUY) ?
(open_price - sl) : (sl - open_price);
if(stop_distance > 0)
{
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick_size > 0)
risk = (stop_distance / tick_size) * tick_value * volume;
}
}
else
{
// Use default risk for positions without stop loss
risk = AccountInfoDouble(ACCOUNT_BALANCE) * m_config.risk_percent / 100;
}
//--- Find or create symbol entry
int idx = -1;
for(int j = 0; j < exposure_count; j++)
{
if(temp_exposures[j].symbol == symbol)
{
idx = j;
break;
}
}
if(idx < 0)
{
ArrayResize(temp_exposures, exposure_count + 1);
idx = exposure_count;
temp_exposures[idx].symbol = symbol;
temp_exposures[idx].long_exposure = 0;
temp_exposures[idx].short_exposure = 0;
temp_exposures[idx].position_count = 0;
exposure_count++;
}
//--- Update exposure
if(type == POSITION_TYPE_BUY)
temp_exposures[idx].long_exposure += risk;
else
temp_exposures[idx].short_exposure += risk;
temp_exposures[idx].position_count++;
}
}
//--- Copy to member array
ArrayResize(m_exposures, exposure_count);
for(int i = 0; i < exposure_count; i++)
{
m_exposures[i] = temp_exposures[i];
m_exposures[i].net_exposure = m_exposures[i].long_exposure - m_exposures[i].short_exposure;
}
}
//+------------------------------------------------------------------+
//| Calculate correlation risk for new position |
//+------------------------------------------------------------------+
double CRiskManager::CalculateCorrelationRisk(string symbol, double volume, ENUM_POSITION_TYPE type)
{
//--- Update correlation matrix if needed
if(TimeCurrent() - m_correlation_update_time > 3600) // Update hourly
{
UpdateCorrelationMatrix();
}
double correlation_risk = 0;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
//--- Check correlation with existing positions
for(int i = 0; i < ArraySize(m_exposures); i++)
{
if(m_exposures[i].symbol == symbol) continue;
double correlation = GetSymbolCorrelation(symbol, m_exposures[i].symbol);
//--- High correlation threshold
if(MathAbs(correlation) > 0.7)
{
double existing_exposure = (type == POSITION_TYPE_BUY) ?
m_exposures[i].long_exposure :
m_exposures[i].short_exposure;
//--- Positive correlation increases risk for same direction
//--- Negative correlation increases risk for opposite direction
if(correlation > 0)
{
correlation_risk += existing_exposure * correlation / balance * 100;
}
else
{
double opposite_exposure = (type == POSITION_TYPE_BUY) ?
m_exposures[i].short_exposure :
m_exposures[i].long_exposure;
correlation_risk += opposite_exposure * MathAbs(correlation) / balance * 100;
}
}
}
return correlation_risk;
}
//+------------------------------------------------------------------+
//| Update correlation matrix for monitored symbols |
//+------------------------------------------------------------------+
void CRiskManager::UpdateCorrelationMatrix()
{
//--- Get unique symbols from positions
m_correlation_count = 0;
int total = PositionsTotal();
for(int i = 0; i < total && m_correlation_count < 20; i++)
{
if(PositionSelectByTicket(PositionGetTicket(i)))
{
string symbol = PositionGetString(POSITION_SYMBOL);
bool found = false;
for(int j = 0; j < m_correlation_count; j++)
{
if(m_correlation_symbols[j] == symbol)
{
found = true;
break;
}
}
if(!found)
{
m_correlation_symbols[m_correlation_count] = symbol;
m_correlation_count++;
}
}
}
//--- Calculate correlations
int period = 100; // 100 bar correlation
for(int i = 0; i < m_correlation_count; i++)
{
for(int j = i; j < m_correlation_count; j++)
{
if(i == j)
{
m_correlation_matrix[i][j] = 1.0;
}
else
{
//--- Calculate correlation between symbols
double prices1[], prices2[];
ArrayResize(prices1, period);
ArrayResize(prices2, period);
bool valid = true;
for(int k = 0; k < period; k++)
{
prices1[k] = iClose(m_correlation_symbols[i], PERIOD_H1, k);
prices2[k] = iClose(m_correlation_symbols[j], PERIOD_H1, k);
if(prices1[k] <= 0 || prices2[k] <= 0)
{
valid = false;
break;
}
}
if(valid)
{
//--- Convert to returns
for(int k = 0; k < period - 1; k++)
{
prices1[k] = (prices1[k] - prices1[k+1]) / prices1[k+1];
prices2[k] = (prices2[k] - prices2[k+1]) / prices2[k+1];
}
//--- Calculate correlation coefficient
double corr = MathCorrelationPearson(prices1, prices2, period - 1);
m_correlation_matrix[i][j] = corr;
m_correlation_matrix[j][i] = corr;
}
else
{
m_correlation_matrix[i][j] = 0;
m_correlation_matrix[j][i] = 0;
}
}
}
}
m_correlation_update_time = TimeCurrent();
}
//+------------------------------------------------------------------+
//| Get correlation between two symbols |
//+------------------------------------------------------------------+
double CRiskManager::GetSymbolCorrelation(string symbol1, string symbol2)
{
//--- Find indices
int idx1 = -1, idx2 = -1;
for(int i = 0; i < m_correlation_count; i++)
{
if(m_correlation_symbols[i] == symbol1) idx1 = i;
if(m_correlation_symbols[i] == symbol2) idx2 = i;
}
//--- Return correlation if found
if(idx1 >= 0 && idx2 >= 0)
return m_correlation_matrix[idx1][idx2];
//--- Default no correlation
return 0;
}
//+------------------------------------------------------------------+
//| Check daily drawdown limit |
//+------------------------------------------------------------------+
bool CRiskManager::CheckDailyDrawdown()
{
//--- Reset daily metrics if new day
MqlDateTime current_time;
TimeToStruct(TimeCurrent(), current_time);
MqlDateTime last_reset;
TimeToStruct(m_last_reset_day, last_reset);
if(current_time.day != last_reset.day)
{
ResetDailyMetrics();
}
//--- Calculate current daily performance
double current_balance = AccountInfoDouble(ACCOUNT_BALANCE);
double daily_change = current_balance - m_daily_starting_balance;
//--- Check if within daily drawdown limit
double max_daily_loss = m_daily_starting_balance * m_config.max_daily_dd / 100;
return (daily_change > -max_daily_loss);
}
//+------------------------------------------------------------------+
//| Reset daily metrics |
//+------------------------------------------------------------------+
void CRiskManager::ResetDailyMetrics()
{
m_daily_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE);
m_daily_loss = 0;
m_last_reset_day = TimeCurrent();
Print("RiskManager: Daily metrics reset. Starting balance: ", m_daily_starting_balance);
}
//+------------------------------------------------------------------+
//| Enforce risk rules on external trades |
//+------------------------------------------------------------------+
void CRiskManager::EnforceRiskRules(ManagedTrade &trade, double default_risk, bool close_excessive)
{
//--- Calculate implied risk
double implied_risk = CalculateImpliedRisk(trade);
//--- If no stop loss, use default risk
if(trade.sl == 0)
{
trade.risk_percent = default_risk;
trade.risk_amount = AccountInfoDouble(ACCOUNT_BALANCE) * default_risk / 100;
}
else
{
trade.risk_percent = implied_risk;
}
//--- Check if exceeds maximum allowed risk
if(trade.risk_percent > m_config.max_risk_percent)
{
Print("RiskManager: External trade #", trade.ticket,
" exceeds risk limit: ", DoubleToString(trade.risk_percent, 2), "%");
if(close_excessive)
{
//--- Mark for closure
trade.close_requested = true;
trade.close_reason = "Excessive risk: " + DoubleToString(trade.risk_percent, 2) + "%";
}
}
//--- Check correlation risk
double corr_risk = CalculateCorrelationRisk(trade.symbol, trade.volume, trade.type);
if(corr_risk > m_config.max_risk_percent * 0.5) // 50% of max risk for correlation
{
Print("RiskManager: High correlation risk for #", trade.ticket,
": ", DoubleToString(corr_risk, 2), "%");
}
}
//+------------------------------------------------------------------+
//| Calculate implied risk from position parameters |
//+------------------------------------------------------------------+
double CRiskManager::CalculateImpliedRisk(const ManagedTrade &trade)
{
if(trade.sl == 0) return 0;
double stop_distance;
if(trade.type == POSITION_TYPE_BUY)
stop_distance = trade.open_price - trade.sl;
else
stop_distance = trade.sl - trade.open_price;
if(stop_distance <= 0) return 0;
double tick_value = SymbolInfoDouble(trade.symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(trade.symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick_size <= 0) return 0;
double potential_loss = (stop_distance / tick_size) * tick_value * trade.volume;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
return (balance > 0) ? (potential_loss / balance * 100) : 0;
}
//+------------------------------------------------------------------+
//| Get portfolio heatmap |
//+------------------------------------------------------------------+
double CRiskManager::GetPortfolioHeatmap(ExposureInfo &exposures[])
{
UpdateExposureMap();
//--- Copy exposure data
int size = ArraySize(m_exposures);
ArrayResize(exposures, size);
for(int i = 0; i < size; i++)
{
exposures[i] = m_exposures[i];
}
return GetCurrentExposure(false);
}
//+------------------------------------------------------------------+
//| Update performance metrics for Kelly calculation |
//+------------------------------------------------------------------+
void CRiskManager::UpdatePerformanceMetrics(bool won, double profit, double risk)
{
m_sample_size++;
//--- Update win rate (exponential moving average)
double alpha = 2.0 / (m_sample_size + 1);
m_win_rate = m_win_rate * (1 - alpha) + (won ? 1.0 : 0.0) * alpha;
//--- Update win/loss ratio
if(won && risk > 0)
{
double rr_achieved = profit / risk;
m_avg_win_loss_ratio = m_avg_win_loss_ratio * (1 - alpha) + rr_achieved * alpha;
}
//--- Update daily loss tracking
if(!won)
{
m_daily_loss += MathAbs(profit);
}
}
//+------------------------------------------------------------------+
//| Get Value at Risk (VaR) calculation |
//+------------------------------------------------------------------+
double CRiskManager::GetVaR(double confidence_level)
{
//--- Simple VaR based on current exposure and volatility
double exposure = GetCurrentExposure();
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
//--- Use normal distribution assumption
//--- z-score for 95% confidence = 1.645
double z_score = (confidence_level == 0.95) ? 1.645 :
(confidence_level == 0.99) ? 2.326 : 1.645;
//--- Estimate portfolio volatility (simplified)
double portfolio_volatility = exposure / 100 * 0.02; // 2% daily volatility assumption
return balance * portfolio_volatility * z_score;
}
//+------------------------------------------------------------------+
//| Get remaining daily risk allowance |
//+------------------------------------------------------------------+
double CRiskManager::GetRemainingDailyRisk()
{
double max_daily_loss = m_daily_starting_balance * m_config.max_daily_dd / 100;
double remaining = max_daily_loss - m_daily_loss;
return MathMax(remaining, 0);
}
//+------------------------------------------------------------------+
//| Update market conditions |
//+------------------------------------------------------------------+
void CRiskManager::UpdateMarketConditions(const MarketConditions &conditions)
{
//--- Adjust risk parameters based on market conditions
if(conditions.condition == MARKET_VOLATILE)
{
//--- Reduce risk in volatile markets
//--- This is just a notification, actual adjustment happens in position sizing
Print("RiskManager: Volatile market detected - consider reducing position sizes");
}
}
#endif // RISK_MANAGER_MQH
//+------------------------------------------------------------------+