//+------------------------------------------------------------------+
//|                                              CTimeFeatures.mqh   |
//|                            Feature Engineering for ML — Part 4   |
//|                           Patrick M. Njoroge — Blueprint Quant   |
//+------------------------------------------------------------------+
//|  Computes time-based ML features for each bar, mirroring the     |
//|  Python get_time_features() function from trading_session.py.    |
//|                                                                  |
//|  Feature groups:                                                 |
//|    1. Non-time bar duration (bar_duration, bar_duration_accel)   |
//|       prepended when is_time_bar=false                           |
//|    2. Cyclical Fourier encoding — hour, day-of-week, day-of-year |
//|    3. Forex session flags — Sydney, Tokyo, London, New York,     |
//|       and the overlap indicator                                  |
//|    4. Calendar effects — Friday NY close, Sunday open,           |
//|       month-end, quarter-end  (D1 / W1 / MN1 only)               |
//|    5. Session-conditional rolling volatility — 9 ring buffers,   |
//|       one per session/calendar column, 20-bar window             |
//|                                                                  |
//|  Feature order exactly matches Python output. Pass features[]    |
//|  directly to OnnxRun() or write to CSV for offline analysis.     |
//+------------------------------------------------------------------+
#ifndef __C_TIME_FEATURES_MQH__
#define __C_TIME_FEATURES_MQH__
#property strict

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define TF_VOL_WINDOW    20   // rolling window for session volatility (bars)
#define TF_VOL_BUF_COUNT  9   // 4 sessions + overlap + 4 calendar effects

//--- Vol buffer indices — same column order as Python's session_feat
#define VB_SYDNEY       0
#define VB_TOKYO        1
#define VB_LONDON       2
#define VB_NY           3
#define VB_OVERLAP      4
#define VB_FRIDAY       5
#define VB_SUNDAY_OPEN  6
#define VB_MONTH_END    7
#define VB_QUARTER_END  8

//+------------------------------------------------------------------+
//| CRingBuffer — fixed-capacity circular buffer; rolling std        |
//+------------------------------------------------------------------+
class CRingBuffer
  {
private:
   double            m_data[];
   int               m_size;
   int               m_head;
   int               m_count;

public:
   //--- Constructor: initialize to empty state
                     CRingBuffer(void) : m_size(0), m_head(0), m_count(0)
     {
     }

   //+------------------------------------------------------------------+
   //| Init: allocate buffer to capacity and reset all state            |
   //+------------------------------------------------------------------+
   void              Init(const int capacity)
     {
      m_size = MathMax(capacity, 2);
      ArrayResize(m_data, m_size);
      ArrayInitialize(m_data, 0.0);
      m_head  = 0;
      m_count = 0;
     }

   //+------------------------------------------------------------------+
   //| Push: append value at head position and advance the head index   |
   //+------------------------------------------------------------------+
   void              Push(const double value)
     {
      m_data[m_head] = value;
      m_head         = (m_head + 1) % m_size;
      if(m_count < m_size)
         m_count++;
     }

   //+------------------------------------------------------------------+
   //| Std: population standard deviation (ddof=0) of buffered values   |
   //| Matches NumPy default used in Python's rolling volatility calc   |
   //+------------------------------------------------------------------+
   double            Std(void) const
     {
      if(m_count < 2)
         return(0.0);
      double sum = 0.0, ssq = 0.0;
      for(int i = 0; i < m_count; i++)
        {
         sum += m_data[i];
         ssq += m_data[i] * m_data[i];
        }
      double mean = sum / m_count;
      double var  = ssq / m_count - mean * mean;
      return(var > 0.0 ? MathSqrt(var) : 0.0);
     }

   //+------------------------------------------------------------------+
   //| Reset: clear values without reallocating the buffer              |
   //+------------------------------------------------------------------+
   void              Reset(void)
     {
      m_head  = 0;
      m_count = 0;
      ArrayInitialize(m_data, 0.0);
     }

   int               Count(void)  const { return(m_count); }
   bool              IsWarm(void) const { return(m_count >= TF_VOL_WINDOW); }
  };

//+------------------------------------------------------------------+
//| CTimeFeatures — bar-by-bar time feature generator                |
//+------------------------------------------------------------------+
class CTimeFeatures
  {
private:
   //--- Configuration
   ENUM_TIMEFRAMES   m_timeframe;
   int               m_n_terms;
   bool              m_forex;
   bool              m_is_time_bar;         // false → prepend bar_duration / bar_duration_accel
   int               m_gmt_offset;          // seconds added to broker time → UTC

   //--- Frequency gate flags (derived from timeframe at Initialize)
   bool              m_extra_hour_harms;    // true for sub-hourly bars
   bool              m_include_calendar;    // true for D1 and higher

   //--- Feature metadata
   int               m_feature_count;
   string            m_names[];

   //--- Session boundaries (UTC, start inclusive / end exclusive)
   //    Layout: [0]=Sydney [1]=Tokyo [2]=London [3]=New York
   int               m_s_start[4];
   int               m_s_end[4];
   bool              m_s_xmid[4];           // true if window crosses midnight

   //--- Volatility state (9 ring buffers, one per session/calendar column)
   CRingBuffer       m_buf[TF_VOL_BUF_COUNT];
   double            m_last_vol[TF_VOL_BUF_COUNT];
   double            m_prev_close;
   bool              m_prev_valid;

   //--- Non-time bar duration state
   datetime          m_prev_bar_time;       // broker timestamp of the previous closed bar
   double            m_prev_duration;       // previous interval in seconds
   double            m_last_duration;       // bar_duration for the current bar (read by Calculate)
   double            m_last_duration_accel; // bar_duration_accel for the current bar

public:
                     CTimeFeatures(void);
                    ~CTimeFeatures(void)
     {
     }

   //--- Call once from EA OnInit()
   bool              Initialize(
      const ENUM_TIMEFRAMES timeframe   = PERIOD_H1,
      const int             n_terms     = 3,
      const bool            forex       = true,
      const bool            is_time_bar = true
      );

   //--- Call on every CLOSED bar (bar index 1 in OnTick, or from warmup loop)
   void              Update(const double close_price, const datetime bar_broker_time);

   //--- Fill features[] for the bar opening at bar_broker_time.
   //--- Call AFTER Update() for the previous bar.
   bool              Calculate(const datetime bar_broker_time, double &features[]);

   int               FeatureCount(void) const { return(m_feature_count); }
   void              FeatureNames(string &names[]) const;

private:
   datetime          _UTC(const datetime t) const { return(t + m_gmt_offset); }
   bool              _InSession(const int h, const int s) const;
   void              _Activity(const int hour, const MqlDateTime &dt, bool &active[]) const;
   void              _BuildNames(void);
   bool              _IsSubHourly(void) const;
   bool              _IsDailyPlus(void) const;
  };

//+------------------------------------------------------------------+
//| Constructor: initialize members to safe defaults                 |
//+------------------------------------------------------------------+
CTimeFeatures::CTimeFeatures(void)
   : m_timeframe(PERIOD_H1),
     m_n_terms(3),
     m_forex(true),
     m_is_time_bar(true),
     m_gmt_offset(0),
     m_extra_hour_harms(false),
     m_include_calendar(false),
     m_feature_count(0),
     m_prev_close(0.0),
     m_prev_valid(false),
     m_prev_bar_time(0),
     m_prev_duration(0.0),
     m_last_duration(0.0),
     m_last_duration_accel(0.0)
  {
   ArrayInitialize(m_last_vol, 0.0);

   //--- Session boundaries in UTC (start inclusive, end exclusive)
   m_s_start[VB_SYDNEY] = 21;  m_s_end[VB_SYDNEY] = 6;  m_s_xmid[VB_SYDNEY] = true;
   m_s_start[VB_TOKYO]  =  0;  m_s_end[VB_TOKYO]  = 9;  m_s_xmid[VB_TOKYO]  = false;
   m_s_start[VB_LONDON] =  7;  m_s_end[VB_LONDON] = 16; m_s_xmid[VB_LONDON] = false;
   m_s_start[VB_NY]     = 13;  m_s_end[VB_NY]     = 22; m_s_xmid[VB_NY]     = false;
  }

//+------------------------------------------------------------------+
//| Initialize: capture UTC offset, set gate flags, build registry   |
//+------------------------------------------------------------------+
bool CTimeFeatures::Initialize(
   const ENUM_TIMEFRAMES timeframe,
   const int             n_terms,
   const bool            forex,
   const bool            is_time_bar
   )
  {
   m_timeframe   = timeframe;
   m_n_terms     = MathMax(1, n_terms);
   m_forex       = forex;
   m_is_time_bar = is_time_bar;

   //--- Capture broker-to-UTC offset at init time.
   //--- IMPORTANT: this offset is fixed for the lifetime of the instance.
   //--- Brokers that observe DST (UTC+2 winter / UTC+3 summer) produce
   //--- ±1 h session classification errors during the transition weeks
   //--- in March and October. Use a UTC-only broker to avoid this.
   m_gmt_offset = (int)(TimeGMT() - TimeCurrent());

   m_extra_hour_harms = _IsSubHourly();
   m_include_calendar = _IsDailyPlus();

   for(int i = 0; i < TF_VOL_BUF_COUNT; i++)
     {
      m_buf[i].Init(TF_VOL_WINDOW);
      m_last_vol[i] = 0.0;
     }

   m_prev_valid          = false;
   m_prev_bar_time       = 0;
   m_prev_duration       = 0.0;
   m_last_duration       = 0.0;
   m_last_duration_accel = 0.0;

   _BuildNames();
   m_feature_count = ArraySize(m_names);
   return(m_feature_count > 0);
  }

//+------------------------------------------------------------------+
//| IsSubHourly: true when the configured timeframe is below H1      |
//+------------------------------------------------------------------+
bool CTimeFeatures::_IsSubHourly(void) const
  {
   return(PeriodSeconds(m_timeframe) < PeriodSeconds(PERIOD_H1));
  }

//+------------------------------------------------------------------+
//| IsDailyPlus: true when the configured timeframe is D1 or higher  |
//+------------------------------------------------------------------+
bool CTimeFeatures::_IsDailyPlus(void) const
  {
   return(PeriodSeconds(m_timeframe) >= PeriodSeconds(PERIOD_D1));
  }

//+------------------------------------------------------------------+
//| InSession: true when UTC hour h falls inside session s           |
//+------------------------------------------------------------------+
bool CTimeFeatures::_InSession(const int h, const int s) const
  {
   if(m_s_xmid[s])
      return(h >= m_s_start[s] || h < m_s_end[s]);
   return(h >= m_s_start[s] && h < m_s_end[s]);
  }

//+------------------------------------------------------------------+
//| Activity: populate the 9 active-buffer flags for a given bar     |
//+------------------------------------------------------------------+
void CTimeFeatures::_Activity(
   const int         hour,
   const MqlDateTime &dt,
   bool              &active[]
   ) const
  {
   //--- MQL5 day_of_week: 0=Sunday; Python dayofweek: 0=Monday
   int py_dow = (dt.day_of_week == 0) ? 6 : dt.day_of_week - 1;

   //--- Four main sessions
   int n_active = 0;
   for(int i = VB_SYDNEY; i <= VB_NY; i++)
     {
      active[i] = _InSession(hour, i);
      if(active[i])
         n_active++;
     }

   //--- Overlap: two or more sessions simultaneously active
   active[VB_OVERLAP] = (n_active > 1);

   //--- Calendar effects
   //--- friday_ny_close: Python day_of_week==4 (Fri) and hour>=21
   //--- sunday_open:     Python day_of_week==6 (Sun) and hour<=2
   //--- month_end:       Python day >= 28
   //--- quarter_end:     Python month%3==0 and day>=28
   active[VB_FRIDAY]      = (py_dow == 4 && hour >= 21);
   active[VB_SUNDAY_OPEN] = (dt.day_of_week == 0 && hour <= 2);
   active[VB_MONTH_END]   = (dt.day >= 28);
   active[VB_QUARTER_END] = (dt.mon % 3 == 0 && dt.day >= 28);
  }

//+------------------------------------------------------------------+
//| Update: push log return into active session volatility buffers   |
//| Call on every CLOSED bar before the corresponding Calculate call |
//+------------------------------------------------------------------+
void CTimeFeatures::Update(
   const double   close_price,
   const datetime bar_broker_time
   )
  {
   if(!m_prev_valid)
     {
      m_prev_close    = close_price;
      m_prev_bar_time = bar_broker_time;
      m_prev_valid    = true;
      return;
     }

   double log_ret = 0.0;
   if(m_prev_close > 0.0 && close_price > 0.0)
      log_ret = MathLog(close_price / m_prev_close);

   //--- Non-time bar: compute bar_duration and bar_duration_accel.
   //--- Matches Python: durations = df.index.diff().dt.total_seconds()
   //---                 duration_accel = durations.diff()
   if(!m_is_time_bar)
     {
      double dur            = (double)(bar_broker_time - m_prev_bar_time);
      m_last_duration_accel = (m_prev_duration > 0.0) ? dur - m_prev_duration : 0.0;
      m_prev_duration       = dur;
      m_last_duration       = dur;
      m_prev_bar_time       = bar_broker_time;
     }

   datetime    utc = _UTC(bar_broker_time);
   MqlDateTime dt  = {};
   TimeToStruct(utc, dt);

   bool active[];
   ArrayResize(active, TF_VOL_BUF_COUNT);
   _Activity(dt.hour, dt, active);

   for(int i = 0; i < TF_VOL_BUF_COUNT; i++)
     {
      if(active[i])
        {
         m_buf[i].Push(log_ret);
         m_last_vol[i] = m_buf[i].Std();
        }
      // else: forward-fill — m_last_vol[i] holds the previous session's estimate
     }

   m_prev_close = close_price;
  }

//+------------------------------------------------------------------+
//| Calculate: fill the feature vector for the bar at bar_broker_time|
//| Call AFTER Update() for the previous closed bar                  |
//+------------------------------------------------------------------+
bool CTimeFeatures::Calculate(
   const datetime  bar_broker_time,
   double          &features[]
   )
  {
   if(ArrayResize(features, m_feature_count) != m_feature_count)
      return(false);
   ArrayInitialize(features, 0.0);

   datetime    utc = _UTC(bar_broker_time);
   MqlDateTime dt  = {};
   TimeToStruct(utc, dt);

   int hour   = dt.hour;
   int py_dow = (dt.day_of_week == 0) ? 6 : dt.day_of_week - 1;
   int py_doy = dt.day_of_year + 1;   // MQL5: 0-based; Python: 1-based

   bool active[];
   ArrayResize(active, TF_VOL_BUF_COUNT);
   _Activity(hour, dt, active);

   int idx = 0;

   //--- 0. Non-time bar duration features (prepended — matches Python feature order)
   if(!m_is_time_bar)
     {
      features[idx++] = m_last_duration;
      features[idx++] = m_last_duration_accel;
     }

   //--- 1. Cyclical features
   //--- Hour: extra harmonics for sub-hourly bars; base harmonic only for H1+
   int hour_harms = m_extra_hour_harms ? m_n_terms : 1;
   for(int k = 1; k <= hour_harms; k++)
     {
      double ang = 2.0 * M_PI * k * hour / 24.0;
      features[idx++] = MathSin(ang);
      features[idx++] = MathCos(ang);
     }

   //--- Day-of-week (base harmonic, cycle 7)
   double dow_ang = 2.0 * M_PI * py_dow / 7.0;
   features[idx++] = MathSin(dow_ang);
   features[idx++] = MathCos(dow_ang);

   //--- Day-of-year (base harmonic, cycle 366)
   double doy_ang = 2.0 * M_PI * py_doy / 366.0;
   features[idx++] = MathSin(doy_ang);
   features[idx++] = MathCos(doy_ang);

   if(!m_forex)
      return(true);

   //--- 2. Session flags
   features[idx++] = active[VB_SYDNEY]  ? 1.0 : 0.0;
   features[idx++] = active[VB_TOKYO]   ? 1.0 : 0.0;
   features[idx++] = active[VB_LONDON]  ? 1.0 : 0.0;
   features[idx++] = active[VB_NY]      ? 1.0 : 0.0;
   features[idx++] = active[VB_OVERLAP] ? 1.0 : 0.0;

   //--- 3. Calendar effects (D1, W1, MN1 only)
   if(m_include_calendar)
     {
      features[idx++] = active[VB_FRIDAY]      ? 1.0 : 0.0;
      features[idx++] = active[VB_SUNDAY_OPEN] ? 1.0 : 0.0;
      features[idx++] = active[VB_MONTH_END]   ? 1.0 : 0.0;
      features[idx++] = active[VB_QUARTER_END] ? 1.0 : 0.0;
     }

   //--- 4. Session volatility (all 9 buffers, always present)
   //--- Python's frequency gate drops the 4 calendar FLAG columns for
   //--- sub-daily timeframes but retains their volatility counterparts.
   //--- This matches that behavior exactly: volatility columns are unconditional.
   for(int i = 0; i < TF_VOL_BUF_COUNT; i++)
      features[idx++] = m_last_vol[i];

   return(true);
  }

//+------------------------------------------------------------------+
//| BuildNames: construct the feature-name registry                  |
//+------------------------------------------------------------------+
void CTimeFeatures::_BuildNames(void)
  {
   string tmp[];
   int idx = 0;

   //--- Non-time bar duration features (prepended — matches Python feature order)
   if(!m_is_time_bar)
     {
      ArrayResize(tmp, idx + 2);
      tmp[idx++] = "bar_duration";
      tmp[idx++] = "bar_duration_accel";
     }

   //--- Hour cyclical — suffix _h{k} for sub-hourly, no suffix for H1+
   int hour_harms = m_extra_hour_harms ? m_n_terms : 1;
   for(int k = 1; k <= hour_harms; k++)
     {
      string sfx = m_extra_hour_harms ? ("_h" + IntegerToString(k)) : "";
      ArrayResize(tmp, idx + 2);
      tmp[idx++] = "hour_sin" + sfx;
      tmp[idx++] = "hour_cos" + sfx;
     }

   //--- Day-of-week and day-of-year (base harmonic, no suffix)
   ArrayResize(tmp, idx + 4);
   tmp[idx++] = "dayofweek_sin";
   tmp[idx++] = "dayofweek_cos";
   tmp[idx++] = "dayofyear_sin";
   tmp[idx++] = "dayofyear_cos";

   if(m_forex)
     {
      //--- Session flags
      ArrayResize(tmp, idx + 5);
      tmp[idx++] = "sydney_session";
      tmp[idx++] = "tokyo_session";
      tmp[idx++] = "london_session";
      tmp[idx++] = "ny_session";
      tmp[idx++] = "session_overlap";

      //--- Calendar flags — present only for D1 and higher
      if(m_include_calendar)
        {
         ArrayResize(tmp, idx + 4);
         tmp[idx++] = "friday_ny_close";
         tmp[idx++] = "sunday_open";
         tmp[idx++] = "month_end";
         tmp[idx++] = "quarter_end";
        }

      //--- All 9 volatility columns — always present regardless of timeframe
      ArrayResize(tmp, idx + TF_VOL_BUF_COUNT);
      tmp[idx++] = "sydney_session_vol";
      tmp[idx++] = "tokyo_session_vol";
      tmp[idx++] = "london_session_vol";
      tmp[idx++] = "ny_session_vol";
      tmp[idx++] = "session_overlap_vol";
      tmp[idx++] = "friday_ny_close_vol";
      tmp[idx++] = "sunday_open_vol";
      tmp[idx++] = "month_end_vol";
      tmp[idx++] = "quarter_end_vol";
     }

   ArrayResize(m_names, idx);
   ArrayCopy(m_names, tmp);
  }

//+------------------------------------------------------------------+
//| FeatureNames: copy the feature-name registry to names[]          |
//+------------------------------------------------------------------+
void CTimeFeatures::FeatureNames(string &names[]) const
  {
   int n = ArraySize(m_names);
   ArrayResize(names, n);
   ArrayCopy(names, m_names);
  }

#endif // __C_TIME_FEATURES_MQH__
