789 lines
No EOL
27 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+ |