//+------------------------------------------------------------------+
//|                                                   TradeStats.mqh |
//|                        Copyright 2019, Igor Ryabchikov aka Rigal |
//|                      http://tlap.com/forum/profile/109537-rigal/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Igor Ryabchikov (aka Rigal)"
#property link      "http://tlap.com/forum/profile/109537-rigal/"
#property strict
#ifndef TRADE_STATS_INCLUDE
#define TRADE_STATS_INCLUDE   (1)
#include <Arrays/ArrayObj.mqh>
#include <SymbolInfo.mqh>
#include <Trade.mqh>

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

class TypeTradeStats {
   int               type;
   SymbolInfo*       symbolInfo;
   CArrayObj*        trades[];
   
   double            totalLots;
   double            totalProfit;
   double            positiveProfit;
   double            positiveLots;
   double            maxProfit;
   double            sumTakeProfit;
   double            sumStopLoss;
   double            breakeven;
public:
   TypeTradeStats(int _type, SymbolInfo* _symbolInfo, bool _freeMemory = true) : type(_type), symbolInfo(_symbolInfo){
      ArrayResize(trades, SORTING_MODES_TOTAL);
      for(int i = 0; i < SORTING_MODES_TOTAL; i++){
         trades[i] = new CArrayObj();
         trades[i].FreeMode(_freeMemory);
      }
      Reset();
   }
   ~TypeTradeStats(void) {
      Reset();
      for(int i = 0; i < SORTING_MODES_TOTAL; i++)     
         if(CheckPointer(trades[i]) == POINTER_DYNAMIC)
            delete (trades[i]);
   }
   void Reset(void){
      totalLots = 0;
      totalProfit = 0;
      positiveProfit = 0;
      positiveLots = 0;
      sumTakeProfit = 0;
      sumStopLoss = 0;
      maxProfit = DBL_MIN;
      breakeven = EMPTY_VALUE;
      for(int i = 0; i < SORTING_MODES_TOTAL; i++){
//         Print("TypeStats: Clearing trades[", i, "]");
         trades[i].Clear();
         trades[i].Sort(i);
      }
   }
   void Add(Trade* _trade) {
      for(int i = 0; i < SORTING_MODES_TOTAL; i++)
         trades[i].InsertSort(_trade);
      totalProfit += _trade.GetProfit();
      if(_trade.GetProfit() > 0) {
         positiveProfit += _trade.GetProfit();
         positiveLots += _trade.GetLots();
      }
      totalLots += _trade.GetLots();
      maxProfit = MathMax(maxProfit, _trade.GetProfit());
      sumStopLoss += _trade.GetStopLoss();
      sumTakeProfit += _trade.GetTakeProfit();
   }
   void CalculateBreakEven(MqlTick &_tick, double _tickSize, double _tickValue) {
      if(totalLots < DBL_EPSILON)
         return;
      if(type == OP_BUY)
         breakeven = _tick.bid - totalProfit * _tickSize / _tickValue / totalLots;
      else
         breakeven = _tick.ask + totalProfit * _tickSize / _tickValue / totalLots;
   }
   
   int GetCount(void) const {
      return (trades[TIME_ASC].Total());
   }
   double GetTotalLots(void) const {
      return (totalLots);
   }
   double GetTotalProfit(void) const {
      return (totalProfit);
   }
   double GetPositiveProfit(void) const {
      return (positiveProfit);
   }
   double GetAverageTakeProfit(void) const {
      return (GetCount() > 0 ? sumTakeProfit / GetCount() : 0);
   } 
   double GetAverageStopLoss(void) const {
      return (GetCount() > 0 ? sumStopLoss / GetCount() : 0);
   } 
   double GetPositiveLots(void) const {
      return (positiveLots);
   }
   double GetMaxProfit(void) const {
      return (maxProfit);
   }
   Trade* GetBestLocated(void)const {
      if(trades[LOCATION_BEST_TO_WORST].Total() > 0)
         return ((Trade*)trades[LOCATION_BEST_TO_WORST].At(0));
      return (NULL);
   }
   Trade* GetWorstLocated(void) const {
      if(trades[LOCATION_BEST_TO_WORST].Total() > 0)
         return ((Trade*)trades[LOCATION_BEST_TO_WORST].At(trades[LOCATION_BEST_TO_WORST].Total() - 1));
      return (NULL);
   }
   Trade* GetMostProfitable(void) const {
      if(trades[PROFIT_DESC].Total() > 0)
         return ((Trade*)trades[PROFIT_DESC].At(0));
      return (NULL);
   }
   Trade* GetLeastProfitable(void) const {
      if(trades[PROFIT_DESC].Total() > 0)
         return ((Trade*)trades[PROFIT_DESC].At(trades[PROFIT_DESC].Total() - 1));
      return (NULL);
   }
   Trade* GetFirst(void) const {
      if(trades[TIME_ASC].Total() > 0)
         return ((Trade*)trades[TIME_ASC].At(0));
      return (NULL);
   }
   Trade* GetLast(void) const {
      if(trades[TIME_ASC].Total() > 0)
         return ((Trade*)trades[TIME_ASC].At(trades[TIME_ASC].Total() - 1));
      return (NULL);
   }
   CArrayObj* GetTrades(TradesSortingMode _mode = TIME_ASC) const {
      return (trades[_mode]);
   }
   double GetBreakEven(void) const { 
      return (breakeven);
   }
};
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class TradeStats : public CObject {
protected:
   int                     magic;
   SymbolInfo*             symbolInfo;
   double                  totalProfit;
   double                  positiveProfit;
   double                  positiveLots;
   double                  totalLots;
   double                  breakeven;
   bool                    recalculateTickValue;
   bool                    isTickValueRecalculated[2];
   CArrayObj*              trades[];
   TypeTradeStats*         typeStats[2];
   double                  tickValue[2];
   double                  tickSize;
   MqlTick                 mqlTick;
   
   virtual void Add(Trade* _trade) {
      for(int i = 0; i < SORTING_MODES_TOTAL; i++){
         if(i != LOCATION_BEST_TO_WORST)
            trades[i].InsertSort(_trade);
      }
      TypeTradeStats* tts = typeStats[_trade.GetType()];
      tts.Add(_trade);
      totalProfit += _trade.GetProfit();
      if(_trade.GetProfit() > 0) {
         positiveProfit += _trade.GetProfit();
         positiveLots += _trade.GetLots();
      }
      totalLots += _trade.GetLots();
   }
   
public:
   TradeStats(SymbolInfo* _symbolInfo, int _magic, bool _freeMemory = true) :
      magic(_magic),
      symbolInfo(_symbolInfo),
      totalProfit(0),
      positiveProfit(0),
      positiveLots(0),
      totalLots(0),
      breakeven(EMPTY_VALUE) {
      if(_magic < 0)
         Alert("Magic is negative, the expert will handle every open ", _symbolInfo.symbol, " position!");
      typeStats[OP_BUY] = new TypeTradeStats(OP_BUY, _symbolInfo, _freeMemory);
      typeStats[OP_SELL] = new TypeTradeStats(OP_SELL, _symbolInfo, _freeMemory);
      ArrayResize(trades, SORTING_MODES_TOTAL);
      for(int i = 0; i < SORTING_MODES_TOTAL; i++) {
         trades[i] = new CArrayObj();
         trades[i].FreeMode(_freeMemory);
//         if(!_freeMemory)
//            Print("Requested to leave the memory as is... the flag is ", trades[i].FreeMode());
      }
      int pos = StringFind(_symbolInfo.symbol, AccountCurrency());
      recalculateTickValue = IsTesting() && pos >= 0 && pos < 3;
   }
   
   ~TradeStats(void) {
      if(CheckPointer(typeStats[OP_BUY]) == POINTER_DYNAMIC)
         delete typeStats[OP_BUY];
      if(CheckPointer(typeStats[OP_SELL]) == POINTER_DYNAMIC)
         delete typeStats[OP_SELL];
      for(int i = 0; i < SORTING_MODES_TOTAL; i++)     
         if(CheckPointer(trades[i]) == POINTER_DYNAMIC)
            delete (trades[i]);
   }
   void SetMagic(int _magic) {
      magic = _magic;
   }
   void Reset(void){
      typeStats[OP_BUY].Reset();
      typeStats[OP_SELL].Reset();
      totalProfit = 0;
      positiveProfit = 0;
      positiveLots = 0;
      breakeven = EMPTY_VALUE;
      totalLots = 0;
      for(int i = 0; i < SORTING_MODES_TOTAL; i++){
//         Print("Clearing trades[", i, "]");
         trades[i].Clear();
         trades[i].Sort(i);
      }
      tickSize = SymbolInfoDouble(symbolInfo.symbol, SYMBOL_TRADE_TICK_SIZE);
      tickValue[OP_BUY] = SymbolInfoDouble(symbolInfo.symbol, SYMBOL_TRADE_TICK_VALUE);
      tickValue[OP_SELL] = tickValue[OP_BUY];
      isTickValueRecalculated[OP_BUY] = false;
      isTickValueRecalculated[OP_SELL] = false;
   }
   
   virtual void UpdatePosition(void){
//      logger.Trace("TradeStats " + IntegerToString(magic) + " update start");
      if(trades[TIME_ASC].Total() == 0 && OrdersTotal() == 0)
         return;
      Reset();
      double maxAbsRecalculated[] = {0.0, 0.0};
//      logger.Trace("TradeStats " + IntegerToString(magic) + " reset. Count: " + IntegerToString(trades[TIME_ASC].Total()));
      for(int i = OrdersTotal() - 1; i >= 0; i--){
         if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)){
            logger.Error("Error with OrderSelect: ", GetLastError());
            continue;
           }
   
         if((magic >= 0 && OrderMagicNumber() != magic) || OrderSymbol() != symbolInfo.symbol || OrderType() > OP_SELL )
            continue;
         double orderProfit = OrderProfit() + OrderSwap() + OrderCommission();
         Trade* trade = new Trade(symbolInfo, OrderTicket(), OrderType(), OrderOpenPrice(), OrderLots(), OrderOpenTime(), orderProfit, OrderStopLoss(), OrderTakeProfit());
         Add(trade);
         if(recalculateTickValue && MathAbs(OrderProfit()) > maxAbsRecalculated[OrderType()]) {
            maxAbsRecalculated[OrderType()] = MathAbs(OrderProfit());
            double priceDistance = sgn(OrderType()) * (OrderClosePrice() - OrderOpenPrice());
            if(MathAbs(priceDistance) >= tickSize && MathAbs(OrderProfit()) > 0.01) {
               tickValue[OrderType()] = tickSize * OrderProfit() / OrderLots() / priceDistance;
               isTickValueRecalculated[OrderType()] = true;
            }
         }
      }
      SymbolInfoTick(symbolInfo.symbol, mqlTick);
      typeStats[OP_BUY].CalculateBreakEven(mqlTick, tickSize, tickValue[OP_BUY]);
      typeStats[OP_SELL].CalculateBreakEven(mqlTick, tickSize, tickValue[OP_SELL]);
      CalculateBreakEven(mqlTick);
//      logger.Trace("TradeStats " + IntegerToString(magic) + " update complete. Count: " + IntegerToString(trades[TIME_ASC].Total()));
   }
   
   void CalculateBreakEven(MqlTick &_tick) {
      int netDirection = GetNetDirection();
      if(netDirection == OP_BUY)
         breakeven = _tick.bid - totalProfit * tickSize / tickValue[OP_BUY] / NormalizeDouble(typeStats[OP_BUY].GetTotalLots() - typeStats[OP_SELL].GetTotalLots(), symbolInfo.lotPrecision);
      else if(netDirection == OP_SELL)
         breakeven = _tick.ask + totalProfit * tickSize / tickValue[OP_SELL] / NormalizeDouble(typeStats[OP_SELL].GetTotalLots() - typeStats[OP_BUY].GetTotalLots(), symbolInfo.lotPrecision);
   }
   
   int GetMagic(void) const {
      return (magic);
   }
   SymbolInfo* GetSymbolInfo(void) const {
      return (symbolInfo);
   }
   int GetCount(void) const {
      return (trades[TIME_ASC].Total());
   }
   int GetCount(int type) const {
      return (typeStats[type].GetCount());
   }
   double GetTotalProfit(void) const {
      return (totalProfit);
   }
   double GetPositiveProfit(void) const {
      return (positiveProfit);
   }
   double GetPositiveProfit(int _type) const {
      return (typeStats[_type].GetPositiveProfit());
   }
   double GetPositiveLots(void) const {
      return (positiveLots);
   }
   double GetPositiveLots(int _type) const {
      return (typeStats[_type].GetPositiveLots());
   }
   double GetTotalLots(void) const {
      return (totalLots);
   }
   double GetMaxProfit(int type) const {
      return (typeStats[type].GetMaxProfit());
   }
   double GetTotalProfit(int type) const {
      return (typeStats[type].GetTotalProfit());
   }
   double GetTotalLots(int type) const {
      return (typeStats[type].GetTotalLots());
   }
   Trade* GetBestLocatedTrade(int type) const {
      return (typeStats[type].GetBestLocated());
   }
   Trade* GetWorstLocatedTrade(int type) const {
      return (typeStats[type].GetWorstLocated());
   }
   Trade* GetMostProfitableTrade(int type) const {
      return (typeStats[type].GetMostProfitable());
   }
   Trade* GetMostProfitable(void) const {
      if(trades[PROFIT_DESC].Total() > 0)
         return ((Trade*)trades[PROFIT_DESC].At(0));
      return (NULL);
   }
   Trade* GetLeastProfitableTrade(int type) const {
      return (typeStats[type].GetLeastProfitable());
   }
   Trade* GetLeastProfitable(void) const {
      if(trades[PROFIT_DESC].Total() > 0)
         return ((Trade*)trades[PROFIT_DESC].At(trades[PROFIT_DESC].Total() - 1));
      return (NULL);
   }
   Trade* GetFirstTrade(int type) const {
      return (typeStats[type].GetFirst());
   }
   Trade* GetFirstTrade(void) const {
      if(trades[TIME_ASC].Total() > 0)
         return ((Trade*)trades[TIME_ASC].At(0));
      return (NULL);
   }
   Trade* GetLastTrade(int type) const {
      return (typeStats[type].GetLast());
   }
   Trade* GetLast(void) const {
      if(trades[TIME_ASC].Total() > 0)
         return ((Trade*)trades[TIME_ASC].At(trades[TIME_ASC].Total() - 1));
      return (NULL);
   }
   CArrayObj* GetTrades(TradesSortingMode _mode = TIME_ASC) const {
      return (trades[_mode]);
   }
   CArrayObj* GetTrades(int type, TradesSortingMode _mode = TIME_ASC) const {
      return (typeStats[type].GetTrades(_mode));
   }

   int GetNetDirection(void) const {
      if(typeStats[OP_BUY].GetTotalLots() - typeStats[OP_SELL].GetTotalLots() >= symbolInfo.lotStep)
         return OP_BUY;
      if(typeStats[OP_SELL].GetTotalLots() - typeStats[OP_BUY].GetTotalLots() >= symbolInfo.lotStep)
         return OP_SELL;
      return OP_FLAT;
   }
   
   double GetNetLots(void) const {
      return MathAbs(typeStats[OP_BUY].GetTotalLots() - typeStats[OP_SELL].GetTotalLots());
   }
   double GetAverageTakeProfit(const int _type) const {
      return (typeStats[_type].GetAverageTakeProfit());
   } 
   double GetAverageStopLoss(const int _type) const {
      return (typeStats[_type].GetAverageStopLoss());
   } 


   Trade* FindTicket(int _ticket) {
      for(int i = 0; i < GetCount(); i++){
         Trade* trade = trades[TIME_ASC].At(i);
         if(trade.GetTicket() == _ticket)
            return (trade);
      }
      return NULL;
   }
   virtual string GetName() const {
      return ("TradeStats (" + IntegerToString(magic) + ", " + symbolInfo.symbol + ")");
   }
   
   double GetBreakEven(int _type) const {
      return (typeStats[_type].GetBreakEven());
   }
   double GetBreakEven(void) const {
      return (breakeven);
   }
   bool IsTickValueRecalculated(int _type) {
      return (isTickValueRecalculated[_type]);
   }
   double GetTickValue(int _type) {
      return (tickValue[_type]);
   }
};
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

class TradeStatsAggregation : public TradeStats {
   TradeStats* tradeStats[];
   bool freeMemory;
   void Reset(void){
      for(int i = 0; i < ArraySize(tradeStats); i++) {
         tradeStats[i].Reset();
      }
      TradeStats::Reset();
   }
public:
   TradeStatsAggregation(SymbolInfo* _symbolInfo, bool _freeMemory = true) : TradeStats(_symbolInfo, INT_MAX), freeMemory(_freeMemory) {      
   }
   ~TradeStatsAggregation() {
      if(freeMemory) {
         for(int i = 0; i < ArraySize(tradeStats); i++)
            if(CheckPointer(tradeStats[i]) == POINTER_DYNAMIC)
               delete (tradeStats[i]);
      }
   }
   void SetMagics(int &_magic[]) {
      ArrayResize(tradeStats, ArraySize(_magic));
      for(int i = 0; i < ArraySize(_magic); i++)
         tradeStats[i] = new TradeStats(symbolInfo, _magic[i]);
   }
   void SetTradeStats(TradeStats &_tradeStats[]){
      ArrayResize(tradeStats, ArraySize(_tradeStats));
      for(int i = 0; i < ArraySize(_tradeStats); i++)
         tradeStats[i] = &_tradeStats[i];
   }
   void AddTradeStats(TradeStats *_tradeStats) {
      int pos = ArraySize(tradeStats);
      ArrayResize(tradeStats, pos + 1);
      tradeStats[pos] = _tradeStats;
   }
   void RemoveTradeStats(int _magic) {
      bool isShiftOn = false;
      for(int i = 0; i < ArraySize(tradeStats) - 1; i++) {
         if(tradeStats[i].GetMagic() == _magic) {
            isShiftOn = true;
            if(CheckPointer(tradeStats[i]) == POINTER_DYNAMIC)
               delete (tradeStats[i]);
         }
         if(isShiftOn) {
            tradeStats[i] = tradeStats[i + 1];
         }
      }
      ArrayResize(tradeStats, ArraySize(tradeStats) - 1);
   }

   void UpdatePosition(){
//      logger.Trace("Aggregated trade stats update start");
      if(GetCount() == 0 && OrdersTotal() == 0)
         return;
      Reset();
//      logger.Trace("Aggregated trade stats reset. Count: " + IntegerToString(trades[NONE].Total()));
      for(int i = 0; i < ArraySize(tradeStats); i++) {
         tradeStats[i].UpdatePosition();
         for(int o = 0; o < tradeStats[i].GetCount(); o++)
           Add((Trade*) tradeStats[i].GetTrades().At(o));
         if(tradeStats[i].IsTickValueRecalculated(OP_BUY)) 
            tickValue[OP_BUY] = tradeStats[i].GetTickValue(OP_BUY);
         if(tradeStats[i].IsTickValueRecalculated(OP_SELL))
            tickValue[OP_SELL] = tradeStats[i].GetTickValue(OP_SELL);
         
      }
      SymbolInfoTick(symbolInfo.symbol, mqlTick);
      typeStats[OP_BUY].CalculateBreakEven(mqlTick, tickSize, tickValue[OP_BUY]);
      typeStats[OP_SELL].CalculateBreakEven(mqlTick, tickSize, tickValue[OP_SELL]);
      CalculateBreakEven(mqlTick);
//      logger.Trace("Aggregated trade stats updated. Count: " + IntegerToString(trades[NONE].Total()));
   }
   
   TradeStats* GetIndividualStats(int i) {
      return (tradeStats[i]);
   }
   TradeStats* GetStatsForMagic(int _magic) {
      for(int i = 0; i < ArraySize(tradeStats); i++)
         if(tradeStats[i].GetMagic() == _magic)
            return (tradeStats[i]);
      return NULL;
   }
   virtual string GetName() {
      string magicstr = "";
      for(int i = 0; i < ArraySize(tradeStats); i++)
         magicstr = magicstr + IntegerToString(tradeStats[i].GetMagic()) + ", ";
      return ("TradeStats (" + magicstr + ", " + symbolInfo.symbol + ")");
   }
   int Total() {
      return (ArraySize(tradeStats));
   }
   bool IsOneOfMagics(int _magic) {
      for(int i = 0; i < ArraySize(tradeStats); i++)
         if(_magic == tradeStats[i].GetMagic())
            return (true);
      return (false);
   }
};
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

#endif