//+------------------------------------------------------------------+
//|                                            HeatmapAggregator.mqh |
//+------------------------------------------------------------------+
#ifndef HEATMAPAGGREGATOR_MQH
#define HEATMAPAGGREGATOR_MQH

#include "HeatmapTypes.mqh"

//+------------------------------------------------------------------+
//| CHeatmapAggregator                                               |
//| Groups deal samples by symbol and hour, computing win rate and   |
//| average reward-to-risk ratio for each combination found.         |
//+------------------------------------------------------------------+
class CHeatmapAggregator
  {
public:
                     CHeatmapAggregator(void);
                    ~CHeatmapAggregator(void);

   int                   Aggregate(const CDealSample &deals[], int count,
                                   CHeatmapCell &cells_out[]);
  };

//+------------------------------------------------------------------+
//| Constructor: no member state to initialize.                      |
//+------------------------------------------------------------------+
CHeatmapAggregator::CHeatmapAggregator(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor: no dynamic resources to release.                     |
//+------------------------------------------------------------------+
CHeatmapAggregator::~CHeatmapAggregator(void)
  {
  }

//+-------------------------------------------------------------------+
//| Aggregate                                                         |
//| Groups deals by (symbol, hour), accumulating trade count, win     |
//| count, and separate win and loss sums, then computes win rate and |
//| edge ratio for every combination found. Returns the number of     |
//| cells populated.                                                  |
//+-------------------------------------------------------------------+
int CHeatmapAggregator::Aggregate(const CDealSample &deals[], int count,
                                  CHeatmapCell &cells_out[])
  {
   string symbols[];
   int    hours[];
   int    trade_count[];
   int    win_count[];
   double win_sum[];
   int    win_n[];
   double loss_sum[];
   int    loss_n[];
   ::ArrayResize(symbols, 0);
//--- group every deal into its (symbol, hour) cell, creating one if new
   for(int i = 0; i < count; i++)
     {
      int idx = -1;
      int n   = ::ArraySize(symbols);
      for(int j = 0; j < n; j++)
        {
         if(symbols[j] == deals[i].symbol && hours[j] == deals[i].hour_utc)
           {
            idx = j;
            break;
           }
        }
      if(idx == -1)
        {
         ::ArrayResize(symbols,     n + 1);
         ::ArrayResize(hours,       n + 1);
         ::ArrayResize(trade_count, n + 1);
         ::ArrayResize(win_count,   n + 1);
         ::ArrayResize(win_sum,     n + 1);
         ::ArrayResize(win_n,       n + 1);
         ::ArrayResize(loss_sum,    n + 1);
         ::ArrayResize(loss_n,      n + 1);
         idx = n;
         symbols[idx]     = deals[i].symbol;
         hours[idx]       = deals[i].hour_utc;
         trade_count[idx] = 0;
         win_count[idx]   = 0;
         win_sum[idx]     = 0.0;
         win_n[idx]       = 0;
         loss_sum[idx]    = 0.0;
         loss_n[idx]      = 0;
        }
      trade_count[idx]++;
      //--- accumulate win and loss totals separately for the edge ratio calculation.
      //--- a trade that closed at exactly zero profit counts toward
      //--- trade_count above but is neither a win nor a loss, since
      //--- classifying it as a loss with a zero magnitude would let
      //--- loss_n become positive while loss_sum stays at 0.0, causing
      //--- a division by zero when the average loss is computed below
      if(deals[i].profit > 0.0)
        {
         win_count[idx]++;
         win_sum[idx] += deals[i].profit;
         win_n[idx]++;
        }
      else
         if(deals[i].profit < 0.0)
           {
            loss_sum[idx] += ::MathAbs(deals[i].profit);
            loss_n[idx]++;
           }
     }
//--- compute win rate and edge ratio for every cell found
   int cell_count = ::ArraySize(symbols);
   ::ArrayResize(cells_out, cell_count);
   for(int i = 0; i < cell_count; i++)
     {
      cells_out[i].symbol      = symbols[i];
      cells_out[i].hour        = hours[i];
      cells_out[i].trade_count = trade_count[i];
      cells_out[i].win_count   = win_count[i];
      cells_out[i].win_rate_percent =
         (trade_count[i] > 0) ? ((double)win_count[i] / (double)trade_count[i]) * 100.0 : 0.0;
      //--- a ratio only exists where the cell has both a win and a loss to compare
      bool has_rr = (win_n[i] > 0 && loss_n[i] > 0);
      cells_out[i].has_rr = has_rr;
      if(has_rr)
        {
         double avg_win  = win_sum[i]  / win_n[i];
         double avg_loss = loss_sum[i] / loss_n[i];
         cells_out[i].avg_rr = avg_win / avg_loss;
        }
      else
         cells_out[i].avg_rr = 0.0;
     }
   return(cell_count);
  }

#endif // HEATMAPAGGREGATOR_MQH
//+------------------------------------------------------------------+