A fully featured risk management module including: Advanced position sizing methods: Fixed lot, fixed percent, ATR-based, Kelly Criterion, Optimal F, and volatility-adjusted sizing Comprehensive risk validation: Daily loss limits, drawdown management, correlation risk, symbol concentration limits Performance tracking: For adaptive sizing algorithms Helper methods: All properly implemented for risk calculations Safety mechanisms: Multiple layers of protection against overleveraging 2. Complete ERMT_6.8.mq5 The final main EA file with: Full initialization sequence: With proper error handling and cleanup Enhanced tick processing: Optimized for performance with timer-based periodic tasks Trading hours management: Including Friday close functionality Alert system: Email and push notification support Performance tracking: Automatic snapshots and reporting System health monitoring: Memory and CPU usage checks Comprehensive logging: Multi-level logging with file output Helper functions: All utility functions properly implemented Key Features Added in the Complete Version: Enhanced Safety: Input validation on initialization Trading condition checks System health monitoring Proper cleanup on deinitialization Professional Features: Trading hours restrictions Friday close functionality Performance snapshots saved to CSV Multi-level alerts (email/push) Configurable logging levels Risk Management Enhancements: Symbol concentration limits Portfolio correlation analysis Adaptive position sizing based on performance Emergency position closure triggers User Experience: Detailed dashboard updates Status logging Configuration logging on startup Meaningful error messages File Organization: All seven modules are now complete and ready for compilation: DataTypes.mqh - Complete data structures and enums Utilities.mqh - Full utility functions library RiskManager.mqh - Advanced risk management system TechnicalAnalysis.mqh - (from previous conversation) ExternalTradeManager.mqh - External trade detection and management TradeManager.mqh - (from previous conversation) Dashboard.mqh - (from previous conversation) ERMT_6.8.mq5 - Main EA file with all features The EA is now feature-complete with institutional-grade capabilities including: Multi-strategy position sizing Advanced risk management External trade management Real-time dashboard Comprehensive logging and reporting Performance tracking and optimization This should compile successfully and provide a robust, professional trading system ready for testing and deployment
1004 lines
No EOL
72 KiB
MQL5
1004 lines
No EOL
72 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| RiskManager.mqh |
|
|
//| Institutional Grade Risk Management Module |
|
|
//| Complete Risk Control System |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| RiskManager.mqh |
|
|
//| Advanced Risk Management System |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef RISK_MANAGER_MQH
|
|
#define RISK_MANAGER_MQH
|
|
|
|
#include <Object.mqh>
|
|
#include "DataTypes.mqh"
|
|
#include "Utilities.mqh"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Risk Manager Class |
|
|
//+------------------------------------------------------------------+
|
|
class CRiskManager : public CObject
|
|
{
|
|
private:
|
|
// Dependencies
|
|
CUtilities* m_utils;
|
|
|
|
// Risk parameters
|
|
RiskParameters m_params;
|
|
|
|
// Current state
|
|
double m_daily_loss;
|
|
double m_max_daily_loss;
|
|
datetime m_day_start;
|
|
double m_starting_balance;
|
|
double m_current_exposure;
|
|
int m_open_positions;
|
|
int m_magic_number;
|
|
|
|
// Risk tracking
|
|
double m_peak_balance;
|
|
double m_current_drawdown;
|
|
ENUM_RISK_EVENT m_last_risk_event;
|
|
|
|
// Performance tracking for Kelly/Optimal F
|
|
double m_win_rate;
|
|
double m_average_win;
|
|
double m_average_loss;
|
|
double m_trade_returns[];
|
|
int m_return_count;
|
|
|
|
// Helper methods
|
|
double CalculateImpliedRisk(const TradeInfo &trade);
|
|
double CalculateCorrelationRisk(string symbol, const TradeInfo &trade);
|
|
void UpdateDrawdown();
|
|
bool CheckDailyLoss();
|
|
bool CheckMaxDrawdown();
|
|
double GetSymbolExposure(string symbol);
|
|
|
|
public:
|
|
// Constructor/Destructor
|
|
CRiskManager();
|
|
~CRiskManager();
|
|
|
|
// Initialization
|
|
bool Initialize(CUtilities* utils, const RiskParameters ¶ms);
|
|
void SetMagicNumber(int magic) { m_magic_number = magic; }
|
|
|
|
// Core risk functions
|
|
double CalculatePositionSize(string symbol, double stop_distance,
|
|
ENUM_POSITION_SIZING method);
|
|
bool ValidateNewPosition(string symbol, double lot_size,
|
|
double stop_distance);
|
|
bool ValidateAndAdjustRisk(TradeInfo &trade);
|
|
double CalculateTradeRisk(double lot_size, double stop_distance,
|
|
string symbol);
|
|
bool EnforceRiskRules(TradeInfo &trade);
|
|
|
|
// Risk status methods
|
|
bool CanOpenNewPosition();
|
|
bool IsDailyLossLimitReached();
|
|
bool IsDrawdownLimitReached();
|
|
double GetCurrentRiskLevel();
|
|
double GetAvailableRisk();
|
|
|
|
// Position sizing calculations
|
|
double CalculateKellyCriterion(double win_rate, double avg_win, double avg_loss);
|
|
double CalculateOptimalF(const double &returns[], int count);
|
|
double CalculateVolatilityAdjustedSize(string symbol, double base_size);
|
|
|
|
// Risk metrics
|
|
double GetCurrentExposure() const { return m_current_exposure; }
|
|
double GetDailyLoss() const { return m_daily_loss; }
|
|
double GetCurrentDrawdown() const { return m_current_drawdown; }
|
|
int GetOpenPositions() const { return m_open_positions; }
|
|
ENUM_RISK_EVENT GetLastRiskEvent() const { return m_last_risk_event; }
|
|
|
|
// Performance updates
|
|
void UpdatePerformanceMetrics(double win_rate, double avg_win, double avg_loss);
|
|
void AddTradeReturn(double return_value);
|
|
|
|
// Settings
|
|
void UpdateParameters(const RiskParameters ¶ms) { m_params = params; }
|
|
void ResetDailyCounters();
|
|
|
|
// Correlation and exposure
|
|
double CalculatePortfolioCorrelation();
|
|
bool CheckSymbolConcentration(string symbol);
|
|
double GetMaxPositionSize(string symbol, double stop_distance);
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Constructor |
|
|
//+------------------------------------------------------------------+
|
|
CRiskManager::CRiskManager()
|
|
{
|
|
m_utils = NULL;
|
|
m_daily_loss = 0;
|
|
m_max_daily_loss = 0;
|
|
m_day_start = 0;
|
|
m_starting_balance = 0;
|
|
m_current_exposure = 0;
|
|
m_open_positions = 0;
|
|
m_magic_number = 0;
|
|
m_peak_balance = 0;
|
|
m_current_drawdown = 0;
|
|
m_last_risk_event = RISK_NONE;
|
|
m_win_rate = 50;
|
|
m_average_win = 0;
|
|
m_average_loss = 0;
|
|
m_return_count = 0;
|
|
ArrayResize(m_trade_returns, 100);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Destructor |
|
|
//+------------------------------------------------------------------+
|
|
CRiskManager::~CRiskManager()
|
|
{
|
|
ArrayFree(m_trade_returns);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Risk Manager |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::Initialize(CUtilities* utils, const RiskParameters ¶ms)
|
|
{
|
|
if(utils == NULL) return false;
|
|
|
|
m_utils = utils;
|
|
m_params = params;
|
|
|
|
m_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
m_peak_balance = m_starting_balance;
|
|
m_day_start = TimeCurrent();
|
|
m_max_daily_loss = m_starting_balance * m_params.max_daily_loss / 100;
|
|
|
|
m_utils.Log("Risk Manager initialized", LOG_INFO);
|
|
m_utils.Log(StringFormat("Max risk per trade: %.2f%%", m_params.max_risk_per_trade), LOG_INFO);
|
|
m_utils.Log(StringFormat("Max daily loss: %.2f%%", m_params.max_daily_loss), LOG_INFO);
|
|
m_utils.Log(StringFormat("Max drawdown: %.2f%%", m_params.max_drawdown), LOG_INFO);
|
|
m_utils.Log(StringFormat("Max positions: %d", (int)m_params.max_positions), LOG_INFO);
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Position Size |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculatePositionSize(string symbol, double stop_distance,
|
|
ENUM_POSITION_SIZING method)
|
|
{
|
|
if(stop_distance <= 0) return 0;
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double risk_amount = balance * m_params.max_risk_per_trade / 100;
|
|
double lot_size = 0;
|
|
|
|
switch(method)
|
|
{
|
|
case SIZING_FIXED_LOT:
|
|
lot_size = 0.1; // Default fixed lot
|
|
break;
|
|
|
|
case SIZING_FIXED_PERCENT:
|
|
lot_size = m_utils.CalculatePositionSize(risk_amount, stop_distance, symbol);
|
|
break;
|
|
|
|
case SIZING_ATR_BASED:
|
|
{
|
|
double atr = m_utils.GetATR(14);
|
|
if(atr > 0)
|
|
{
|
|
// Size based on ATR volatility
|
|
double normalized_stop = stop_distance / atr;
|
|
double adjusted_risk = risk_amount / normalized_stop;
|
|
lot_size = m_utils.CalculatePositionSize(adjusted_risk, stop_distance, symbol);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SIZING_KELLY_CRITERION:
|
|
{
|
|
double kelly_fraction = CalculateKellyCriterion(m_win_rate, m_average_win, m_average_loss);
|
|
double kelly_risk = balance * kelly_fraction;
|
|
lot_size = m_utils.CalculatePositionSize(kelly_risk, stop_distance, symbol);
|
|
}
|
|
break;
|
|
|
|
case SIZING_OPTIMAL_F:
|
|
{
|
|
if(m_return_count > 10)
|
|
{
|
|
double optimal_f = CalculateOptimalF(m_trade_returns, m_return_count);
|
|
double optimal_risk = balance * optimal_f;
|
|
lot_size = m_utils.CalculatePositionSize(optimal_risk, stop_distance, symbol);
|
|
}
|
|
else
|
|
{
|
|
// Fall back to fixed percent if insufficient data
|
|
lot_size = m_utils.CalculatePositionSize(risk_amount, stop_distance, symbol);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SIZING_VOLATILITY_ADJUSTED:
|
|
lot_size = CalculateVolatilityAdjustedSize(symbol,
|
|
m_utils.CalculatePositionSize(risk_amount, stop_distance, symbol));
|
|
break;
|
|
|
|
default:
|
|
lot_size = m_utils.CalculatePositionSize(risk_amount, stop_distance, symbol);
|
|
break;
|
|
}
|
|
|
|
// Apply risk multiplier
|
|
lot_size *= m_params.risk_multiplier;
|
|
|
|
// Apply scale with equity if enabled
|
|
if(m_params.scale_with_equity)
|
|
{
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double equity_ratio = equity / m_starting_balance;
|
|
if(equity_ratio > 1.0)
|
|
lot_size *= MathSqrt(equity_ratio); // Conservative scaling
|
|
}
|
|
|
|
// Ensure within broker limits
|
|
double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
|
|
if(lot_size < min_lot) lot_size = min_lot;
|
|
if(lot_size > max_lot) lot_size = max_lot;
|
|
|
|
// Final check against maximum position size
|
|
double max_allowed = GetMaxPositionSize(symbol, stop_distance);
|
|
if(lot_size > max_allowed) lot_size = max_allowed;
|
|
|
|
return m_utils.NormalizeLot(lot_size, symbol);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate New Position |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::ValidateNewPosition(string symbol, double lot_size, double stop_distance)
|
|
{
|
|
// Update position count
|
|
m_open_positions = 0;
|
|
for(int i = 0; i < PositionsTotal(); i++)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(m_magic_number == 0 || PositionGetInteger(POSITION_MAGIC) == m_magic_number)
|
|
m_open_positions++;
|
|
}
|
|
}
|
|
|
|
// Check if we can open new positions
|
|
if(m_open_positions >= m_params.max_positions)
|
|
{
|
|
m_utils.Log("Maximum positions reached", LOG_WARNING);
|
|
m_last_risk_event = RISK_OVERLEVERAGED;
|
|
return false;
|
|
}
|
|
|
|
// Check daily loss limit
|
|
if(IsDailyLossLimitReached())
|
|
{
|
|
m_utils.Log("Daily loss limit reached", LOG_WARNING);
|
|
m_last_risk_event = RISK_DAILY_LOSS_LIMIT;
|
|
return false;
|
|
}
|
|
|
|
// Check drawdown
|
|
if(IsDrawdownLimitReached())
|
|
{
|
|
m_utils.Log("Drawdown limit reached", LOG_WARNING);
|
|
m_last_risk_event = RISK_DRAWDOWN_LIMIT;
|
|
return false;
|
|
}
|
|
|
|
// Calculate potential risk
|
|
double risk = CalculateTradeRisk(lot_size, stop_distance, symbol);
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double risk_percent = (risk / balance) * 100;
|
|
|
|
if(risk_percent > m_params.max_risk_per_trade)
|
|
{
|
|
m_utils.Log(StringFormat("Risk too high: %.2f%% > %.2f%%",
|
|
risk_percent, m_params.max_risk_per_trade), LOG_WARNING);
|
|
m_last_risk_event = RISK_OVERLEVERAGED;
|
|
return false;
|
|
}
|
|
|
|
// Check symbol concentration
|
|
if(!CheckSymbolConcentration(symbol))
|
|
{
|
|
m_utils.Log("Symbol concentration limit reached", LOG_WARNING);
|
|
m_last_risk_event = RISK_CORRELATION_HIGH;
|
|
return false;
|
|
}
|
|
|
|
// Check margin requirements
|
|
double required_margin = 0;
|
|
if(!OrderCalcMargin(ORDER_TYPE_BUY, symbol, lot_size,
|
|
SymbolInfoDouble(symbol, SYMBOL_ASK), required_margin))
|
|
{
|
|
m_utils.Log("Failed to calculate margin", LOG_ERROR);
|
|
return false;
|
|
}
|
|
|
|
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
if(required_margin > free_margin * 0.8) // Use max 80% of free margin
|
|
{
|
|
m_utils.Log("Insufficient margin", LOG_WARNING);
|
|
m_last_risk_event = RISK_MARGIN_CALL;
|
|
return false;
|
|
}
|
|
|
|
// Check volatility conditions
|
|
double current_spread = m_utils.CalculateSpread(symbol);
|
|
double max_spread = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * 2.0;
|
|
if(current_spread > max_spread)
|
|
{
|
|
m_utils.Log("Spread too high for entry", LOG_WARNING);
|
|
m_last_risk_event = RISK_VOLATILITY_SPIKE;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate and Adjust Risk for Existing Trade |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::ValidateAndAdjustRisk(TradeInfo &trade)
|
|
{
|
|
// Calculate current risk
|
|
double implied_risk = CalculateImpliedRisk(trade);
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
// Update trade risk metrics
|
|
trade.risk_amount = implied_risk;
|
|
trade.risk_percent = (implied_risk / balance) * 100;
|
|
|
|
// Calculate current R/R
|
|
if(trade.tp != 0 && trade.sl != 0)
|
|
{
|
|
trade.current_rr = m_utils.CalculateRiskReward(trade.entry_price, trade.sl,
|
|
trade.tp, trade.type);
|
|
}
|
|
|
|
// Check if trade violates risk rules
|
|
if(trade.risk_percent > m_params.max_risk_per_trade * 1.5)
|
|
{
|
|
m_utils.Log(StringFormat("Trade %d exceeds 150%% risk limit: %.2f%%",
|
|
trade.ticket, trade.risk_percent), LOG_WARNING);
|
|
trade.close_requested = true;
|
|
trade.close_reason = "Risk limit exceeded";
|
|
return false;
|
|
}
|
|
|
|
// Check correlation risk
|
|
double correlation_risk = CalculateCorrelationRisk(trade.symbol, trade);
|
|
|
|
if(correlation_risk > m_params.max_correlation)
|
|
{
|
|
m_utils.Log(StringFormat("High correlation risk: %.2f", correlation_risk), LOG_WARNING);
|
|
m_last_risk_event = RISK_CORRELATION_HIGH;
|
|
|
|
// Consider partial close
|
|
trade.partial_close_volume = trade.volume * 0.5;
|
|
trade.partial_close_reason = "High correlation";
|
|
}
|
|
|
|
// Check if R/R has deteriorated
|
|
if(trade.current_rr < m_params.min_risk_reward * 0.5 && trade.profit < 0)
|
|
{
|
|
m_utils.Log(StringFormat("Trade %d R/R deteriorated: %.2f",
|
|
trade.ticket, trade.current_rr), LOG_WARNING);
|
|
trade.close_requested = true;
|
|
trade.close_reason = "Poor risk/reward";
|
|
return false;
|
|
}
|
|
|
|
trade.last_update = TimeCurrent();
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Trade Risk |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculateTradeRisk(double lot_size, double stop_distance, string symbol)
|
|
{
|
|
if(stop_distance <= 0) return 0;
|
|
|
|
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
|
|
if(tick_size == 0) return 0;
|
|
|
|
double risk = (stop_distance / tick_size) * tick_value * lot_size;
|
|
|
|
return risk;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Enforce Risk Rules |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::EnforceRiskRules(TradeInfo &trade)
|
|
{
|
|
// Check if emergency close needed
|
|
if(trade.risk_percent > m_params.max_risk_per_trade * 2.0)
|
|
{
|
|
trade.close_requested = true;
|
|
trade.close_reason = "Emergency: Risk exceeded 200% of limit";
|
|
m_last_risk_event = RISK_OVERLEVERAGED;
|
|
m_utils.LogRiskEvent(RISK_OVERLEVERAGED,
|
|
StringFormat("Trade %d risk at %.2f%%", trade.ticket, trade.risk_percent));
|
|
return false;
|
|
}
|
|
|
|
// Check drawdown
|
|
UpdateDrawdown();
|
|
if(m_current_drawdown > m_params.max_drawdown)
|
|
{
|
|
trade.close_requested = true;
|
|
trade.close_reason = "Maximum drawdown reached";
|
|
m_last_risk_event = RISK_DRAWDOWN_LIMIT;
|
|
m_utils.LogRiskEvent(RISK_DRAWDOWN_LIMIT,
|
|
StringFormat("Drawdown at %.2f%%", m_current_drawdown));
|
|
return false;
|
|
}
|
|
|
|
// Check daily loss
|
|
if(CheckDailyLoss())
|
|
{
|
|
trade.close_requested = true;
|
|
trade.close_reason = "Daily loss limit reached";
|
|
m_last_risk_event = RISK_DAILY_LOSS_LIMIT;
|
|
m_utils.LogRiskEvent(RISK_DAILY_LOSS_LIMIT,
|
|
StringFormat("Daily loss at %.2f", m_daily_loss));
|
|
return false;
|
|
}
|
|
|
|
// Check margin level
|
|
double margin_level = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
|
if(margin_level > 0 && margin_level < 150)
|
|
{
|
|
trade.close_requested = true;
|
|
trade.close_reason = "Low margin level";
|
|
m_last_risk_event = RISK_MARGIN_CALL;
|
|
m_utils.LogRiskEvent(RISK_MARGIN_CALL,
|
|
StringFormat("Margin level at %.2f%%", margin_level));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Implied Risk (Helper) |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculateImpliedRisk(const TradeInfo &trade)
|
|
{
|
|
if(trade.sl == 0)
|
|
{
|
|
// Estimate risk based on ATR if no stop loss
|
|
double atr = m_utils.GetATR(14);
|
|
double estimated_stop = atr * 2.0;
|
|
return CalculateTradeRisk(trade.volume, estimated_stop, trade.symbol);
|
|
}
|
|
|
|
double stop_distance = MathAbs(trade.entry_price - trade.sl);
|
|
return CalculateTradeRisk(trade.volume, stop_distance, trade.symbol);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Correlation Risk (Helper) |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculateCorrelationRisk(string symbol, const TradeInfo &trade)
|
|
{
|
|
double correlation = 0;
|
|
int same_direction_trades = 0;
|
|
int same_symbol_trades = 0;
|
|
double total_exposure = 0;
|
|
|
|
// Analyze existing positions
|
|
for(int i = 0; i < PositionsTotal(); i++)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0 || ticket == trade.ticket) continue;
|
|
|
|
// Check magic number if set
|
|
if(m_magic_number != 0 && PositionGetInteger(POSITION_MAGIC) != m_magic_number)
|
|
continue;
|
|
|
|
string pos_symbol = PositionGetString(POSITION_SYMBOL);
|
|
ENUM_ORDER_TYPE pos_type = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
double pos_volume = PositionGetDouble(POSITION_VOLUME);
|
|
|
|
// Count same direction trades
|
|
if(pos_type == trade.type)
|
|
same_direction_trades++;
|
|
|
|
// Count same symbol trades
|
|
if(pos_symbol == symbol)
|
|
{
|
|
same_symbol_trades++;
|
|
total_exposure += pos_volume;
|
|
}
|
|
|
|
// Check for correlated pairs (simplified)
|
|
if(StringFind(pos_symbol, StringSubstr(symbol, 0, 3)) >= 0 ||
|
|
StringFind(pos_symbol, StringSubstr(symbol, 3, 3)) >= 0)
|
|
{
|
|
correlation += 0.3; // Base correlation for currency pairs
|
|
}
|
|
}
|
|
|
|
// Calculate correlation score
|
|
if(m_open_positions > 0)
|
|
{
|
|
correlation += (double)same_direction_trades / m_open_positions * 0.5;
|
|
correlation += (double)same_symbol_trades / m_open_positions * 0.7;
|
|
}
|
|
|
|
// Normalize to 0-1 range
|
|
correlation = MathMin(correlation, 1.0);
|
|
|
|
return correlation;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update Drawdown |
|
|
//+------------------------------------------------------------------+
|
|
void CRiskManager::UpdateDrawdown()
|
|
{
|
|
double current_balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
if(current_balance > m_peak_balance)
|
|
m_peak_balance = current_balance;
|
|
|
|
if(m_peak_balance > 0)
|
|
m_current_drawdown = ((m_peak_balance - current_balance) / m_peak_balance) * 100;
|
|
else
|
|
m_current_drawdown = 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check Daily Loss |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::CheckDailyLoss()
|
|
{
|
|
MqlDateTime current_time, start_time;
|
|
TimeToStruct(TimeCurrent(), current_time);
|
|
TimeToStruct(m_day_start, start_time);
|
|
|
|
// Reset if new day
|
|
if(current_time.day != start_time.day)
|
|
{
|
|
ResetDailyCounters();
|
|
return false;
|
|
}
|
|
|
|
double current_balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
m_daily_loss = m_starting_balance - current_balance;
|
|
|
|
return (m_daily_loss >= m_max_daily_loss);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Can Open New Position |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::CanOpenNewPosition()
|
|
{
|
|
// Update position count
|
|
m_open_positions = 0;
|
|
for(int i = 0; i < PositionsTotal(); i++)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(m_magic_number == 0 || PositionGetInteger(POSITION_MAGIC) == m_magic_number)
|
|
m_open_positions++;
|
|
}
|
|
}
|
|
|
|
if(m_open_positions >= m_params.max_positions)
|
|
{
|
|
m_utils.Log(StringFormat("Cannot open: Max positions (%d) reached",
|
|
(int)m_params.max_positions), LOG_DEBUG);
|
|
return false;
|
|
}
|
|
|
|
if(IsDailyLossLimitReached())
|
|
{
|
|
m_utils.Log("Cannot open: Daily loss limit reached", LOG_DEBUG);
|
|
return false;
|
|
}
|
|
|
|
if(IsDrawdownLimitReached())
|
|
{
|
|
m_utils.Log("Cannot open: Drawdown limit reached", LOG_DEBUG);
|
|
return false;
|
|
}
|
|
|
|
// Check margin level
|
|
double margin_level = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
|
if(margin_level > 0 && margin_level < 200)
|
|
{
|
|
m_utils.Log(StringFormat("Cannot open: Low margin level (%.2f%%)", margin_level), LOG_DEBUG);
|
|
return false;
|
|
}
|
|
|
|
// Check if market is open
|
|
if(!m_utils.IsMarketOpen())
|
|
{
|
|
m_utils.Log("Cannot open: Market closed", LOG_DEBUG);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Is Daily Loss Limit Reached |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::IsDailyLossLimitReached()
|
|
{
|
|
return CheckDailyLoss();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Is Drawdown Limit Reached |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::IsDrawdownLimitReached()
|
|
{
|
|
UpdateDrawdown();
|
|
return (m_current_drawdown >= m_params.max_drawdown);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Current Risk Level |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::GetCurrentRiskLevel()
|
|
{
|
|
double total_risk = 0;
|
|
m_current_exposure = 0;
|
|
|
|
for(int i = 0; i < PositionsTotal(); i++)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
|
|
// Check magic number if set
|
|
if(m_magic_number != 0 && PositionGetInteger(POSITION_MAGIC) != m_magic_number)
|
|
continue;
|
|
|
|
double volume = PositionGetDouble(POSITION_VOLUME);
|
|
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double sl = PositionGetDouble(POSITION_SL);
|
|
string symbol = PositionGetString(POSITION_SYMBOL);
|
|
|
|
m_current_exposure += volume;
|
|
|
|
if(sl != 0)
|
|
{
|
|
double stop_distance = MathAbs(entry - sl);
|
|
total_risk += CalculateTradeRisk(volume, stop_distance, symbol);
|
|
}
|
|
else
|
|
{
|
|
// Estimate risk based on ATR if no stop
|
|
double atr = m_utils.GetATR(14);
|
|
total_risk += CalculateTradeRisk(volume, atr * 2.0, symbol);
|
|
}
|
|
}
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
return balance > 0 ? (total_risk / balance) * 100 : 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Available Risk |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::GetAvailableRisk()
|
|
{
|
|
double current_risk = GetCurrentRiskLevel();
|
|
double max_total_risk = m_params.max_risk_per_trade * m_params.max_positions;
|
|
double available = max_total_risk - current_risk;
|
|
|
|
return (available > 0) ? available : 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Kelly Criterion |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculateKellyCriterion(double win_rate, double avg_win, double avg_loss)
|
|
{
|
|
if(avg_loss <= 0) return 0.01; // Default 1% if no loss data
|
|
|
|
double b = avg_win / MathAbs(avg_loss); // Win/loss ratio
|
|
double p = win_rate / 100.0; // Win probability
|
|
double q = 1.0 - p; // Loss probability
|
|
|
|
if(b <= 0) return 0.01;
|
|
|
|
double kelly = (p * b - q) / b;
|
|
|
|
// Apply Kelly fraction (typically 25% of full Kelly for safety)
|
|
kelly *= 0.25;
|
|
|
|
// Ensure within reasonable bounds (1% to 25%)
|
|
if(kelly < 0.01) kelly = 0.01;
|
|
if(kelly > 0.25) kelly = 0.25;
|
|
|
|
m_utils.Log(StringFormat("Kelly Criterion: %.2f%% (WR=%.1f%%, W/L=%.2f)",
|
|
kelly * 100, win_rate, b), LOG_DEBUG);
|
|
|
|
return kelly;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Optimal F |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculateOptimalF(const double &returns[], int count)
|
|
{
|
|
if(count < 10) return 0.02; // Default 2% if insufficient data
|
|
|
|
// Find largest loss
|
|
double max_loss = 0;
|
|
for(int i = 0; i < count; i++)
|
|
{
|
|
if(returns[i] < max_loss) max_loss = returns[i];
|
|
}
|
|
|
|
if(max_loss >= 0) return 0.02; // No losses, use default
|
|
|
|
max_loss = MathAbs(max_loss);
|
|
|
|
// Test different F values to find optimal
|
|
double optimal_f = 0.01;
|
|
double max_twr = 0;
|
|
|
|
for(double f = 0.01; f <= 0.5; f += 0.01)
|
|
{
|
|
double twr = 1.0; // Terminal wealth relative
|
|
|
|
for(int i = 0; i < count; i++)
|
|
{
|
|
double hpr = 1.0 + f * (returns[i] / max_loss);
|
|
if(hpr <= 0)
|
|
{
|
|
twr = 0;
|
|
break;
|
|
}
|
|
twr *= hpr;
|
|
}
|
|
|
|
if(twr > max_twr)
|
|
{
|
|
max_twr = twr;
|
|
optimal_f = f;
|
|
}
|
|
}
|
|
|
|
// Apply safety factor (50% of optimal)
|
|
optimal_f *= 0.5;
|
|
|
|
// Ensure within reasonable bounds
|
|
if(optimal_f < 0.01) optimal_f = 0.01;
|
|
if(optimal_f > 0.25) optimal_f = 0.25;
|
|
|
|
m_utils.Log(StringFormat("Optimal F: %.2f%% (TWR=%.2f)",
|
|
optimal_f * 100, max_twr), LOG_DEBUG);
|
|
|
|
return optimal_f;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Volatility Adjusted Size |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculateVolatilityAdjustedSize(string symbol, double base_size)
|
|
{
|
|
double current_atr = m_utils.GetATR(14);
|
|
|
|
// Calculate historical average ATR (simplified - use 50 period MA of ATR)
|
|
int atr_handle = iATR(symbol, PERIOD_CURRENT, 14);
|
|
if(atr_handle == INVALID_HANDLE) return base_size;
|
|
|
|
double atr_buffer[50];
|
|
if(CopyBuffer(atr_handle, 0, 0, 50, atr_buffer) < 50)
|
|
{
|
|
IndicatorRelease(atr_handle);
|
|
return base_size;
|
|
}
|
|
IndicatorRelease(atr_handle);
|
|
|
|
double avg_atr = 0;
|
|
for(int i = 0; i < 50; i++)
|
|
avg_atr += atr_buffer[i];
|
|
avg_atr /= 50;
|
|
|
|
if(avg_atr == 0) return base_size;
|
|
|
|
double volatility_ratio = current_atr / avg_atr;
|
|
double adjusted_size = base_size;
|
|
|
|
// Adjust size based on volatility
|
|
if(volatility_ratio > 2.0)
|
|
adjusted_size = base_size * 0.25; // Very high volatility - reduce to 25%
|
|
else if(volatility_ratio > 1.5)
|
|
adjusted_size = base_size * 0.5; // High volatility - reduce to 50%
|
|
else if(volatility_ratio > 1.2)
|
|
adjusted_size = base_size * 0.75; // Elevated volatility - reduce to 75%
|
|
else if(volatility_ratio < 0.5)
|
|
adjusted_size = base_size * 1.5; // Low volatility - increase to 150%
|
|
else if(volatility_ratio < 0.8)
|
|
adjusted_size = base_size * 1.25; // Below average volatility - increase to 125%
|
|
|
|
m_utils.Log(StringFormat("Volatility adjustment: %.2f (ratio=%.2f)",
|
|
adjusted_size/base_size, volatility_ratio), LOG_DEBUG);
|
|
|
|
return adjusted_size;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update Performance Metrics |
|
|
//+------------------------------------------------------------------+
|
|
void CRiskManager::UpdatePerformanceMetrics(double win_rate, double avg_win, double avg_loss)
|
|
{
|
|
m_win_rate = win_rate;
|
|
m_average_win = avg_win;
|
|
m_average_loss = avg_loss;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Add Trade Return |
|
|
//+------------------------------------------------------------------+
|
|
void CRiskManager::AddTradeReturn(double return_value)
|
|
{
|
|
if(m_return_count >= ArraySize(m_trade_returns))
|
|
{
|
|
ArrayResize(m_trade_returns, m_return_count + 100);
|
|
}
|
|
|
|
m_trade_returns[m_return_count] = return_value;
|
|
m_return_count++;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Reset Daily Counters |
|
|
//+------------------------------------------------------------------+
|
|
void CRiskManager::ResetDailyCounters()
|
|
{
|
|
m_day_start = TimeCurrent();
|
|
m_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
m_daily_loss = 0;
|
|
m_max_daily_loss = m_starting_balance * m_params.max_daily_loss / 100;
|
|
|
|
m_utils.Log("Daily risk counters reset", LOG_INFO);
|
|
m_utils.Log(StringFormat("New day starting balance: %.2f", m_starting_balance), LOG_INFO);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Portfolio Correlation |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::CalculatePortfolioCorrelation()
|
|
{
|
|
if(m_open_positions <= 1) return 0;
|
|
|
|
double correlation = 0;
|
|
string symbols[];
|
|
ENUM_ORDER_TYPE types[];
|
|
ArrayResize(symbols, m_open_positions);
|
|
ArrayResize(types, m_open_positions);
|
|
|
|
int index = 0;
|
|
for(int i = 0; i < PositionsTotal() && index < m_open_positions; i++)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
|
|
if(m_magic_number != 0 && PositionGetInteger(POSITION_MAGIC) != m_magic_number)
|
|
continue;
|
|
|
|
symbols[index] = PositionGetString(POSITION_SYMBOL);
|
|
types[index] = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
index++;
|
|
}
|
|
|
|
// Calculate pairwise correlations (simplified)
|
|
for(int i = 0; i < index - 1; i++)
|
|
{
|
|
for(int j = i + 1; j < index; j++)
|
|
{
|
|
// Same direction trades
|
|
if(types[i] == types[j])
|
|
correlation += 0.3;
|
|
|
|
// Check currency overlap
|
|
string base1 = StringSubstr(symbols[i], 0, 3);
|
|
string quote1 = StringSubstr(symbols[i], 3, 3);
|
|
string base2 = StringSubstr(symbols[j], 0, 3);
|
|
string quote2 = StringSubstr(symbols[j], 3, 3);
|
|
|
|
if(base1 == base2 || quote1 == quote2)
|
|
correlation += 0.4;
|
|
else if(base1 == quote2 || quote1 == base2)
|
|
correlation += 0.2;
|
|
}
|
|
}
|
|
|
|
// Normalize
|
|
int pair_count = index * (index - 1) / 2;
|
|
if(pair_count > 0)
|
|
correlation /= pair_count;
|
|
|
|
return MathMin(correlation, 1.0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check Symbol Concentration |
|
|
//+------------------------------------------------------------------+
|
|
bool CRiskManager::CheckSymbolConcentration(string symbol)
|
|
{
|
|
double symbol_exposure = GetSymbolExposure(symbol);
|
|
double total_exposure = m_current_exposure;
|
|
|
|
if(total_exposure == 0) return true;
|
|
|
|
double concentration = symbol_exposure / total_exposure;
|
|
|
|
// Don't allow more than 40% in single symbol
|
|
if(concentration > 0.4)
|
|
{
|
|
m_utils.Log(StringFormat("Symbol concentration too high: %.1f%%",
|
|
concentration * 100), LOG_WARNING);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Symbol Exposure |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::GetSymbolExposure(string symbol)
|
|
{
|
|
double exposure = 0;
|
|
|
|
for(int i = 0; i < PositionsTotal(); i++)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
|
|
if(m_magic_number != 0 && PositionGetInteger(POSITION_MAGIC) != m_magic_number)
|
|
continue;
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) == symbol)
|
|
exposure += PositionGetDouble(POSITION_VOLUME);
|
|
}
|
|
|
|
return exposure;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Maximum Position Size |
|
|
//+------------------------------------------------------------------+
|
|
double CRiskManager::GetMaxPositionSize(string symbol, double stop_distance)
|
|
{
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double max_risk = balance * m_params.max_risk_per_trade / 100;
|
|
|
|
// Adjust for current exposure
|
|
double available_risk = GetAvailableRisk();
|
|
if(available_risk < m_params.max_risk_per_trade)
|
|
max_risk = balance * available_risk / 100;
|
|
|
|
double max_size = m_utils.CalculatePositionSize(max_risk, stop_distance, symbol);
|
|
|
|
return max_size;
|
|
}
|
|
|
|
#endif // RISK_MANAGER_MQH |