607 lines
No EOL
20 KiB
MQL5
607 lines
No EOL
20 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 <Math/Stat/Math.mqh>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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 |