forked from rosh/MQL5Book
300 lines
8.8 KiB
MQL5
300 lines
8.8 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| TradeReport.mqh |
| |||
//| Copyright (c) 2022, MetaQuotes Ltd. |
| |||
//| https://www.mql5.com |
| |||
//+------------------------------------------------------------------+
| |||
#include "DealFilter.mqh"
| |||
#include "AutoPtr.mqh"
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Main class for trade statistics calculation |
| |||
//+------------------------------------------------------------------+
| |||
class TradeReport
| |||
{
| |||
double balance; // on-the-fly totaling
| |||
double floating; // on-the-fly change
| |||
| |||
double data[]; // entire period balance curve
| |||
datetime moments[];
| |||
| |||
public:
| |||
struct DrawDown
| |||
{
| |||
double maxpeak;
| |||
double minpeak;
| |||
| |||
double
| |||
series_start,
| |||
series_min,
| |||
series_dd,
| |||
series_dd_percent,
| |||
series_dd_relative_percent,
| |||
series_dd_relative;
| |||
| |||
DrawDown()
| |||
{
| |||
reset();
| |||
}
| |||
| |||
void reset()
| |||
{
| |||
ZeroMemory(this);
| |||
series_min = DBL_MAX;
| |||
}
| |||
| |||
void calcDrawdown(const double &data[])
| |||
{
| |||
reset();
| |||
for(int i = 0; i < ArraySize(data); ++i)
| |||
{
| |||
calcDrawdown(data[i]);
| |||
}
| |||
}
| |||
| |||
void calcDrawdown(const double amount)
| |||
{
| |||
if(series_start == 0.0) series_start = amount;
| |||
if(amount < series_min) series_min = amount;
| |||
| |||
if(maxpeak == 0.0) maxpeak = amount;
| |||
if(minpeak == 0.0) minpeak = amount;
| |||
| |||
// check of extremum condition
| |||
if(amount > maxpeak || amount < minpeak)
| |||
{
| |||
if(amount > maxpeak) maxpeak = amount;
| |||
minpeak = amount;
| |||
| |||
const double drawdown = maxpeak - minpeak;
| |||
const double drawdown_percent = drawdown / maxpeak * 100.0;
| |||
| |||
if(series_dd_relative_percent < drawdown_percent)
| |||
{
| |||
series_dd_relative_percent = drawdown_percent;
| |||
series_dd_relative = drawdown;
| |||
}
| |||
| |||
if(series_dd < drawdown)
| |||
{
| |||
series_dd = drawdown;
| |||
series_dd_percent = drawdown_percent;
| |||
}
| |||
}
| |||
}
| |||
| |||
void print() const
| |||
{
| |||
DrawDown temp[1];
| |||
temp[0] = this;
| |||
ArrayPrint(temp, 2);
| |||
}
| |||
};
| |||
| |||
struct GenericStats: public DrawDown
| |||
{
| |||
long deals;
| |||
long trades;
| |||
long buy_trades;
| |||
long wins;
| |||
long buy_wins;
| |||
long sell_wins;
| |||
| |||
double profits;
| |||
double losses;
| |||
double net;
| |||
double pf;
| |||
double average_trade;
| |||
double recovery;
| |||
| |||
double max_profit;
| |||
double max_loss;
| |||
| |||
double sharpe;
| |||
| |||
GenericStats()
| |||
{
| |||
ZeroMemory(this);
| |||
}
| |||
| |||
void fillByTester()
| |||
{
| |||
deals = (long)TesterStatistics(STAT_DEALS);
| |||
trades = (long)TesterStatistics(STAT_TRADES);
| |||
buy_trades = (long)TesterStatistics(STAT_LONG_TRADES);
| |||
wins = (long)TesterStatistics(STAT_PROFIT_TRADES);
| |||
buy_wins = (long)TesterStatistics(STAT_PROFIT_LONGTRADES);
| |||
sell_wins = (long)TesterStatistics(STAT_PROFIT_SHORTTRADES);
| |||
| |||
profits = TesterStatistics(STAT_GROSS_PROFIT);
| |||
losses = TesterStatistics(STAT_GROSS_LOSS);
| |||
net = TesterStatistics(STAT_PROFIT);
| |||
pf = TesterStatistics(STAT_PROFIT_FACTOR);
| |||
average_trade = TesterStatistics(STAT_EXPECTED_PAYOFF);
| |||
recovery = TesterStatistics(STAT_RECOVERY_FACTOR);
| |||
sharpe = TesterStatistics(STAT_SHARPE_RATIO);
| |||
| |||
max_profit = TesterStatistics(STAT_MAX_PROFITTRADE);
| |||
max_loss = TesterStatistics(STAT_MAX_LOSSTRADE);
| |||
| |||
series_start = TesterStatistics(STAT_INITIAL_DEPOSIT);
| |||
series_min = TesterStatistics(STAT_EQUITYMIN);
| |||
series_dd = TesterStatistics(STAT_EQUITY_DD);
| |||
series_dd_percent = TesterStatistics(STAT_EQUITYDD_PERCENT);
| |||
series_dd_relative_percent = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
| |||
series_dd_relative = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
| |||
}
| |||
| |||
void print() const
| |||
{
| |||
GenericStats temp[1];
| |||
temp[0] = this;
| |||
ArrayPrint(temp, 2);
| |||
}
| |||
};
| |||
| |||
TradeReport()
| |||
{
| |||
balance = AccountInfoDouble(ACCOUNT_BALANCE);
| |||
}
| |||
| |||
void resetFloatingPL()
| |||
{
| |||
floating = 0;
| |||
}
| |||
| |||
void addFloatingPL(const double pl)
| |||
{
| |||
floating += pl;
| |||
}
| |||
| |||
void addBalance(const double pl)
| |||
{
| |||
balance += pl;
| |||
}
| |||
| |||
double getCurrent() const
| |||
{
| |||
return balance + floating;
| |||
}
| |||
| |||
static double calcSharpe(const double &data[], const double riskFreeRate = 0)
| |||
{
| |||
const int limit = ArraySize(data);
| |||
if(limit < 3)
| |||
{
| |||
Print("Too short array for Sharpe calculation: ", limit);
| |||
return 0;
| |||
}
| |||
| |||
double AHPR = 0, Std = 0;
| |||
double HPR[];
| |||
ArrayResize(HPR, limit);
| |||
const int n = limit - 1;
| |||
for(int i = 1; i < limit; i++)
| |||
{
| |||
if(data[i - 1] != 0)
| |||
{
| |||
HPR[i - 1] = (data[i] - data[i - 1]) / data[i - 1];
| |||
AHPR += HPR[i - 1];
| |||
}
| |||
}
| |||
AHPR = AHPR / n;
| |||
| |||
for(int i = 0; i < n - 1; i++)
| |||
{
| |||
Std += (AHPR - HPR[i]) * (AHPR - HPR[i]);
| |||
}
| |||
Std = sqrt(Std / (n - 1));
| |||
if(Std == 0) return 0;
| |||
return (AHPR - riskFreeRate) / Std;
| |||
}
| |||
| |||
GenericStats calcStatistics(DealFilter &filter, const double start = 0, const datetime origin = 0, const double riskFreeRate = 0)
| |||
{
| |||
GenericStats stats;
| |||
ArrayResize(data, 0);
| |||
ArrayResize(moments, 0);
| |||
ulong tickets[];
| |||
if(!filter.select(tickets)) return stats;
| |||
| |||
balance = start;
| |||
PUSH(data, balance);
| |||
PUSH(moments, origin);
| |||
| |||
for(int i = 0; i < ArraySize(tickets); ++i)
| |||
{
| |||
DealMonitor m(tickets[i]);
| |||
if(m.get(DEAL_TYPE) == DEAL_TYPE_BALANCE) // deposit/withdrawal
| |||
{
| |||
balance += m.get(DEAL_PROFIT);
| |||
PUSH(data, balance);
| |||
PUSH(moments, (datetime)m.get(DEAL_TIME));
| |||
}
| |||
else if(m.get(DEAL_TYPE) == DEAL_TYPE_BUY || m.get(DEAL_TYPE) == DEAL_TYPE_SELL)
| |||
{
| |||
const double profit = m.get(DEAL_PROFIT) + m.get(DEAL_SWAP) + m.get(DEAL_COMMISSION) + m.get(DEAL_FEE);
| |||
balance += profit;
| |||
| |||
stats.deals++;
| |||
if(m.get(DEAL_ENTRY) == DEAL_ENTRY_OUT || m.get(DEAL_ENTRY) == DEAL_ENTRY_INOUT
| |||
|| m.get(DEAL_ENTRY) == DEAL_ENTRY_OUT_BY)
| |||
{
| |||
PUSH(data, balance);
| |||
PUSH(moments, (datetime)m.get(DEAL_TIME));
| |||
stats.trades++;
| |||
if(m.get(DEAL_TYPE) == DEAL_TYPE_SELL)
| |||
{
| |||
stats.buy_trades++; // close made in opposite direction
| |||
}
| |||
if(profit >= 0)
| |||
{
| |||
stats.wins++;
| |||
if(m.get(DEAL_TYPE) == DEAL_TYPE_BUY)
| |||
{
| |||
stats.sell_wins++;
| |||
}
| |||
else
| |||
{
| |||
stats.buy_wins++;
| |||
}
| |||
}
| |||
}
| |||
else if(!TU::Equal(profit, 0))
| |||
{
| |||
PUSH(data, balance); // entry commission (if any)
| |||
PUSH(moments, (datetime)m.get(DEAL_TIME));
| |||
}
| |||
| |||
if(profit >= 0)
| |||
{
| |||
stats.profits += profit;
| |||
stats.max_profit = fmax(profit, stats.max_profit);
| |||
}
| |||
else
| |||
{
| |||
stats.losses += profit;
| |||
stats.max_loss = fmin(profit, stats.max_loss);
| |||
}
| |||
}
| |||
}
| |||
| |||
if(stats.trades > 0)
| |||
{
| |||
stats.net = stats.profits + stats.losses;
| |||
stats.pf = -stats.losses > DBL_EPSILON ? stats.profits / -stats.losses : MathExp(10000.0); // NaN(+inf)
| |||
stats.average_trade = stats.net / stats.trades;
| |||
stats.sharpe = calcSharpe(data, riskFreeRate);
| |||
stats.calcDrawdown(data); // fills all fields from DrawDown sub-struct
| |||
stats.recovery = stats.series_dd > DBL_EPSILON ? stats.net / stats.series_dd : MathExp(10000.0);
| |||
}
| |||
| |||
return stats;
| |||
}
| |||
| |||
void getCurve(double &output[], datetime &timeline[])
| |||
{
| |||
ArraySwap(output, data);
| |||
ArraySwap(timeline, moments);
| |||
}
| |||
};
| |||
//+------------------------------------------------------------------+
|