#define RESERVE_HISTORY_ORDERS 2000 #include "Order.mqh" class HISTORYORDERS { private: // HISTORYORDERS::Swap int StartPos; int LastPos; int PrevAmountHistoryOrders; int TradesProfit; int TradesLoss; int TradesShortTotal; int TradesLongTotal; int TradesShortProfit; int TradesLongProfit; double InitialDeposit; double Withdrawal; double GrossProfit; double GrossLoss; double MaxProfit; double MaxLoss; double Balance; double MaxBalance; double BalanceDD; void InitStatistic() { this.LastPos = this.StartPos; this.TradesProfit = 0; this.TradesLoss = 0; this.TradesShortTotal = 0; this.TradesLongTotal = 0; this.TradesShortProfit = 0; this.TradesLongProfit = 0; this.InitialDeposit = ((this.StartPos < this.AmountHistoryOrders) && (this.HistoryOrders[this.LastPos] == OP_BALANCE)) ? this.HistoryOrders[this.LastPos++].GetProfit() : 0; this.Withdrawal = 0; this.GrossProfit = 0; this.GrossLoss = 0; this.MaxProfit = 0; this.MaxLoss = 0; this.Balance = 0; this.MaxBalance = 0; this.BalanceDD = 0; return; } void SetOrder( const double Profit, int &TradesLongShortProfit, int &TradesLongShortTotal ) { if (Profit > 0) { if (Profit > this.MaxProfit) this.MaxProfit = Profit; this.GrossProfit += Profit; this.TradesProfit++; TradesLongShortProfit++; } else if (Profit < 0) { if (Profit < this.MaxLoss) this.MaxLoss = Profit; this.GrossLoss += Profit; this.TradesLoss++; } this.Balance += Profit; if (this.Balance > this.MaxBalance) this.MaxBalance = this.Balance; else if (this.MaxBalance - this.Balance > this.BalanceDD) this.BalanceDD = this.MaxBalance - this.Balance; TradesLongShortTotal++; return; } void Refresh( void ) { while (this.LastPos < this.AmountHistoryOrders) { switch (this.HistoryOrders[this.LastPos].GetType()) { case ORDER_TYPE_BUY: this.SetOrder(this.HistoryOrders[this.LastPos].GetFullProfit(), this.TradesLongProfit, this.TradesLongTotal); break; case ORDER_TYPE_SELL: this.SetOrder(this.HistoryOrders[this.LastPos].GetFullProfit(), this.TradesShortProfit, this.TradesShortTotal); break; case OP_BALANCE: if (this.LastPos != this.StartPos) this.Withdrawal += this.HistoryOrders[this.LastPos].GetProfit(); else this.InitialDeposit = this.HistoryOrders[this.LastPos].GetProfit(); } this.LastPos++; } return; } static string DoubleToString( const double Num, const int digits ) { return((Num == DBL_MAX) ? "Max" : ::DoubleToString(Num, digits)); } protected: // HISTORYORDERS::Swap ORDER_BASE HistoryOrders[]; int ReserveHistoryOrders; bool FlagChange; bool Swap( HISTORYORDERS &OtherHistoryOrders ) { #ifdef __MQL5__ const bool Res = ::ArraySwap(this.HistoryOrders, OtherHistoryOrders.HistoryOrders); #else // #ifdef __MQL5__ const bool Res = false; #endif // #ifdef __MQL5__ #else if (Res) { this.AmountHistoryOrders = OtherHistoryOrders.AmountHistoryOrders; this.ReserveHistoryOrders = OtherHistoryOrders.ReserveHistoryOrders; this.FlagChange = OtherHistoryOrders.FlagChange; this.StartPos = OtherHistoryOrders.StartPos; this.LastPos = OtherHistoryOrders.LastPos; this.PrevAmountHistoryOrders = OtherHistoryOrders.PrevAmountHistoryOrders; this.TradesProfit = OtherHistoryOrders.TradesProfit; this.TradesLoss = OtherHistoryOrders.TradesLoss; this.TradesShortTotal = OtherHistoryOrders.TradesShortTotal; this.TradesLongTotal = OtherHistoryOrders.TradesLongTotal; this.TradesShortProfit = OtherHistoryOrders.TradesShortProfit; this.TradesLongProfit = OtherHistoryOrders.TradesLongProfit; this.InitialDeposit = OtherHistoryOrders.InitialDeposit; this.Withdrawal = OtherHistoryOrders.Withdrawal; this.GrossProfit = OtherHistoryOrders.GrossProfit; this.GrossLoss = OtherHistoryOrders.GrossLoss; this.MaxProfit = OtherHistoryOrders.MaxProfit; this.MaxLoss = OtherHistoryOrders.MaxLoss; this.Balance = OtherHistoryOrders.Balance; this.MaxBalance = OtherHistoryOrders.MaxBalance; this.BalanceDD = OtherHistoryOrders.BalanceDD; } return(Res); } public: // ORDER::IsChange2 void AddHistoryOrder( const ORDER_BASE &Order ) { if (this.AmountHistoryOrders == this.ReserveHistoryOrders) // https://www.mql5.com/ru/forum/282062/page48#comment_50770216 this.ReserveHistoryOrders = ::ArrayResize(this.HistoryOrders, ::MathMax(RESERVE_HISTORY_ORDERS, this.ReserveHistoryOrders << 1)); // ORDER OrderTmp = Order; // https://www.mql5.com/ru/forum/492248/page3#comment_57718727 ORDER OrderTmp; OrderTmp = Order; if (OrderTmp.IsNotNull() && this.AmountHistoryOrders < this.ReserveHistoryOrders) { OrderTmp.ToHistory(); this.HistoryOrders[this.AmountHistoryOrders++] = OrderTmp; this.FlagChange = true; } return; } protected: bool CopyFrom( const HISTORYORDERS* SourceOrders ) { this.AmountHistoryOrders = SourceOrders.AmountHistoryOrders; this.ReserveHistoryOrders = ::ArrayCopy(this.HistoryOrders, SourceOrders.HistoryOrders); this.InitStat(); return(this.ReserveHistoryOrders == ::ArraySize(SourceOrders.HistoryOrders)); } bool GetReduceOrder( ORDER_BASE &Order, const int Pos ) const { const bool Res = (Pos >= 0) && (Pos < this.AmountHistoryOrders); if (Res) { double Sum = 0; for (int i = 0; i <= Pos; i++) Sum += this.HistoryOrders[i].GetFullProfit(); Order.Set(this.HistoryOrders[Pos].GetTicket(), OP_BALANCE, 0, NULL, "ReduceHistory", 0, this.HistoryOrders[Pos].GetCloseTimeMsc(), 0, 0, 0, this.HistoryOrders[Pos].GetCloseTimeMsc(), 0, Sum); } return(Res); } bool GetReduceOrder( ORDER_BASE &Order, const int Pos, const bool ChangeHistory ) { const bool Res = this.GetReduceOrder(Order, Pos); if (Res && ChangeHistory && Pos) { int Amount = 1; for (int i = Pos + 1; i < this.AmountHistoryOrders; i++) this.HistoryOrders[Amount++] = this.HistoryOrders[i]; this.HistoryOrders[0] = Order; this.AmountHistoryOrders = Amount; this.InitStatistic(); } return(Res); } TICKET_TYPE GetMaxHistoryTicket( void ) const { TICKET_TYPE MaxTicket = 0; for (int i = this.AmountHistoryOrders - 1; i >= 0; i--) { const TICKET_TYPE Ticket = this.HistoryOrders[i].GetTicket(); if (Ticket > MaxTicket) MaxTicket = Ticket; } return(MaxTicket); } public: int AmountHistoryOrders; HISTORYORDERS() : AmountHistoryOrders(0), ReserveHistoryOrders(0), PrevAmountHistoryOrders(0), StartPos(0), FlagChange(true) { this.InitStatistic(); } bool ResetTickets( void ) { long Tickets[][2]; const int Size = ::ArrayResize(Tickets, this.AmountHistoryOrders); #define VALUE 0 #define INDEX 1 for (int i = 0; i < Size; i++) { // Tickets[i][VALUE] = this.HistoryOrders[i].GetTicket(); Tickets[i][VALUE] = this.HistoryOrders[i].GetOpenTimeMsc(); Tickets[i][INDEX] = i; } ::ArraySort(Tickets); for (int i = 0; i < Size; i++) this.HistoryOrders[(int)Tickets[i][INDEX]].SetTicket(i + 1); #undef INDEX #undef VALUE ORDER::SetTicketCount(Size); return(true); } bool InitStat( const long StartTime = 0 ) { if (this.AmountHistoryOrders) { if (this.StartPos < this.AmountHistoryOrders) { if (this.HistoryOrders[this.StartPos] != StartTime) { if (this.HistoryOrders[this.StartPos] < StartTime) { while ((++this.StartPos < this.AmountHistoryOrders) && (this.HistoryOrders[this.StartPos] < StartTime)) ; this.InitStatistic(); } else if (this.StartPos && (this.HistoryOrders[0] <= StartTime)) { const int PrevStartPos = this.StartPos; while ((--this.StartPos >= 0) && (this.HistoryOrders[this.StartPos] >= StartTime)) ; this.StartPos++; if (this.StartPos != PrevStartPos) this.InitStatistic(); } } } else if (this.HistoryOrders[this.StartPos - 1] > StartTime) { while ((--this.StartPos >= 0) && (this.HistoryOrders[this.StartPos] >= StartTime)) ; this.StartPos++; this.InitStatistic(); } } return(true); } double TesterStatistics( const ENUM_STATISTICS StatID ) { double Res = 0; this.Refresh(); switch (StatID) { case STAT_INITIAL_DEPOSIT: // Значение начального депозита Res = this.InitialDeposit; break; case STAT_WITHDRAWAL: // Количество выведенных со счета средств Res = this.Withdrawal; break; case STAT_PROFIT: // Чистая прибыль по окончании тестирования Res = this.GrossLoss + this.GrossProfit; break; case STAT_GROSS_PROFIT: // Общая прибыль, сумма всех прибыльных (положительных) трейдов. Значение больше или равно нулю Res = this.GrossProfit; break; case STAT_GROSS_LOSS: // Общий убыток, сумма всех убыточных (отрицательных) трейдов. Значение меньше или равно нулю Res = this.GrossLoss; break; case STAT_MAX_PROFITTRADE: // Максимальная прибыль – наибольшее значение среди всех прибыльных трейдов. Значение больше или равно нулю Res = this.MaxProfit; break; case STAT_MAX_LOSSTRADE: // Максимальный убыток – наименьшее значение среди всех убыточных трейдов. Значение меньше или равно нулю Res = this.MaxLoss; break; case STAT_BALANCE_DD: // Максимальная просадка баланса в деньгах. В процессе торговли баланс может испытать множество просадок, берется наибольшее значение. Res = this.BalanceDD; break; case STAT_EXPECTED_PAYOFF: // Математическое ожидание выигрыша { const int Total = this.TradesLongTotal + this.TradesShortTotal; Res = Total ? (this.GrossLoss + this.GrossProfit) / Total : 0; } break; case STAT_PROFIT_FACTOR: // Прибыльность – отношение STAT_GROSS_PROFIT/STAT_GROSS_LOSS. Если STAT_GROSS_LOSS=0, то прибыльность принимает значение DBL_MAX Res = this.GrossLoss ? -this.GrossProfit / this.GrossLoss : DBL_MAX; break; case STAT_RECOVERY_FACTOR: // Фактор восстановления – отношение STAT_PROFIT/STAT_BALANCE_DD Res = this.BalanceDD ? (this.GrossLoss + this.GrossProfit) / this.BalanceDD : 0; break; case STAT_DEALS: // Количество совершенных сделок Res = (this.TradesLongTotal + this.TradesShortTotal) << 1; break; case STAT_TRADES: // Количество трейдов Res = this.TradesLongTotal + this.TradesShortTotal; break; case STAT_PROFIT_TRADES: // Прибыльные трейды Res = this.TradesProfit; break; case STAT_LOSS_TRADES: // Убыточные трейды Res = this.TradesLoss; break; case STAT_SHORT_TRADES: // Короткие трейды Res = this.TradesShortTotal; break; case STAT_LONG_TRADES: // Длинные трейды Res = this.TradesLongTotal; break; case STAT_PROFIT_SHORTTRADES: // Короткие прибыльные трейды Res = this.TradesShortProfit; break; case STAT_PROFIT_LONGTRADES: // Длинные прибыльные трейды Res = this.TradesLongProfit; break; } return(Res); } bool IsHistoryChanged( void ) { const bool Res = (this.AmountHistoryOrders != this.PrevAmountHistoryOrders); if (Res) this.PrevAmountHistoryOrders = this.AmountHistoryOrders; return(Res); } bool GetFlagChange( void ) { const bool Res = this.FlagChange; this.FlagChange = false; return(Res); } string StatToString( const int &Amount ) { return("Profit = " + HISTORYORDERS::DoubleToString(this.TesterStatistics(STAT_PROFIT) / Amount, 2) + ", " + "Trades = " + HISTORYORDERS::DoubleToString(this.TesterStatistics(STAT_TRADES) / Amount, 0) + ", " + "PF = " + HISTORYORDERS::DoubleToString(this.TesterStatistics(STAT_PROFIT_FACTOR), 2) + ", " + "Mean = " + HISTORYORDERS::DoubleToString(this.TesterStatistics(STAT_EXPECTED_PAYOFF), 2) + ", " + "MaxDD = " + HISTORYORDERS::DoubleToString(-this.TesterStatistics(STAT_BALANCE_DD) / Amount, 2) + ", " + "RF = " + HISTORYORDERS::DoubleToString(this.TesterStatistics(STAT_RECOVERY_FACTOR), 2)); } double CalcSwaps( const double &SwapShort, const double &SwapLong, const datetime &RolloverTime, const int &Rollover3Days ) { double ChangeSwaps = 0; for (int i = this.AmountHistoryOrders - 1; i >= 0; i--) ChangeSwaps += this.HistoryOrders[i].SetSwap(SwapShort, SwapLong, RolloverTime, Rollover3Days); this.InitStatistic(); return(ChangeSwaps); } void MoveHistory( HISTORYORDERS& pOrders ) { for (int i = 0; i < this.AmountHistoryOrders; i++) pOrders.AddHistoryOrder(this.HistoryOrders[i]); this.AmountHistoryOrders = 0; return; } }; #undef RESERVE_HISTORY_ORDERS