// MQL4&5-code #property strict #define __REPORT__ #ifdef __MQL5__ #include // https://www.mql5.com/ru/code/16006 #include #else #define OP_BALANCE 6 #endif class REPORT { private: static int AmountFields; static const string Shablon; static string AddField( void ) { REPORT::AmountFields++; return(NULL); } static void Head( string &Res ) { const string AccountNumber = (string)::AccountInfoInteger(ACCOUNT_LOGIN); const string AccountName = (string)::AccountInfoString(ACCOUNT_NAME); Res = "\n" + "" + "Statement: \'" + AccountNumber + " \', " + AccountName + "\n" + "\n" + "\n" + "\n" + "\n" + "
\n" + #ifdef ERR_USER_INVALID_HANDLE // Graphics\Graphic.mqh from StdLibErr.mqh - https://www.mql5.com/ru/forum/211620#comment_5482547 ((!::MQLInfoInteger(MQL_TESTER) #ifdef __MQL5__ || ::MQLInfoInteger(MQL_OPTIMIZATION) || ::MQLInfoInteger(MQL_VISUAL_MODE) || ::MQLInfoInteger(MQL_FRAME_MODE) #endif // __MQL5__ ) ? "\n" : "") + #endif // ERR_USER_INVALID_HANDLE (::MQLInfoInteger(MQL_TESTER) ? "
ExpertName: " + ::MQLInfoString(MQL_PROGRAM_NAME) +" (" + ::Symbol() + #ifdef __MQL5__ (::SymbolInfoInteger(::Symbol(), SYMBOL_CUSTOM) ? " - Custom (" + ::SymbolInfoString(::Symbol(), SYMBOL_CURRENCY_PROFIT) + ")" : "") + #endif // __MQL5__ ")
\n" : "") + // ((::MQLInfoInteger(MQL_TESTER) && OrderSelect(0, SELECT_BY_POS, MODE_HISTORY)) ? // "
Tester Interval: " + (string)OrderOpenTime() + " - " + (string)::TimeLocal() + "
\n" : "") + #ifdef __MQL5__ (::MQLInfoInteger(MQL_OPTIMIZATION) ? "Input Parameters:" + "
Tester Pass:
\n" : "") + #endif // __MQL5__ "
Build: " + (string)::TerminalInfoInteger(TERMINAL_BUILD) + "
\n" + "
Server: " + ::AccountInfoString(ACCOUNT_SERVER) + "
\n" + "
" + ::AccountInfoString(ACCOUNT_COMPANY)+ " (by RickD) " + /**/ "
\n" + "\n" + "\n" + "" + "" + "" + "" + "\n" + "\n" + "\n" + "" + REPORT::Shablon + "\n"; } #define ADD(A, B) ::StringReplace(Str, #A, (string)(B)) static string TypeToString( const int Type ) { static const string Types[] = {"buy", "sell", "buy limit", "sell limit", "buy stop", "sell stop", "balance"}; return(((Type < ::ArraySize(Types)) ? Types[Type] : "unknown")); } static string OrderToString( const int Num ) { const int digits = (int)SymbolInfoInteger(OrderSymbol(), SYMBOL_DIGITS); const bool Balance = (OrderType() == OP_BALANCE); string Str = REPORT::Shablon; ADD(MagicNumber, (Balance || (!OrderMagicNumber())) ? "" : (string)OrderMagicNumber()); ADD(N, Num); ADD(Ticket, OrderTicket()); ADD(OpenTime, OrderOpenTime()); ADD(Type, TypeToString(OrderType())); ADD(Lots, Balance ? "" : ::DoubleToString(OrderLots(), 2)); const string Symb = Balance ? "" : OrderSymbol() #ifdef __MQL5__ + (::SymbolInfoInteger(OrderSymbol(), SYMBOL_CUSTOM) ? " (" + ::SymbolInfoString(OrderSymbol(), SYMBOL_CURRENCY_PROFIT) + ")" : "") #endif // __MQL5__ ; ADD(Symbol, Symb); ADD(OpenPrice, Balance ? "" : ::DoubleToString(OrderOpenPrice(), digits)); ADD(StopLoss, Balance ? "" : ::DoubleToString(OrderStopLoss(), digits)); ADD(TakeProfit, Balance ? "" : ::DoubleToString(OrderTakeProfit(), digits)); ADD(CloseTime, Balance ? "" : (string)OrderCloseTime()); ADD(ClosePrice, Balance ? "" : ::DoubleToString(OrderClosePrice(), digits)); ADD(Commission, (Balance || (!(bool)OrderCommission())) ? "" : DoubleToString(OrderCommission(), 2)); ADD(Swap, (Balance || (!(bool)OrderSwap())) ? "" : ::DoubleToString(OrderSwap(), 2)); ADD(Profit, ::DoubleToString(OrderProfit(), 2)); ADD(Comment, OrderComment()); return(Str); } static void AddSummary( string &Res ) { double depo = 0; double comm = 0; double swap = 0; double profit = 0; double loss = 0; double max_win = 0; double max_loss = 0; int cons_win_cnt = 0; int cons_los_cnt = 0; double cons_win_sum = 0; double cons_los_sum = 0; int mc_winners_cnt = 0; int mc_losers_cnt = 0; double mc_winners_sum = 0; double mc_losers_sum = 0; int mc_profit_cnt = 0; int mc_loss_cnt = 0; double mc_profit_sum = 0; double mc_loss_sum = 0; double max_summ_pl = 0; double min_summ_pl = 0; double max_dd = 0; double max_dd2 = 0; double val; int ind = 0; int num = 1; int pos_cnt = 0; int neg_cnt = 0; const int Total = OrdersHistoryTotal(); for (int i = 0; i < Total; i++) if (OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) { if (OrderType() == OP_BALANCE) depo += OrderProfit(); else { comm += OrderCommission(); swap += OrderSwap(); val = OrderProfit() + OrderCommission() + OrderSwap(); if (val > 0) { pos_cnt++; profit += val; max_win = ::MathMax(max_win, val); cons_los_cnt = 0; cons_los_sum = 0; cons_win_cnt++; cons_win_sum += val; if (mc_winners_cnt < cons_win_cnt) { mc_winners_cnt = cons_win_cnt; mc_winners_sum = cons_win_sum; } else if (mc_winners_cnt == cons_win_cnt) { mc_winners_sum = ::MathMax(mc_winners_sum, cons_win_sum); } if (mc_profit_sum < cons_win_sum) { mc_profit_cnt = cons_win_cnt; mc_profit_sum = cons_win_sum; } max_summ_pl = ::MathMax(max_summ_pl, profit+loss); } else if (val < 0) { neg_cnt++; loss += val; max_loss = ::MathMin(max_loss, val); cons_win_cnt = 0; cons_win_sum = 0; cons_los_cnt++; cons_los_sum += val; if (mc_losers_cnt < cons_los_cnt) { mc_losers_cnt = cons_los_cnt; mc_losers_sum = cons_los_sum; } else if (mc_losers_cnt == cons_los_cnt) { mc_losers_sum = ::MathMin(mc_losers_sum, cons_los_sum); } if (mc_loss_sum > cons_los_sum) { mc_loss_cnt = cons_los_cnt; mc_loss_sum = cons_los_sum; } min_summ_pl = ::MathMin(min_summ_pl, profit+loss); if (max_dd < max_summ_pl-(profit+loss)) { max_dd = max_summ_pl-(profit+loss); if (depo+max_summ_pl <= 0) max_dd2 = 100; else max_dd2 = 100*max_dd/(depo+max_summ_pl); } } } } string Str = REPORT::Shablon; ADD(MagicNumber, ""); ADD(N, ""); ADD(Ticket, ""); ADD(OpenTime, ""); ADD(Type, ""); ADD(Lots, ""); ADD(Symbol, ""); ADD(OpenPrice, ""); ADD(StopLoss, ""); ADD(TakeProfit, ""); ADD(CloseTime, ""); ADD(ClosePrice, ""); ADD(Commission, (comm) ? ::DoubleToString(comm, 2) : ""); ADD(Swap, ::DoubleToString(swap, 2)); ADD(Profit, ::DoubleToString(profit + loss, 2)); ADD(Comment, ""); #undef ADD Res += "" + Str + "\n" + REPORT::WriteTotal("Deposit/Withdrawal:", ::DoubleToString(depo, 2)) + REPORT::WriteTotal("Summary P/L:", ::DoubleToString(profit+loss, 2)) + REPORT::WriteTotal("Balance:", ::DoubleToString(depo+profit+loss, 2)) + "\n" + REPORT::WriteTotal("Winning trades:", "("+(string)pos_cnt+") " + ::DoubleToString(profit, 2)) + REPORT::WriteTotal("Losing trades:", "("+(string)neg_cnt+") " + ::DoubleToString(loss, 2)) + REPORT::WriteTotal("Max summary P/L:", ::DoubleToString(max_summ_pl, 2)) + REPORT::WriteTotal("Largest winning trade:", ::DoubleToString(max_win, 2)) + REPORT::WriteTotal("Largest losing trade:", ::DoubleToString(max_loss, 2)) + REPORT::WriteTotal("Max consecutive winners:", (string)mc_winners_cnt +" ("+ ::DoubleToString(mc_winners_sum, 2) +")") + REPORT::WriteTotal("Max consecutive losers:", (string)mc_losers_cnt +" ("+ ::DoubleToString(mc_losers_sum, 2) +")") + REPORT::WriteTotal("Max consecutive profit:", ::DoubleToString(mc_profit_sum, 2) +" ("+ (string)mc_profit_cnt +")") + REPORT::WriteTotal("Max consecutive loss:", ::DoubleToString(mc_loss_sum, 2) +" ("+ (string)mc_loss_cnt +")") + REPORT::WriteTotal("Absolute drawdown:", "*") + REPORT::WriteTotal("Max drawdown:", ::DoubleToString(max_dd, 2) +" ("+ ::DoubleToString(max_dd2, 2) +"%)") + REPORT::WriteTotal("Profit factor:", loss ? ::DoubleToString(::MathAbs(profit/loss), 2) : "*") + REPORT::WriteTotal("Avg. profit factor:", (bool)(loss * pos_cnt) ? ::DoubleToString(::MathAbs((profit*neg_cnt)/(loss*pos_cnt)), 2) : "*") + REPORT::WriteTotal("Risk factor:", max_dd ? ::DoubleToString((profit+loss)/max_dd, 2) : "*") + "\n"; return; } static void Body( string &Res ) { const int Total = OrdersHistoryTotal(); for (int i = 0; i < Total; i++) if (OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) Res += "" + OrderToString(i) + "\n"; REPORT::AddSummary(Res); } static string WriteTotal( string text, string val ) { static int t_ind = 0; int cols = 5; string res = ""; cols = REPORT::AmountFields - cols - 1; res = res + ""; res = res + ""; res = res + "\n"; t_ind++; return(res); } static void Tail( string &Res ) { Res += "
A/C No: " + AccountNumber + " (" + ::AccountInfoString(ACCOUNT_CURRENCY) +")Name: " + AccountName + "" + (string)::TimeLocal() + " (local time)
 
Closed Transactions:
 
 
" + text + "" + val + "
\n" + "
* * *
\n" + "
\n" + "\n"; } #ifdef __MQL5__ // закомментировать, если нужно https://www.mql5.com/ru/forum/211620#comment_5482547 static string GraphPlot( const double &Y[], int Width = 0, int Height = 0, const ENUM_CURVE_TYPE Type = CURVE_LINES, string ObjName = NULL ) { Width = Width ? Width : (int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); Height = Height ? Height : (int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); ObjName = (ObjName == NULL) ? __FUNCTION__ : ObjName; CGraphic Graphic; const bool Res = (::ObjectFind(0, ObjName) >= 0) ? Graphic.Attach(0, ObjName) : Graphic.Create(0, ObjName, 0, 0, 0, Width, Height); if (Res) { Graphic.CurveAdd(Y, Type); Graphic.CurvePlotAll(); Graphic.Update(); } return (Res ? Graphic.ChartObjectName() : NULL); } static bool BitmapObjectToFile( const long chartID, const string ObjName, const string FileName ) { bool Res = (::ObjectFind(chartID, ObjName) >= 0) && ((ENUM_OBJECT)::ObjectGetInteger(chartID, ObjName, OBJPROP_TYPE) == OBJ_BITMAP_LABEL); if (Res) { const bool DateScale = (bool)::ChartGetInteger(chartID, CHART_SHOW_DATE_SCALE); const bool PriceScale = (bool)::ChartGetInteger(chartID, CHART_SHOW_PRICE_SCALE); ::ChartSetInteger(chartID, CHART_SHOW_DATE_SCALE, false); ::ChartSetInteger(chartID, CHART_SHOW_PRICE_SCALE, false); const int X = (int)::ObjectGetInteger(chartID, ObjName, OBJPROP_XDISTANCE); const int Y = (int)::ObjectGetInteger(chartID, ObjName, OBJPROP_YDISTANCE); ::ObjectSetInteger(chartID, ObjName, OBJPROP_XDISTANCE, 0); ::ObjectSetInteger(chartID, ObjName, OBJPROP_YDISTANCE, 0); const int Width = (int)::ObjectGetInteger(chartID, ObjName, OBJPROP_XSIZE); const int Height = (int)::ObjectGetInteger(chartID, ObjName, OBJPROP_YSIZE); Res = ::ChartScreenShot(chartID, FileName, Width, Height, ALIGN_LEFT); ::ObjectSetInteger(chartID, ObjName, OBJPROP_XDISTANCE, X); ::ObjectSetInteger(chartID, ObjName, OBJPROP_YDISTANCE, Y); ::ChartSetInteger(chartID, CHART_SHOW_DATE_SCALE, DateScale); ::ChartSetInteger(chartID, CHART_SHOW_PRICE_SCALE, PriceScale); ::ChartRedraw(chartID); } return(Res); } #endif // __MQL5__ // закомментировать, если нужно https://www.mql5.com/ru/forum/211620#comment_5482547 public: static int ToString( string &Res ) { REPORT::Head(Res); REPORT::Body(Res); REPORT::Tail(Res); return(::StringLen(Res)); } static string ToString( void ) { string Res; REPORT::ToString(Res); return(Res); } static bool ToFile( const string FileName ) { const int handle = ::FileOpen(FileName, FILE_WRITE | FILE_TXT | FILE_ANSI); const bool Res = (handle != INVALID_HANDLE); if (Res) { string Str; REPORT::ToString(Str); #ifdef __MQL5__ // закомментировать, если нужно https://www.mql5.com/ru/forum/211620#comment_5482547 if (::MQLInfoInteger(MQL_VISUAL_MODE) || (!::MQLInfoInteger(MQL_TESTER))) { string ImageName = FileName; int Pos = 0; while ((Pos = ::StringFind(ImageName, "\\")) >= 0) ImageName = ::StringSubstr(ImageName, Pos + 1); ImageName += ".png"; ::StringReplace(Str, "::IMAGENAME::", ImageName); const string ObjName = REPORT::ToChart(1025, 400, CURVE_LINES, FileName + ".png"); if (ObjName != NULL) { ::ObjectDelete(0, ObjName); ::ChartRedraw(0); } } #endif // __MQL5__ // закомментировать, если нужно https://www.mql5.com/ru/forum/211620#comment_5482547 ::FileWrite(handle, Str); ::FileClose(handle); } return(Res); } static int GetBalanceHistory( double &Balance[] ) { int Amount = 0; double Sum = 0; const int Total = OrdersHistoryTotal(); ::ArrayResize(Balance, Total); for (int i = 0; i < Total; i++) if (OrderSelect(i, SELECT_BY_POS, MODE_HISTORY) && ((OrderType() <= OP_SELL) || (OrderType() == OP_BALANCE))) { Sum += OrderProfit() + OrderCommission() + OrderSwap(); Balance[Amount] = Sum; Amount++; } return(::ArrayResize(Balance, Amount)); } #ifdef __MQL5__ // закомментировать, если нужно https://www.mql5.com/ru/forum/211620#comment_5482547 static string ToChart( const double &Y[], const int Width = 0, const int Height = 0, const ENUM_CURVE_TYPE Type = CURVE_POINTS_AND_LINES, const string FileName = NULL ) { const string ObjName = REPORT::GraphPlot(Y, Width, Height, Type); if ((ObjName != NULL) && (FileName != NULL)) REPORT::BitmapObjectToFile(0, ObjName, FileName); return(ObjName); } static string ToChart( const int Width = 0, const int Height = 0, const ENUM_CURVE_TYPE Type = CURVE_POINTS_AND_LINES, const string FileName = NULL ) { double Balance[]; REPORT::GetBalanceHistory(Balance); return(REPORT::ToChart(Balance, Width, Height, Type, FileName)); } #endif // __MQL5__ // закомментировать, если нужно https://www.mql5.com/ru/forum/211620#comment_5482547 }; #define FIELD(A) ("" + #A + "" + REPORT::AddField()) int REPORT::AmountFields = 0; const string REPORT:: Shablon = FIELD(N) + FIELD(Ticket) + FIELD(OpenTime) + FIELD(Type) + FIELD(Lots) + FIELD(Symbol) + FIELD(OpenPrice) + FIELD(StopLoss) + FIELD(TakeProfit) + FIELD(CloseTime) + FIELD(ClosePrice) + FIELD(Commission) + FIELD(Swap) + FIELD(Profit) + FIELD(Comment) + FIELD(MagicNumber); #ifdef __MQL5__ #ifdef REPORT_TESTER sinput bool Report = true; // ¬кл/выкл. запись стейтментов в тестере #define PATH_FILES (::TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\") #define PATH_REPORT ("Reports\\" + ::MQLInfoString(MQL_PROGRAM_NAME)) class DEINIT_REPORT { public: ~DEINIT_REPORT( void ) { if (::Report) { #ifndef __TESTERBENCH__ if (::MQLInfoInteger(MQL_OPTIMIZATION)) { string Str; REPORT::ToString(Str); double Balance[]; REPORT::GetBalanceHistory(Balance); #ifdef __TYPETOBYTES__ CONTAINER Container; Container[0] = Str; Container[1] = Balance; ::FrameAdd(NULL, 0, ::AccountInfoDouble(ACCOUNT_BALANCE), Container.Data); #else // __TYPETOBYTES__ uchar Data[]; ::StringToCharArray(Str, Data); ::FrameAdd(NULL, 0, ::AccountInfoDouble(ACCOUNT_BALANCE), Data); #endif // __TYPETOBYTES__ } else #endif // __TESTERBENCH__ if (::MQLInfoInteger(MQL_TESTER)) { const string FileName = PATH_REPORT + "_Report.htm"; if (REPORT::ToFile(FileName)) ::Print(PATH_FILES + FileName + " - Done!"); } } } }; #ifndef __TESTERBENCH__ string GetDiapason( const string Name ) { bool Enable; long lValue; long lStart; long lStep; long lStop; double dValue; double dStart; double dStep; double dStop; return((::ParameterGetRange(Name, Enable, lValue, lStart, lStep, lStop) && Enable && ::ParameterGetRange(Name, Enable, dValue, dStart, dStep, dStop)) ? " (" +(((lStart != dStart) || (lStep != dStep) || (lStop != dStop)) ? ((string)dStart + ", " + (string)dStep + ", " + (string)dStop) : ((string)lStart + ", " + (string)lStep + ", " + (string)lStop)) + ")" : ""); } void OnTesterPass( void ) { if (::Report) { ulong Pass; string Name; long ID; double Value; #ifdef __TYPETOBYTES__ CONTAINER Container; while (::FrameNext(Pass, Name, ID, Value, Container.Data)) { const int handle = ::FileOpen(PATH_REPORT + "\\" + ::IntegerToString(Pass, 5, '0') + ".htm", FILE_WRITE | FILE_TXT | FILE_ANSI); if (handle != INVALID_HANDLE) { const string ImageName = ::IntegerToString(Pass, 5, '0') + ".png"; string Str; Container[0].Get(Str); ::StringReplace(Str, "::IMAGENAME::", ImageName); string Parameters[]; int Count; if (::FrameInputs(Pass, Parameters, Count)) { string StrParam = "
Input Parameters:
\n"; for (int i = 0; i < Count; i++) if (Parameters[i] != "Report=1") StrParam += "
" + Parameters[i] + ::GetDiapason(::StringSubstr(Parameters[i], 0, ::StringFind(Parameters[i], "="))) + "
\n"; ::StringReplace(Str, "Input Parameters:", StrParam); ::StringReplace(Str, "Tester Pass:", "Tester Pass: " + (string)Pass); } ::FileWrite(handle, Str); ::FileClose(handle); double Balance[]; Container[1].Get(Balance); REPORT::ToChart(Balance, 1025, 400, CURVE_LINES, PATH_REPORT + "\\" + ImageName); } } #else // __TYPETOBYTES__ uchar Data[]; while (::FrameNext(Pass, Name, ID, Value, Data)) ::FileSave(PATH_REPORT + "\\" + ::IntegerToString(Pass, 5, '0') + ".htm", Data); #endif // __TYPETOBYTES__ } return; } void OnTesterInit( void ) { if (::Report) ::Comment("Optimization..."); else ::ChartClose(); return; } void OnTesterDeinit( void ) { if (::Report) { ::Print(PATH_FILES + PATH_REPORT + "\\*.* - Done!"); ::ChartClose(); } return; } DEINIT_REPORT DeinitReport; #endif // __TESTERBENCH__ #undef PATH_REPORT #undef PATH_FILES #endif // REPORT_TESTER #endif // __MQL5__