//+------------------------------------------------------------------+ //| PerformanceTracker.mqh v1.0 | //| Centralized Performance Tracking & Analytics | //| Optimized for Real-time Updates | //+------------------------------------------------------------------+ #ifndef PERFORMANCE_TRACKER_MQH #define PERFORMANCE_TRACKER_MQH #include "DataTypes.mqh" #include //+------------------------------------------------------------------+ //| Trade history entry for performance calculation | //+------------------------------------------------------------------+ struct TradeHistoryEntry { ulong ticket; string symbol; ENUM_POSITION_TYPE type; datetime open_time; datetime close_time; double volume; double open_price; double close_price; double profit; double commission; double swap; double risk_amount; double r_multiple; int duration_bars; string strategy; }; //+------------------------------------------------------------------+ //| Performance Tracker Class | //+------------------------------------------------------------------+ class CPerformanceTracker { private: //--- Trade history TradeHistoryEntry m_history[]; int m_history_size; int m_max_history; //--- Cached metrics PerformanceMetrics m_metrics; datetime m_last_update; bool m_cache_valid; //--- Daily tracking double m_daily_starting_balance; datetime m_day_start; PerformanceMetrics m_daily_metrics; //--- Return series for advanced calculations double m_returns[]; double m_equity_curve[]; int m_curve_size; //--- Configuration int m_lookback_period; double m_risk_free_rate; //--- Helper methods void UpdateBasicMetrics(); void UpdateAdvancedMetrics(); void UpdateDrawdownMetrics(); void UpdateReturnDistribution(); void CalculateRatios(); double CalculateSharpe(); double CalculateSortino(); double CalculateCalmar(); void UpdateEquityCurve(double current_balance); public: CPerformanceTracker(); ~CPerformanceTracker(); //--- Initialization bool Initialize(int max_history = 1000, int lookback = 252); //--- Trade tracking void AddTrade(const ManagedTrade &trade, double close_price); void UpdateOpenTrade(const ManagedTrade &trade); //--- Metric access PerformanceMetrics GetMetrics(bool force_update = false); PerformanceMetrics GetDailyMetrics(); //--- Specific metrics double GetWinRate() { return m_metrics.win_rate; } double GetProfitFactor() { return m_metrics.profit_factor; } double GetExpectancy() { return m_metrics.expectancy; } double GetSharpeRatio() { return m_metrics.sharpe_ratio; } double GetMaxDrawdown() { return m_metrics.max_drawdown_percent; } double GetRecoveryFactor() { return m_metrics.recovery_factor; } //--- Advanced analytics double GetValueAtRisk(double confidence = 0.95); double GetExpectedShortfall(double confidence = 0.95); double GetMaximumAdverseExcursion(); double GetMaximumFavorableExcursion(); double GetProfitability(); double GetConsistencyScore(); //--- Strategy analysis void GetStrategyBreakdown(string &strategies[], double &profits[]); void GetTimeAnalysis(int &hour_wins[], int &hour_losses[]); void GetSymbolBreakdown(string &symbols[], double &profits[]); //--- Period analysis PerformanceMetrics GetPeriodMetrics(datetime from, datetime to); PerformanceMetrics GetMonthlyMetrics(int year, int month); PerformanceMetrics GetWeeklyMetrics(); //--- Reset functions void ResetDaily(); void ResetAll(); //--- Export functions bool ExportToCSV(string filename); string GetSummaryReport(); //--- Statistical functions double GetAverageTradeTime(); double GetWinLossRatio(); double GetPayoffRatio(); int GetTradingDays(); double GetTradesPerDay(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPerformanceTracker::CPerformanceTracker() { m_history_size = 0; m_max_history = 1000; m_last_update = 0; m_cache_valid = false; m_curve_size = 0; m_lookback_period = 252; // Trading days m_risk_free_rate = 0.02; // 2% annual m_daily_starting_balance = 0; m_day_start = 0; //--- Initialize metrics ZeroMemory(m_metrics); ZeroMemory(m_daily_metrics); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPerformanceTracker::~CPerformanceTracker() { } //+------------------------------------------------------------------+ //| Initialize tracker | //+------------------------------------------------------------------+ bool CPerformanceTracker::Initialize(int max_history, int lookback) { m_max_history = max_history; m_lookback_period = lookback; //--- Allocate arrays ArrayResize(m_history, m_max_history); ArrayResize(m_returns, m_lookback_period); ArrayResize(m_equity_curve, m_lookback_period * 2); //--- Initialize daily tracking m_daily_starting_balance = AccountInfoDouble(ACCOUNT_BALANCE); m_day_start = TimeCurrent(); Print("PerformanceTracker initialized: MaxHistory=", m_max_history, ", Lookback=", m_lookback_period); return true; } //+------------------------------------------------------------------+ //| Add closed trade to history | //+------------------------------------------------------------------+ void CPerformanceTracker::AddTrade(const ManagedTrade &trade, double close_price) { //--- Check array capacity if(m_history_size >= m_max_history) { //--- Shift array (remove oldest) for(int i = 0; i < m_max_history - 1; i++) m_history[i] = m_history[i + 1]; m_history_size = m_max_history - 1; } //--- Add new entry TradeHistoryEntry entry; entry.ticket = trade.ticket; entry.symbol = trade.symbol; entry.type = trade.type; entry.open_time = trade.open_time; entry.close_time = TimeCurrent(); entry.volume = trade.volume; entry.open_price = trade.open_price; entry.close_price = close_price; entry.profit = trade.profit; entry.commission = trade.commission; entry.swap = trade.swap; entry.risk_amount = trade.risk_amount; entry.r_multiple = trade.r_multiple; entry.duration_bars = trade.bars_in_trade; entry.strategy = trade.entry_reason; m_history[m_history_size] = entry; m_history_size++; //--- Invalidate cache m_cache_valid = false; //--- Update equity curve UpdateEquityCurve(AccountInfoDouble(ACCOUNT_BALANCE)); //--- Update metrics if significant time passed if(TimeCurrent() - m_last_update > 5) { UpdateBasicMetrics(); m_last_update = TimeCurrent(); } } //+------------------------------------------------------------------+ //| Get performance metrics with caching | //+------------------------------------------------------------------+ PerformanceMetrics CPerformanceTracker::GetMetrics(bool force_update) { if(!m_cache_valid || force_update || TimeCurrent() - m_last_update > 30) { UpdateBasicMetrics(); UpdateAdvancedMetrics(); UpdateDrawdownMetrics(); CalculateRatios(); m_cache_valid = true; m_last_update = TimeCurrent(); } return m_metrics; } //+------------------------------------------------------------------+ //| Update basic performance metrics | //+------------------------------------------------------------------+ void CPerformanceTracker::UpdateBasicMetrics() { //--- Reset counters m_metrics.total_trades = m_history_size; m_metrics.winning_trades = 0; m_metrics.losing_trades = 0; m_metrics.gross_profit = 0; m_metrics.gross_loss = 0; //--- Process all trades for(int i = 0; i < m_history_size; i++) { double net_profit = m_history[i].profit + m_history[i].commission + m_history[i].swap; if(net_profit > 0) { m_metrics.winning_trades++; m_metrics.gross_profit += net_profit; if(net_profit > m_metrics.largest_win) m_metrics.largest_win = net_profit; } else { m_metrics.losing_trades++; m_metrics.gross_loss += MathAbs(net_profit); if(MathAbs(net_profit) > m_metrics.largest_loss) m_metrics.largest_loss = MathAbs(net_profit); } } //--- Calculate derived metrics m_metrics.net_profit = m_metrics.gross_profit - m_metrics.gross_loss; if(m_metrics.total_trades > 0) { m_metrics.win_rate = (double)m_metrics.winning_trades / m_metrics.total_trades * 100; m_metrics.expectancy = m_metrics.net_profit / m_metrics.total_trades; } if(m_metrics.winning_trades > 0) m_metrics.avg_win = m_metrics.gross_profit / m_metrics.winning_trades; if(m_metrics.losing_trades > 0) m_metrics.avg_loss = m_metrics.gross_loss / m_metrics.losing_trades; if(m_metrics.gross_loss > 0) m_metrics.profit_factor = m_metrics.gross_profit / m_metrics.gross_loss; else m_metrics.profit_factor = m_metrics.gross_profit > 0 ? 999.99 : 0; if(m_metrics.avg_loss > 0) m_metrics.risk_reward_ratio = m_metrics.avg_win / m_metrics.avg_loss; } //+------------------------------------------------------------------+ //| Update advanced metrics | //+------------------------------------------------------------------+ void CPerformanceTracker::UpdateAdvancedMetrics() { if(m_history_size == 0) return; //--- Consecutive tracking int current_wins = 0; int current_losses = 0; m_metrics.max_consecutive_wins = 0; m_metrics.max_consecutive_losses = 0; //--- Trade duration double total_duration = 0; double win_duration = 0; double loss_duration = 0; int win_count = 0; int loss_count = 0; for(int i = 0; i < m_history_size; i++) { double net_profit = m_history[i].profit + m_history[i].commission + m_history[i].swap; //--- Consecutive tracking if(net_profit > 0) { current_wins++; current_losses = 0; if(current_wins > m_metrics.max_consecutive_wins) m_metrics.max_consecutive_wins = current_wins; win_duration += m_history[i].duration_bars; win_count++; } else { current_losses++; current_wins = 0; if(current_losses > m_metrics.max_consecutive_losses) m_metrics.max_consecutive_losses = current_losses; loss_duration += m_history[i].duration_bars; loss_count++; } total_duration += m_history[i].duration_bars; } //--- Average durations if(m_history_size > 0) m_metrics.avg_trade_duration = total_duration / m_history_size; if(win_count > 0) m_metrics.win_duration = win_duration / win_count; if(loss_count > 0) m_metrics.loss_duration = loss_duration / loss_count; } //+------------------------------------------------------------------+ //| Calculate Sharpe ratio | //+------------------------------------------------------------------+ double CPerformanceTracker::CalculateSharpe() { if(m_curve_size < 30) return 0; // Need sufficient data //--- Calculate returns ArrayResize(m_returns, m_curve_size - 1); for(int i = 1; i < m_curve_size; i++) { if(m_equity_curve[i-1] > 0) m_returns[i-1] = (m_equity_curve[i] - m_equity_curve[i-1]) / m_equity_curve[i-1]; else m_returns[i-1] = 0; } //--- Calculate mean and std dev double mean = MathMean(m_returns); double std_dev = MathStandardDeviation(m_returns); if(std_dev == 0) return 0; //--- Annualize (assuming daily returns) double annual_return = mean * 252; double annual_std = std_dev * MathSqrt(252); return (annual_return - m_risk_free_rate) / annual_std; } //+------------------------------------------------------------------+ //| Update drawdown metrics | //+------------------------------------------------------------------+ void CPerformanceTracker::UpdateDrawdownMetrics() { if(m_curve_size == 0) return; double peak = m_equity_curve[0]; m_metrics.max_drawdown_amount = 0; m_metrics.max_drawdown_percent = 0; for(int i = 1; i < m_curve_size; i++) { if(m_equity_curve[i] > peak) peak = m_equity_curve[i]; double drawdown = peak - m_equity_curve[i]; double drawdown_percent = (peak > 0) ? drawdown / peak * 100 : 0; if(drawdown > m_metrics.max_drawdown_amount) { m_metrics.max_drawdown_amount = drawdown; m_metrics.max_drawdown_percent = drawdown_percent; } } //--- Current drawdown double current_balance = AccountInfoDouble(ACCOUNT_BALANCE); if(current_balance < peak) { m_metrics.current_drawdown = (peak - current_balance) / peak * 100; } else { m_metrics.current_drawdown = 0; } //--- Recovery factor if(m_metrics.max_drawdown_amount > 0) m_metrics.recovery_factor = m_metrics.net_profit / m_metrics.max_drawdown_amount; } //+------------------------------------------------------------------+ //| Update equity curve | //+------------------------------------------------------------------+ void CPerformanceTracker::UpdateEquityCurve(double current_balance) { //--- Shift array if full if(m_curve_size >= ArraySize(m_equity_curve)) { for(int i = 0; i < m_curve_size - 1; i++) m_equity_curve[i] = m_equity_curve[i + 1]; m_curve_size--; } //--- Add new value m_equity_curve[m_curve_size] = current_balance; m_curve_size++; } //+------------------------------------------------------------------+ //| Calculate all ratios | //+------------------------------------------------------------------+ void CPerformanceTracker::CalculateRatios() { m_metrics.sharpe_ratio = CalculateSharpe(); m_metrics.sortino_ratio = CalculateSortino(); m_metrics.calmar_ratio = CalculateCalmar(); } //+------------------------------------------------------------------+ //| Calculate Sortino ratio (downside deviation) | //+------------------------------------------------------------------+ double CPerformanceTracker::CalculateSortino() { if(m_curve_size < 30) return 0; //--- Calculate downside returns only double negative_returns[]; int neg_count = 0; for(int i = 0; i < ArraySize(m_returns); i++) { if(m_returns[i] < 0) { ArrayResize(negative_returns, neg_count + 1); negative_returns[neg_count] = m_returns[i]; neg_count++; } } if(neg_count == 0) return 999.99; // No downside double mean_return = MathMean(m_returns); double downside_dev = MathStandardDeviation(negative_returns); if(downside_dev == 0) return 999.99; //--- Annualize double annual_return = mean_return * 252; double annual_downside = downside_dev * MathSqrt(252); return (annual_return - m_risk_free_rate) / annual_downside; } //+------------------------------------------------------------------+ //| Calculate Calmar ratio | //+------------------------------------------------------------------+ double CPerformanceTracker::CalculateCalmar() { if(m_metrics.max_drawdown_percent == 0) return 0; //--- Annualized return int days = GetTradingDays(); if(days == 0) return 0; double annual_return = (m_metrics.net_profit / m_daily_starting_balance) * (252.0 / days) * 100; return annual_return / m_metrics.max_drawdown_percent; } //+------------------------------------------------------------------+ //| Get trading days | //+------------------------------------------------------------------+ int CPerformanceTracker::GetTradingDays() { if(m_history_size == 0) return 0; datetime first_trade = m_history[0].open_time; datetime last_trade = m_history[m_history_size - 1].close_time; int days = (int)((last_trade - first_trade) / 86400); //--- Approximate trading days (exclude weekends) return (int)(days * 5.0 / 7.0); } //+------------------------------------------------------------------+ //| Get strategy breakdown | //+------------------------------------------------------------------+ void CPerformanceTracker::GetStrategyBreakdown(string &strategies[], double &profits[]) { //--- Temporary storage string temp_strategies[]; double temp_profits[]; int strategy_count = 0; //--- Aggregate by strategy for(int i = 0; i < m_history_size; i++) { string strategy = m_history[i].strategy; double profit = m_history[i].profit + m_history[i].commission + m_history[i].swap; //--- Find or add strategy int index = -1; for(int j = 0; j < strategy_count; j++) { if(temp_strategies[j] == strategy) { index = j; break; } } if(index >= 0) { temp_profits[index] += profit; } else { ArrayResize(temp_strategies, strategy_count + 1); ArrayResize(temp_profits, strategy_count + 1); temp_strategies[strategy_count] = strategy; temp_profits[strategy_count] = profit; strategy_count++; } } //--- Copy to output arrays ArrayResize(strategies, strategy_count); ArrayResize(profits, strategy_count); for(int i = 0; i < strategy_count; i++) { strategies[i] = temp_strategies[i]; profits[i] = temp_profits[i]; } } //+------------------------------------------------------------------+ //| Get summary report | //+------------------------------------------------------------------+ string CPerformanceTracker::GetSummaryReport() { GetMetrics(); // Ensure metrics are current string report = "=== PERFORMANCE SUMMARY ===\n"; report += StringFormat("Total Trades: %d (Win: %d, Loss: %d)\n", m_metrics.total_trades, m_metrics.winning_trades, m_metrics.losing_trades); report += StringFormat("Win Rate: %.2f%%\n", m_metrics.win_rate); report += StringFormat("Net Profit: $%.2f\n", m_metrics.net_profit); report += StringFormat("Profit Factor: %.2f\n", m_metrics.profit_factor); report += StringFormat("Expectancy: $%.2f\n", m_metrics.expectancy); report += StringFormat("Sharpe Ratio: %.2f\n", m_metrics.sharpe_ratio); report += StringFormat("Max Drawdown: %.2f%%\n", m_metrics.max_drawdown_percent); report += StringFormat("Recovery Factor: %.2f\n", m_metrics.recovery_factor); return report; } #endif // PERFORMANCE_TRACKER_MQH