forked from rosh/MQL5Book
223 lines
8.4 KiB
MQL5
223 lines
8.4 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| TradeReportWriter.mqh |
| |
//| Copyright (c) 2022, MetaQuotes Ltd. |
| |
//| https://www.mql5.com |
| |
//+------------------------------------------------------------------+
| |
#include "TradeReport.mqh"
| |
#include "AutoPtr.mqh"
| |
| |
#resource "TradeReportPage.htm" as string ReportPageTemplate
| |
#resource "TradeReportTable.htm" as string ReportTableTemplate
| |
#resource "TradeReportSVG.htm" as string SVGBoxTemplate
| |
| |
//+------------------------------------------------------------------+
| |
//| Base abstract class to write trade stats into some storage |
| |
//+------------------------------------------------------------------+
| |
class TradeReportWriter
| |
{
| |
protected:
| |
class DataHolder
| |
{
| |
public:
| |
double data[]; // balance marks
| |
datetime when[]; // time stamps for the marks
| |
string name; // description
| |
color clr; // visible color
| |
int width; // line width
| |
TradeReport::GenericStats stats; // trade statistics
| |
};
| |
| |
AutoPtr<DataHolder> curves[];
| |
double lower, upper;
| |
datetime start, stop;
| |
| |
public:
| |
TradeReportWriter(): lower(DBL_MAX), upper(-DBL_MAX), start(0), stop(0) { }
| |
| |
virtual bool addCurve(TradeReport::GenericStats &stats,
| |
double &data[], datetime &when[], const string name,
| |
const color clr = clrNONE, const int width = 1)
| |
{
| |
if(addCurve(data, when, name, clr, width))
| |
{
| |
curves[ArraySize(curves) - 1][].stats = stats;
| |
return true;
| |
}
| |
return false;
| |
}
| |
| |
virtual bool addCurve(double &data[], datetime &when[], const string name,
| |
const color clr = clrNONE, const int width = 1)
| |
{
| |
if(ArraySize(data) == 0 || ArraySize(when) == 0) return false;
| |
if(ArraySize(data) != ArraySize(when)) return false;
| |
DataHolder *c = new DataHolder();
| |
if(!ArraySwap(data, c.data) || !ArraySwap(when, c.when))
| |
{
| |
delete c;
| |
return false;
| |
}
| |
| |
const double max = c.data[ArrayMaximum(c.data)];
| |
const double min = c.data[ArrayMinimum(c.data)];
| |
| |
lower = fmin(min, lower);
| |
upper = fmax(max, upper);
| |
if(start == 0) start = c.when[0];
| |
else if(c.when[0] != 0) start = fmin(c.when[0], start);
| |
stop = fmax(c.when[ArraySize(c.when) - 1], stop);
| |
| |
c.name = name;
| |
c.clr = clr;
| |
c.width = width;
| |
ZeroMemory(c.stats); // no stats by default
| |
PUSH(curves, c);
| |
return true;
| |
}
| |
| |
virtual void render() = 0;
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| Concrete class to write trade stats into HTML-file |
| |
//+------------------------------------------------------------------+
| |
class HTMLReportWriter: public TradeReportWriter
| |
{
| |
int handle;
| |
int width, height;
| |
| |
public:
| |
HTMLReportWriter(const string name, const int w = 600, const int h = 400):
| |
width(w), height(h)
| |
{
| |
handle = FileOpen(name,
| |
FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_REWRITE | FILE_SHARE_READ | FILE_SHARE_WRITE);
| |
}
| |
| |
~HTMLReportWriter()
| |
{
| |
if(handle != 0) FileClose(handle);
| |
}
| |
| |
void close()
| |
{
| |
if(handle != 0) FileClose(handle);
| |
handle = 0;
| |
}
| |
| |
virtual void render() override
| |
{
| |
string headerAndFooter[2];
| |
StringSplit(ReportPageTemplate, '~', headerAndFooter);
| |
FileWriteString(handle, headerAndFooter[0]);
| |
renderContent();
| |
FileWriteString(handle, headerAndFooter[1]);
| |
}
| |
| |
private:
| |
void renderContent()
| |
{
| |
renderSVG();
| |
renderTables();
| |
}
| |
| |
void renderTable(const int k, TradeReport::GenericStats &stats,
| |
const string name, const color clr)
| |
{
| |
string table = ReportTableTemplate;
| |
StringReplace(table, "%COLOR%", colorToString(k, clr));
| |
StringReplace(table, "%NAME%", name);
| |
| |
StringReplace(table, "%PROFITS%", DoubleToString(stats.profits, 2));
| |
StringReplace(table, "%PF%", DoubleToString(stats.pf, 2));
| |
StringReplace(table, "%LOSSES%", DoubleToString(stats.losses, 2));
| |
StringReplace(table, "%SHARPE%", DoubleToString(stats.sharpe, 2));
| |
StringReplace(table, "%NET%", DoubleToString(stats.profits + stats.losses, 2));
| |
StringReplace(table, "%RF%", DoubleToString(stats.recovery, 2));
| |
StringReplace(table, "%DEPOSIT%", DoubleToString(stats.series_start, 2));
| |
StringReplace(table, "%DEALS%", (string)stats.deals);
| |
| |
StringReplace(table, "%BUYWIN%", (string)stats.buy_wins);
| |
StringReplace(table, "%BUYLOSS%", (string)(stats.buy_trades - stats.buy_wins));
| |
StringReplace(table, "%BUY%", (string)stats.buy_trades);
| |
StringReplace(table, "%SELLWIN%", (string)stats.sell_wins);
| |
StringReplace(table, "%SELLLOSS%", (string)(stats.trades - stats.buy_trades - stats.sell_wins));
| |
StringReplace(table, "%SELL%", (string)(stats.trades - stats.buy_trades));
| |
StringReplace(table, "%WIN%", (string)stats.wins);
| |
StringReplace(table, "%LOSS%", (string)(stats.trades - stats.wins));
| |
StringReplace(table, "%TOTAL%", (string)stats.trades);
| |
| |
StringReplace(table, "%DD%", DoubleToString(stats.series_dd, 2));
| |
StringReplace(table, "%DD_PCT%", DoubleToString(stats.series_dd_percent, 2));
| |
StringReplace(table, "%DD_REL%", DoubleToString(stats.series_dd_relative, 2));
| |
StringReplace(table, "%DD_REL_PCT%", DoubleToString(stats.series_dd_relative_percent, 2));
| |
const double depo_dd = -fmin(stats.series_start - stats.series_min, 0);
| |
StringReplace(table, "%DD_DEPO%", DoubleToString(depo_dd, 2));
| |
StringReplace(table, "%DD_DEPO_PCT%", DoubleToString(depo_dd * 100 / stats.series_start, 2));
| |
| |
StringReplace(table, "%MAX_PROFIT%", DoubleToString(stats.max_profit, 2));
| |
StringReplace(table, "%AVERAGE%", DoubleToString(stats.average_trade, 2));
| |
StringReplace(table, "%MAX_LOSS%", DoubleToString(stats.max_loss, 2));
| |
| |
FileWriteString(handle, table);
| |
}
| |
| |
void renderTables()
| |
{
| |
for(int i = 0; i < ArraySize(curves); ++i)
| |
{
| |
renderTable(i, curves[i][].stats, curves[i][].name, curves[i][].clr);
| |
}
| |
}
| |
| |
static string colorToString(const int k, const color c)
| |
{
| |
#define NUM_COLORS 6
| |
static const color autoColor[NUM_COLORS] =
| |
{clrRed, clrLimeGreen, clrBlue, clrMagenta, clrDodgerBlue, clrOrange};
| |
string s = ColorToString(c == clrNONE ? autoColor[k % NUM_COLORS] : c, true);
| |
StringReplace(s, "clr", "");
| |
StringToLower(s);
| |
return s;
| |
#undef NUM_COLORS
| |
}
| |
| |
void renderCurve(const int k, const double &data[], const datetime &when[],
| |
const string name, const color c, const int w)
| |
{
| |
const string polyline = StringFormat(
| |
"<polyline stroke=\"%s\" stroke-width=\"%dpx\" fill=\"none\" points=\"",
| |
colorToString(k, c), w);
| |
FileWriteString(handle, polyline);
| |
const int n = ArraySize(data);
| |
for(int i = 0; i < n; ++i)
| |
{
| |
const int y = height - (int)((data[i] - lower) / fmax(upper - lower, 1) * height);
| |
const int x = (int)((when[i] - start) * width / fmax(stop - start, 1));
| |
FileWriteString(handle, (string)x + ", " + (string)y + " ");
| |
}
| |
const string polylineend = "\"/>\n";
| |
FileWriteString(handle, polylineend);
| |
| |
const string legend = "<text x=\"%d\" y=\"%d\" fill=\"%s\" class=\"legend\">%s</text>";
| |
FileWriteString(handle, StringFormat(legend, 10, 20 + k * 10, colorToString(k, c), name));
| |
}
| |
| |
void renderSVG()
| |
{
| |
string headerAndFooter[2];
| |
if(StringSplit(SVGBoxTemplate, '~', headerAndFooter) != 2) return;
| |
StringReplace(headerAndFooter[0], "%WIDTH%", (string)width);
| |
StringReplace(headerAndFooter[0], "%HEIGHT%", (string)height);
| |
FileWriteString(handle, headerAndFooter[0]);
| |
| |
for(int i = 0; i < ArraySize(curves); ++i)
| |
{
| |
renderCurve(i, curves[i][].data, curves[i][].when,
| |
curves[i][].name, curves[i][].clr, curves[i][].width);
| |
}
| |
| |
FileWriteString(handle, headerAndFooter[1]);
| |
}
| |
};
| |
//+------------------------------------------------------------------+
|