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);
| |
}
| |
};
| |
//+------------------------------------------------------------------+
|