//+------------------------------------------------------------------+
//|                                    RollingWindowCalculator.mqh   |
//|                   Computes rolling Profit Factor incrementally   |
//+------------------------------------------------------------------+
#ifndef ROLLING_WINDOW_CALCULATOR_MQH
#define ROLLING_WINDOW_CALCULATOR_MQH

#include "RollingPFTypes.mqh"
#include "ITradeSource.mqh"

//+------------------------------------------------------------------+
//| Builds the rolling Profit Factor time series from any trade      |
//| source; depends on the ITradeSource interface, not a concrete    |
//| reader, so a synthetic trade sequence can drive the same code    |
//| path as live account history                                     |
//+------------------------------------------------------------------+
class CRollingWindowCalculator
  {
private:
   SRollingPFRecord  m_series[];
   int               m_window_size;
   double            m_zero_epsilon;

public:
                     CRollingWindowCalculator(void);
                    ~CRollingWindowCalculator(void);

   void              SetZeroEpsilon(const double epsilon);
   bool              Compute(const ITradeSource &source, const int window_size);
   int               SeriesCount(void) const;
   bool              GetRecord(const int index, SRollingPFRecord &record) const;

private:
   double            SafeProfitFactor(const double gross_profit, const double gross_loss) const;
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRollingWindowCalculator::CRollingWindowCalculator(void) : m_window_size(0), m_zero_epsilon(0.0000001)
  {
   ArrayResize(m_series, 0);
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CRollingWindowCalculator::~CRollingWindowCalculator(void)
  {
  }

//+------------------------------------------------------------------+
//| SetZeroEpsilon                                                   |
//| Override the default threshold below which Gross Loss is treated |
//| as zero. The default (1e-7) is a reasonable starting point, not  |
//| a universal constant; accounts in different currencies or        |
//| instruments with different point values may need a different     |
//| threshold.                                                       |
//+------------------------------------------------------------------+
void CRollingWindowCalculator::SetZeroEpsilon(const double epsilon)
  {
   if(epsilon > 0.0)
      m_zero_epsilon = epsilon;
  }

//+------------------------------------------------------------------+
//| SafeProfitFactor                                                 |
//| Compute Profit Factor while guarding against division by zero    |
//| Parameters: gross_profit - accumulated Gross Profit for a window |
//|             gross_loss   - accumulated Gross Loss for a window   |
//| Returns: ROLLING_PF_INFINITE if loss is effectively zero and     |
//|          profit is positive; ROLLING_PF_UNDEFINED if both are    |
//|          effectively zero, since a window with no realized       |
//|          result either way is mathematically undefined, not a    |
//|          genuine breakeven                                       |
//+------------------------------------------------------------------+
double CRollingWindowCalculator::SafeProfitFactor(const double gross_profit, const double gross_loss) const
  {
   if(gross_loss <= m_zero_epsilon)
     {
      if(gross_profit <= m_zero_epsilon)
         return(ROLLING_PF_UNDEFINED);
      return(ROLLING_PF_INFINITE);
     }

   return(gross_profit / gross_loss);
  }

//+------------------------------------------------------------------+
//| Compute                                                          |
//| Build the rolling Profit Factor series using incremental totals  |
//| Parameters: source      - source of completed trades             |
//|             window_size - number of trades per rolling window    |
//| Returns: false if the requested window size cannot be satisfied  |
//| Assumptions: source trades are ordered chronologically           |
//+------------------------------------------------------------------+
bool CRollingWindowCalculator::Compute(const ITradeSource &source, const int window_size)
  {
   ArrayResize(m_series, 0);
   m_window_size = window_size;

   int total_trades = source.TradeCount();
   if(window_size <= 0 || total_trades < window_size)
      return(false);

   double gross_profit = 0.0;
   double gross_loss   = 0.0;
   int    win_count    = 0;
   int    loss_count   = 0;

   STradeRecord trade;

//--- accumulate the first rolling window
   for(int i = 0; i < window_size; i++)
     {
      source.GetTrade(i, trade);
      if(trade.net_profit > 0.0)
        {
         gross_profit += trade.net_profit;
         win_count++;
        }
      else
         if(trade.net_profit < 0.0)
           {
            gross_loss += MathAbs(trade.net_profit);
            loss_count++;
           }
     }

   int record_count = total_trades - window_size + 1;
   ArrayResize(m_series, record_count);

   source.GetTrade(window_size - 1, trade);
   m_series[0].end_index     = window_size - 1;
   m_series[0].end_time      = trade.close_time;
   m_series[0].gross_profit  = gross_profit;
   m_series[0].gross_loss    = gross_loss;
   m_series[0].profit_factor = SafeProfitFactor(gross_profit, gross_loss);
   m_series[0].win_count     = win_count;
   m_series[0].loss_count    = loss_count;

//--- slide the window forward one trade at a time
   for(int w = 1; w < record_count; w++)
     {
      STradeRecord outgoing, incoming;
      source.GetTrade(w - 1, outgoing);
      source.GetTrade(w + window_size - 1, incoming);

      if(outgoing.net_profit > 0.0)
        {
         gross_profit -= outgoing.net_profit;
         win_count--;
        }
      else
         if(outgoing.net_profit < 0.0)
           {
            gross_loss -= MathAbs(outgoing.net_profit);
            loss_count--;
           }

      if(incoming.net_profit > 0.0)
        {
         gross_profit += incoming.net_profit;
         win_count++;
        }
      else
         if(incoming.net_profit < 0.0)
           {
            gross_loss += MathAbs(incoming.net_profit);
            loss_count++;
           }

      //--- guard against floating-point drift accumulating over long histories.
      //--- Logged rather than clamped silently: a large or repeated negative
      //--- total is more likely a data or algorithm error than ordinary
      //--- rounding noise, and should not disappear unnoticed.
      if(gross_profit < 0.0)
        {
         Print("Rolling Profit Factor Calculator: negative Gross Profit detected and clamped, check trade data.");
         gross_profit = 0.0;
        }
      if(gross_loss < 0.0)
        {
         Print("Rolling Profit Factor Calculator: negative Gross Loss detected and clamped, check trade data.");
         gross_loss = 0.0;
        }

      m_series[w].end_index     = w + window_size - 1;
      m_series[w].end_time      = incoming.close_time;
      m_series[w].gross_profit  = gross_profit;
      m_series[w].gross_loss    = gross_loss;
      m_series[w].profit_factor = SafeProfitFactor(gross_profit, gross_loss);
      m_series[w].win_count     = win_count;
      m_series[w].loss_count    = loss_count;
     }

   return(true);
  }

//+------------------------------------------------------------------+
//| SeriesCount                                                      |
//| Number of rolling observations produced by the last Compute call |
//+------------------------------------------------------------------+
int CRollingWindowCalculator::SeriesCount(void) const
  {
   return(ArraySize(m_series));
  }

//+------------------------------------------------------------------+
//| GetRecord                                                        |
//| Retrieve one rolling observation by index                        |
//| Returns: false if the index is out of range                      |
//+------------------------------------------------------------------+
bool CRollingWindowCalculator::GetRecord(const int index, SRollingPFRecord &record) const
  {
   if(index < 0 || index >= ArraySize(m_series))
      return(false);

   record = m_series[index];
   return(true);
  }

#endif // ROLLING_WINDOW_CALCULATOR_MQH
//+------------------------------------------------------------------+