//+------------------------------------------------------------------+
//|                                             ExcursionTracker.mqh |
//|                                Copyright 2026, Tola Moses Hector |
//|                                     https://t.me/tolahector      |
//+------------------------------------------------------------------+
#ifndef EXCURSIONTRACKER_MQH
#define EXCURSIONTRACKER_MQH

//+------------------------------------------------------------------+
//| One closed-trade excursion sample                                |
//+------------------------------------------------------------------+
struct SExcursionSample
  {
   ulong             ticket;
   string            symbol;
   long              type;          // POSITION_TYPE_BUY or POSITION_TYPE_SELL
   double            r_distance;    // price distance representing 1R at open
   double            mfe_r;         // best favorable excursion reached, in R
   double            result_r;      // final realized result, in R
   datetime          close_time;
  };

//+------------------------------------------------------------------+
//| CExcursionTracker                                                |
//| Tracks live MFE-in-R for open positions and persists a sample    |
//| for each closed position to a flat file for later calibration.   |
//+------------------------------------------------------------------+
class CExcursionTracker
  {
private:
   struct SLiveEntry
     {
      ulong             ticket;
      double            entry_price;
      double            r_distance;   // > 0, price distance for 1R
      long              type;
      double            best_mfe_r;   // running peak MFE, in R
     };

   SLiveEntry        m_live[];
   int               m_live_count;
   string            m_filename;

   int               FindLive(ulong ticket) const;

public:
                     CExcursionTracker(void);
                    ~CExcursionTracker(void);

   void              SetFile(const string filename);

   //--- lifecycle
   bool              Register(ulong ticket, double entry_price, double stop_price, long type);
   void              UpdateTick(void);
   bool              Finalize(ulong ticket, double result_r);
   bool              IsRegistered(ulong ticket) const;
   bool              GetLiveInfo(ulong ticket, double &entry_price, double &r_distance, long &type) const;
   int               LiveCount(void) const;

   //--- persistence
   bool              AppendSample(const SExcursionSample &sample);
   int               LoadSamples(SExcursionSample &out[]) const;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CExcursionTracker::CExcursionTracker(void) : m_live_count(0),
   m_filename("ExcursionSamples.csv")
  {
   ArrayResize(m_live, 16);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CExcursionTracker::~CExcursionTracker(void)
  {
  }
//+------------------------------------------------------------------+
//| SetFile                                                          |
//+------------------------------------------------------------------+
void CExcursionTracker::SetFile(const string filename)
  {
   m_filename = filename;
  }
//+------------------------------------------------------------------+
//| FindLive                                                         |
//+------------------------------------------------------------------+
int CExcursionTracker::FindLive(ulong ticket) const
  {
   for(int i = 0; i < m_live_count; i++)
      if(m_live[i].ticket == ticket)
         return(i);
   return(-1);
  }
//+------------------------------------------------------------------+
//| IsRegistered                                                     |
//+------------------------------------------------------------------+
bool CExcursionTracker::IsRegistered(ulong ticket) const
  {
   return(FindLive(ticket) >= 0);
  }
//+------------------------------------------------------------------+
//| GetLiveInfo                                                      |
//| Exposes entry price, r_distance and type for a ticket that is    |
//| still open, so the caller can compute a realized R result before |
//| calling Finalize().                                              |
//+------------------------------------------------------------------+
bool CExcursionTracker::GetLiveInfo(ulong ticket, double &entry_price, double &r_distance, long &type) const
  {
   int idx = FindLive(ticket);
   if(idx < 0)
      return(false);
   entry_price = m_live[idx].entry_price;
   r_distance  = m_live[idx].r_distance;
   type        = m_live[idx].type;
   return(true);
  }
//+------------------------------------------------------------------+
//| LiveCount                                                        |
//+------------------------------------------------------------------+
int CExcursionTracker::LiveCount(void) const
  {
   return(m_live_count);
  }
//+------------------------------------------------------------------+
//| Register                                                         |
//| r_distance is derived from entry and initial stop and must be    |
//| a positive price distance representing one unit of risk (1R).    |
//+------------------------------------------------------------------+
bool CExcursionTracker::Register(ulong ticket, double entry_price, double stop_price, long type)
  {
   double dist = (type == POSITION_TYPE_BUY) ? (entry_price - stop_price) : (stop_price - entry_price);
   if(dist <= 0.0)
      return(false);

   if(FindLive(ticket) >= 0)
      return(true);

   if(m_live_count >= ArraySize(m_live))
      ArrayResize(m_live, ArraySize(m_live) * 2);

   m_live[m_live_count].ticket      = ticket;
   m_live[m_live_count].entry_price = entry_price;
   m_live[m_live_count].r_distance  = dist;
   m_live[m_live_count].type        = type;
   m_live[m_live_count].best_mfe_r  = 0.0;
   m_live_count++;
   return(true);
  }
//+------------------------------------------------------------------+
//| UpdateTick                                                       |
//| Recomputes the running peak MFE in R for every registered ticket.|
//+------------------------------------------------------------------+
void CExcursionTracker::UpdateTick(void)
  {
   for(int i = 0; i < m_live_count; i++)
     {
      ulong ticket = m_live[i].ticket;
      if(!PositionSelectByTicket(ticket))
         continue;

      string symbol = PositionGetString(POSITION_SYMBOL);
      double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);

      double favorable_price = (m_live[i].type == POSITION_TYPE_BUY) ? bid : ask;
      double excursion = (m_live[i].type == POSITION_TYPE_BUY)
                         ? (favorable_price - m_live[i].entry_price)
                         : (m_live[i].entry_price - favorable_price);

      double mfe_r = excursion / m_live[i].r_distance;
      if(mfe_r > m_live[i].best_mfe_r)
         m_live[i].best_mfe_r = mfe_r;
     }
  }
//+------------------------------------------------------------------+
//| Finalize                                                         |
//| Called once a position has fully closed. Writes the sample and   |
//| removes the ticket from the live registry.                       |
//+------------------------------------------------------------------+
bool CExcursionTracker::Finalize(ulong ticket, double result_r)
  {
   int idx = FindLive(ticket);
   if(idx < 0)
      return(false);

   SExcursionSample sample;
   sample.ticket     = ticket;
   sample.symbol     = _Symbol;
   sample.type       = m_live[idx].type;
   sample.r_distance = m_live[idx].r_distance;
   sample.mfe_r      = m_live[idx].best_mfe_r;
   sample.result_r   = result_r;
   sample.close_time = TimeCurrent();

   bool ok = AppendSample(sample);

//--- compact: overwrite the closed slot with the last live entry
   m_live_count--;
   if(idx < m_live_count)
      m_live[idx] = m_live[m_live_count];

   return(ok);
  }
//+------------------------------------------------------------------+
//| AppendSample                                                     |
//| Appends one CSV row: ticket,symbol,type,r_distance,mfe_r,        |
//| result_r,close_time                                              |
//+------------------------------------------------------------------+
bool CExcursionTracker::AppendSample(const SExcursionSample &sample)
  {
   int handle = FileOpen(m_filename, FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI, ',');
   if(handle == INVALID_HANDLE)
      return(false);

   FileSeek(handle, 0, SEEK_END);
   FileWrite(handle, sample.ticket, sample.symbol, (int)sample.type,
             sample.r_distance, sample.mfe_r, sample.result_r,
             TimeToString(sample.close_time, TIME_DATE | TIME_SECONDS));
   FileClose(handle);
   return(true);
  }
//+------------------------------------------------------------------+
//| LoadSamples                                                      |
//| Reads every row from the sample file into out[], oldest first.   |
//| Returns the number of samples loaded.                            |
//+------------------------------------------------------------------+
int CExcursionTracker::LoadSamples(SExcursionSample &out[]) const
  {
   ArrayResize(out, 0);

   if(!FileIsExist(m_filename))
      return(0);

   int handle = FileOpen(m_filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
   if(handle == INVALID_HANDLE)
      return(0);

   int count = 0;
   while(!FileIsEnding(handle))
     {
      string ticket_str = FileReadString(handle);
      if(StringLen(ticket_str) == 0)
         break;

      string symbol_str = FileReadString(handle);
      string type_str   = FileReadString(handle);
      string rdist_str  = FileReadString(handle);
      string mfe_str    = FileReadString(handle);
      string result_str = FileReadString(handle);
      string close_str  = FileReadString(handle);

      ArrayResize(out, count + 1);
      out[count].ticket     = (ulong)StringToInteger(ticket_str);
      out[count].symbol     = symbol_str;
      out[count].type       = (long)StringToInteger(type_str);
      out[count].r_distance = StringToDouble(rdist_str);
      out[count].mfe_r      = StringToDouble(mfe_str);
      out[count].result_r   = StringToDouble(result_str);
      out[count].close_time = StringToTime(close_str);
      count++;
     }

   FileClose(handle);
   return(count);
  }

#endif // EXCURSIONTRACKER_MQH
