#include // https://www.mql5.com/ru/code/22577 enum ENUM_VIRTUALTESTER_TYPE { VIRTUALTESTER_CLASSIC, // Classic VIRTUALTESTER_APART // Apart }; class VIRTUAL_TESTER { private: string Symbols[]; TICKS_ARRAY TicksArray[]; ulong LengthGetTicks[]; int AmountTicks; datetime FromTicks; datetime ToTicks; datetime From; datetime To; double Balance; ulong Length; double Result; static void StringTrim( string &Str ) { ::StringTrimLeft(Str); ::StringTrimRight(Str); return; } static string LengthToString( const datetime Length ) { const int Days = (int)(Length / (24 * 3600)); return(((Days) ? (string)Days + "d ": "") + ::TimeToString(Length, TIME_SECONDS)); } void Parse( const string &sSymbols ) { ::ArrayFree(this.Symbols); for (int i = ::StringSplit(sSymbols, ',', this.Symbols) - 1; i >= 0; i--) VIRTUAL_TESTER::StringTrim(this.Symbols[i]); return; } virtual int Check( void ) { const int Size = ::ArraySize(this.Symbols); int Amount = 0; bool Custom = false; for (int i = 0; i < Size; i++) // if (::SymbolInfoInteger(this.Symbols[i], SYMBOL_EXIST)) // VIRTUAL_NOCHECK_NULL if (::SymbolExist(this.Symbols[i], Custom)) this.Symbols[Amount++] = this.Symbols[i]; else ::Alert("Symbol " + this.Symbols[i] + " is not exist!"); return(::ArrayResize(this.Symbols, Amount)); } int GetSymbols( const string &sSymbols ) { this.Parse(sSymbols); return(this.Check()); } void ToNull( void ) { this.Length = 0; this.Result = 0; return; } protected: ulong GetSumLengthGetTick( void ) const { ulong Sum = 0; for (uint i = ::ArraySize(this.LengthGetTicks); (bool)i--;) Sum += this.LengthGetTicks[i]; return(Sum); } virtual double CalcOnTester( void ) { this.Result = ::IsStopped() ? -DBL_MAX : ::TesterStatistics(STAT_PROFIT); return(this.Result); } public: VIRTUAL_TESTER( const string sSymbols, const double dBalance = DBL_MIN ) : From(INT_MAX), To(0) { this.SetSymbols(sSymbols); this.SetBalance(dBalance); } bool SetSymbols( const string sSymbols ) { string PrevSymbols[]; ::ArraySwap(PrevSymbols, this.Symbols); const bool Res = this.GetSymbols(sSymbols); if (Res) this.FreeTicks(); else ::ArraySwap(PrevSymbols, this.Symbols); return(Res); } bool SetBalance( const double dBalance = DBL_MIN ) { const bool Res = (dBalance > 0); if (Res) { this.Balance = dBalance; this.ToNull(); } return(Res); } bool SetInterval( const datetime dFrom, const datetime dTo = INT_MAX) { const bool Res = (dFrom < dTo); if (Res) { this.FreeTicks(); this.From = dFrom; this.To = dTo; } return(Res); } void FreeTicks( void ) { ::ArrayFree(this.TicksArray); ::ArrayFree(this.LengthGetTicks); this.AmountTicks = 0; this.FromTicks = INT_MAX; this.ToTicks = 0; this.ToNull(); return; } virtual int GetTicks( void ) { this.FreeTicks(); if (this.From != INT_MAX) { ::Print("Getting Ticks (" + (string)this.From + " - " + (string)this.To + ")..."); ::ArrayPrint(this.Symbols); const int Size = ::ArrayResize(this.LengthGetTicks, ::ArrayResize(this.TicksArray, ::ArraySize(this.Symbols))); for (int i = 0; i < Size; i++) { this.LengthGetTicks[i] = ::GetMicrosecondCount(); const int Amount = ::CopyTicksRange(Symbols[i], TicksArray[i].Ticks, COPY_TICKS_ALL, (long)this.From * 1000, (long)this.To * 1000 - 1); this.LengthGetTicks[i] = ::GetMicrosecondCount() - this.LengthGetTicks[i]; if (Amount > 0) { this.FromTicks = ::MathMin(this.FromTicks, TicksArray[i].Ticks[0].time); this.ToTicks = ::MathMax(this.ToTicks, TicksArray[i].Ticks[Amount - 1].time); ::Print((string)(i + 1) + ". " + this.Symbols[i] + ": " + (string)Amount + " ticks (" + (string)TicksArray[i].Ticks[0].time + " - " + (string)TicksArray[i].Ticks[Amount - 1].time + ")"); this.AmountTicks += Amount; } } } return(this.AmountTicks); } int Run( const STRATEGY_MULTI StrategyMulti, const bool StopFlag = true, const bool Netting = false, const ENUM_VIRTUALTESTER_TYPE Type = VIRTUALTESTER_CLASSIC ) { if (!AmountTicks) this.GetTicks(); ::Print("Calculating (" + ::StringSubstr(::EnumToString(Type), ::StringLen("VIRTUALTESTER_")) + ") " + (string)this.AmountTicks + " ticks..."); this.Length = ::GetMicrosecondCount(); this.Result = 0; const int Res = (Type == VIRTUALTESTER_CLASSIC) ? VIRTUAL::TesterMulti(this.Symbols, this.TicksArray, StrategyMulti, this.Balance, StopFlag, Netting) : VIRTUAL::TesterMultiApart(this.Symbols, this.TicksArray, StrategyMulti, this.Balance, StopFlag, Netting); if (Res != -1) { this.CalcOnTester(); this.Length = ::GetMicrosecondCount() - this.Length; } else this.Length = 0; return(Res); } double OnTester( void ) const { return(this.Result); } double GetPerformance( void ) const { return(this.Length ? 1e6 * this.AmountTicks / this.Length : 0); } ulong GetLength( void ) const { return(this.Length); } static string LengthToString( const long Length ) { return(VIRTUAL_TESTER::LengthToString((datetime)(Length / 1000)) + "." + ::IntegerToString(Length % 1000, 3, '0')); } static string NumToString( double dVolume, const int PosLen ) { static int Len[] = {0, 0, 0, 0, 0, 0}; const string Sign = (dVolume < 0) ? "-" : NULL; dVolume = ::MathAbs(dVolume) + 0.001; long Round = (long)dVolume; string Res = ((bool)(Round / 1000) ? ::IntegerToString(Round % 1000, 3, '0') : (string)(Round % 1000)) + "." + ::IntegerToString((long)(dVolume * 10) % 10, 1, '0'); Round /= 1000; while (Round) { Res = ((bool)(Round / 1000) ? ::IntegerToString(Round % 1000, 3, '0') : (string)(Round % 1000)) + " " + Res; Round /= 1000; } Res = Sign + Res; const int Length = ::StringLen(Res); string TmpStr = NULL; if (Length > Len[PosLen]) Len[PosLen] = Length; else ::StringInit(TmpStr, Len[PosLen] - Length, ' '); return(TmpStr + Res); } virtual string ToString( void ) const { string Str = NULL; if (this.Length) { Str += "final balance " + ::DoubleToString(::AccountInfoDouble(ACCOUNT_BALANCE), 2) + " pips"; Str += "\nOnTester result " + (string)this.Result; Str += "\nTest passed in " + VIRTUAL_TESTER::LengthToString((this.Length + this.GetSumLengthGetTick()) / 1000) + " (including ticks preprocessing " + VIRTUAL_TESTER::LengthToString(this.GetSumLengthGetTick() / 1000) + ")."; Str += "\ntotal ticks for all symbols " + (string)this.AmountTicks + " (" + (string)this.FromTicks + " - " + (string)this.ToTicks + ")"; const int Size = ::ArraySize(this.LengthGetTicks); for (int i = 0; i < Size; i++) Str += "\n" + this.Symbols[i] + ": generate " + (string)::ArraySize(this.TicksArray[i].Ticks) + " ticks in " + VIRTUAL_TESTER::LengthToString(this.LengthGetTicks[i] / 1000); Str += "\nPerformance: " + VIRTUAL_TESTER::NumToString(this.GetPerformance(), 3) + " ticks/sec., " + VIRTUAL_TESTER::LengthToString(this.Length / 1000); } return(Str); } };