//+------------------------------------------------------------------+
//|                                            TradeRecordReader.mqh |
//|           Groups closed deal history into one trade per position |
//+------------------------------------------------------------------+
#ifndef TRADERECORDREADER_MQH
#define TRADERECORDREADER_MQH

#include "TradeQualityTypes.mqh"

//+------------------------------------------------------------------+
//| CTradeRecordReader                                               |
//| Reads closed deal history for a time window and an optional      |
//| symbol filter, groups deals by position identifier, and produces |
//| one CTradeRecord per position that shows a complete entry and a  |
//| complete exit inside the requested range.                        |
//+------------------------------------------------------------------+
class CTradeRecordReader
  {
private:
   ulong             m_position_ids[];

   int               CollectPositionIds(const string symbol_filter);
   bool              BuildRecordForPosition(const ulong position_id,const string symbol_filter,CTradeRecord &record);

public:
                     CTradeRecordReader(void);
                    ~CTradeRecordReader(void);

   int               ReadTrades(const datetime from,const datetime to,const string symbol_filter,CTradeRecord &trades[]);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTradeRecordReader::CTradeRecordReader(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTradeRecordReader::~CTradeRecordReader(void)
  {
  }

//+-------------------------------------------------------------------+
//| CollectPositionIds                                                |
//| Scans the deals already selected by HistorySelect() and builds a  |
//| de-duplicated list of position identifiers, optionally restricted |
//| to one symbol.                                                    |
//+-------------------------------------------------------------------+
int CTradeRecordReader::CollectPositionIds(const string symbol_filter)
  {
   ArrayResize(m_position_ids,0);
   int total=::HistoryDealsTotal();

//--- walk every deal once and remember each new position id
   for(int i=0;i<total;i++)
     {
      ulong ticket=::HistoryDealGetTicket(i);
      if(ticket==0)
         continue;

      string deal_symbol=::HistoryDealGetString(ticket,DEAL_SYMBOL);
      if(symbol_filter!="" && deal_symbol!=symbol_filter)
         continue;

      ulong position_id=(ulong)::HistoryDealGetInteger(ticket,DEAL_POSITION_ID);
      if(position_id==0)
         continue;

      bool already_known=false;
      int known_count=ArraySize(m_position_ids);
      for(int j=0;j<known_count;j++)
        {
         if(m_position_ids[j]==position_id)
           {
            already_known=true;
            break;
           }
        }

      if(!already_known)
        {
         int new_index=ArraySize(m_position_ids);
         ArrayResize(m_position_ids,new_index+1);
         m_position_ids[new_index]=position_id;
        }
     }

   return(ArraySize(m_position_ids));
  }

//+-------------------------------------------------------------------+
//| BuildRecordForPosition                                            |
//| Rebuilds one CTradeRecord from every deal sharing position_id.    |
//| The entry deal (DEAL_ENTRY_IN) supplies direction and entry price |
//| because it is the deal that actually opened exposure; the latest  |
//| deal with DEAL_ENTRY_OUT or DEAL_ENTRY_OUT_BY supplies exit price |
//| and close time because a position can be closed across more than  |
//| one partial deal, and the caller wants the final state. Net       |
//| profit sums profit, swap, and commission across every deal        |
//| belonging to the position because all three are real cash effects |
//| of the same trade. Returns false, and the position is dropped,    |
//| if no entry deal or no exit deal was found, so a position that is |
//| only partially visible inside the requested window is never       |
//| partially scored.                                                 |
//+-------------------------------------------------------------------+
bool CTradeRecordReader::BuildRecordForPosition(const ulong position_id,const string symbol_filter,CTradeRecord &record)
  {
   int total=::HistoryDealsTotal();
   bool has_entry=false;
   bool has_exit=false;
   double net_profit=0.0;
   datetime latest_exit_time=0;

   for(int i=0;i<total;i++)
     {
      ulong ticket=::HistoryDealGetTicket(i);
      if(ticket==0)
         continue;

      ulong deal_position_id=(ulong)::HistoryDealGetInteger(ticket,DEAL_POSITION_ID);
      if(deal_position_id!=position_id)
         continue;

      string deal_symbol=::HistoryDealGetString(ticket,DEAL_SYMBOL);
      if(symbol_filter!="" && deal_symbol!=symbol_filter)
         continue;

      double deal_profit=::HistoryDealGetDouble(ticket,DEAL_PROFIT);
      double deal_swap=::HistoryDealGetDouble(ticket,DEAL_SWAP);
      double deal_commission=::HistoryDealGetDouble(ticket,DEAL_COMMISSION);
      net_profit+=deal_profit+deal_swap+deal_commission;

      ENUM_DEAL_ENTRY entry_type=(ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket,DEAL_ENTRY);

      if(entry_type==DEAL_ENTRY_IN)
        {
         ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket,DEAL_TYPE);
         record.m_direction=(deal_type==DEAL_TYPE_BUY)?TRADE_DIRECTION_BUY:TRADE_DIRECTION_SELL;
         record.m_entry_price=::HistoryDealGetDouble(ticket,DEAL_PRICE);
         record.m_entry_time=(datetime)::HistoryDealGetInteger(ticket,DEAL_TIME);
         record.m_symbol=deal_symbol;
         record.m_position_id=position_id;
         has_entry=true;
        }
      else
         if(entry_type==DEAL_ENTRY_OUT || entry_type==DEAL_ENTRY_OUT_BY)
           {
            datetime deal_time=(datetime)::HistoryDealGetInteger(ticket,DEAL_TIME);
            if(deal_time>=latest_exit_time)
              {
               latest_exit_time=deal_time;
               record.m_exit_price=::HistoryDealGetDouble(ticket,DEAL_PRICE);
               record.m_close_time=deal_time;
              }
            has_exit=true;
           }
     }

   record.m_net_profit=net_profit;

   return(has_entry && has_exit);
  }

//+-------------------------------------------------------------------+
//| ReadTrades                                                        |
//| Selects the deal history for [from,to], groups it by position,    |
//| and returns one fully-observed CTradeRecord per position. The     |
//| caller-supplied trades[] array is resized to fit the result.      |
//+-------------------------------------------------------------------+
int CTradeRecordReader::ReadTrades(const datetime from,const datetime to,const string symbol_filter,CTradeRecord &trades[])
  {
   ArrayResize(trades,0);

   if(!::HistorySelect(from,to))
      return(0);

   int position_count=CollectPositionIds(symbol_filter);
   int accepted=0;

   for(int i=0;i<position_count;i++)
     {
      CTradeRecord candidate;
      bool complete=BuildRecordForPosition(m_position_ids[i],symbol_filter,candidate);

      //--- an incompletely observed position is dropped, not partially
      //--- scored, because a missing entry or exit means direction,
      //--- entry price, exit price, or net profit would be silently
      //--- wrong rather than simply absent
      if(!complete)
         continue;

      if(candidate.m_entry_time<from || candidate.m_close_time>to)
         continue;

      int new_index=ArraySize(trades);
      ArrayResize(trades,new_index+1);
      trades[new_index]=candidate;
      accepted++;
     }

   return(accepted);
  }

#endif // TRADERECORDREADER_MQH
//+------------------------------------------------------------------+