﻿//+------------------------------------------------------------------+
//|                                         MultiTimeframeMatrix.mqh |
//|                                Copyright 2025, Clemence Benjamin |
//|                                                     www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Clemence Benjaminr"
#property version   "1.0"

#include <Controls/Dialog.mqh>
#include <Controls/Label.mqh>
#include <Math/Alglib/matrix.mqh>

//+------------------------------------------------------------------+
//| CMultiTimeframeMatrix class – displays a dashboard of market scores |
//+------------------------------------------------------------------+
class CMultiTimeframeMatrix : public CAppDialog
  {
private:
   //--- Data
   string            m_symbols[];
   ENUM_TIMEFRAMES   m_timeframes[];

   //--- UI components
   CLabel*           m_cells[];
   CLabel*           m_rowHeaders[];
   CLabel*           m_colHeaders[];
   CLabel*           m_guidance1, *m_guidance2, *m_guidance3, *m_guidance4;

   //--- Score storage
   double            m_lastValues[];
   double            m_prevValues[];

   //--- Performance
   uint              m_updateIntervalMs;
   bool              m_dirty;
   uint              m_cycleCount;
   ulong             m_totalCycleTime;

   //--- Score parameters
   int               m_windowSize;
   double            m_trendWeight, m_momWeight, m_volWeight, m_scaleFactor;

   //--- Layout
   int               m_cellW, m_cellH;
   int               m_startX, m_startY;

   //--- Helpers
   int               Index(int row, int col) const;
   double            ComputeScore(string symbol, ENUM_TIMEFRAMES tf, int shift);
   double            CalculateTrend(CMatrixDouble &prices);
   double            CalculateMomentum(CMatrixDouble &prices);
   double            CalculateVolatility(CMatrixDouble &prices);
   void              FetchAllData();
   void              UpdateUICell(int idx, double value);
   color             ScoreToColor(double score);
   void              CreateGuidance();
   string            TfToString(ENUM_TIMEFRAMES tf);

public:
                     CMultiTimeframeMatrix();
                    ~CMultiTimeframeMatrix();

   bool              Create(const long chart_id, const string name, const int subwin,
                            const int x, const int y, const int w, const int h);
   void              SetSymbols(string &syms[]);
   void              SetTimeframes(ENUM_TIMEFRAMES &tfs[]);
   void              SetUpdateInterval(const uint ms)     { m_updateIntervalMs = ms; }
   void              SetWindowSize(const int size)        { m_windowSize = size; }
   void              SetWeights(double t, double m, double v)
     { m_trendWeight = t; m_momWeight = m; m_volWeight = v; }
   void              SetScaleFactor(double f)             { m_scaleFactor = f; }

   uint              GetUpdateInterval() const            { return m_updateIntervalMs; }
   uint              GetCycleCount() const                { return m_cycleCount; }
   double            GetAvgCycleTime() const;
   void              ResetStats();

   void              OnTimer();          //--- called from EA
   void              UpdateData();       //--- force refresh

   //--- Expose show/hide for external control (hotkey)
   void              ToggleVisible()     { if(IsVisible()) Hide(); else Show(); }
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CMultiTimeframeMatrix::CMultiTimeframeMatrix()
   : m_updateIntervalMs(1000),
     m_dirty(false),
     m_cycleCount(0),
     m_totalCycleTime(0),
     m_windowSize(50),
     m_trendWeight(0.4),
     m_momWeight(0.3),
     m_volWeight(0.3),
     m_scaleFactor(100.0),
     m_cellW(70),
     m_cellH(20),
     m_startX(35),
     m_startY(30)
  {
   //--- Initialize all guidance pointers to NULL
   m_guidance1 = m_guidance2 = m_guidance3 = m_guidance4 = NULL;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CMultiTimeframeMatrix::~CMultiTimeframeMatrix()
  {
   for(int i = 0; i < ArraySize(m_cells); i++)
      if(m_cells[i] != NULL)
         delete m_cells[i];

   for(int i = 0; i < ArraySize(m_rowHeaders); i++)
      if(m_rowHeaders[i] != NULL)
         delete m_rowHeaders[i];

   for(int i = 0; i < ArraySize(m_colHeaders); i++)
      if(m_colHeaders[i] != NULL)
         delete m_colHeaders[i];

   if(m_guidance1 != NULL)
      delete m_guidance1;
   if(m_guidance2 != NULL)
      delete m_guidance2;
   if(m_guidance3 != NULL)
      delete m_guidance3;
   if(m_guidance4 != NULL)
      delete m_guidance4;
  }

//+------------------------------------------------------------------+
//| Converts timeframe enum to readable string                       |
//+------------------------------------------------------------------+
string CMultiTimeframeMatrix::TfToString(ENUM_TIMEFRAMES tf)
  {
   switch(tf)
     {
      case PERIOD_M1:   return("M1");
      case PERIOD_M5:   return("M5");
      case PERIOD_M15:  return("M15");
      case PERIOD_M30:  return("M30");
      case PERIOD_H1:   return("H1");
      case PERIOD_H4:   return("H4");
      case PERIOD_D1:   return("D1");
      case PERIOD_W1:   return("W1");
      case PERIOD_MN1:  return("MN1");
      default:          return("?");
     }
  }

//+------------------------------------------------------------------+
//| Creates the dashboard dialog and all its UI elements             |
//+------------------------------------------------------------------+
bool CMultiTimeframeMatrix::Create(const long chart_id, const string name, const int subwin,
                                   const int x, const int y, const int w, const int h)
  {
   int x2 = x + w, y2 = y + h;

   if(!CAppDialog::Create(chart_id, name, subwin, x, y, x2, y2))
      return(false);

   int rows = ArraySize(m_symbols);
   int cols = ArraySize(m_timeframes);

   if(rows == 0 || cols == 0)
     {
      Print("ERROR: No symbols or timeframes set");
      return(false);
     }

   int totalCells = rows * cols;
   ArrayResize(m_cells, totalCells);
   ArrayResize(m_lastValues, totalCells);
   ArrayResize(m_prevValues, totalCells);

   for(int i = 0; i < totalCells; i++)
     {
      m_lastValues[i] = EMPTY_VALUE;
      m_prevValues[i] = EMPTY_VALUE;
     }

   ArrayResize(m_rowHeaders, rows);
   ArrayResize(m_colHeaders, cols);

//--- Row headers (symbols)
   for(int row = 0; row < rows; row++)
     {
      m_rowHeaders[row] = new CLabel();
      int xp = m_startX;
      int yp = m_startY + (row + 1) * m_cellH;

      if(!m_rowHeaders[row].Create(chart_id, m_name + "_rh" + string(row), m_subwin, xp, yp, m_cellW, m_cellH))
         return(false);

      string obj = m_rowHeaders[row].Name();
      ::ObjectSetString(chart_id, obj, OBJPROP_TEXT, m_symbols[row]);
      ::ObjectSetInteger(chart_id, obj, OBJPROP_COLOR, clrBlack);
      ::ObjectSetInteger(chart_id, obj, OBJPROP_ANCHOR, ANCHOR_CENTER);
      Add(m_rowHeaders[row]);
     }

//--- Column headers (timeframes)
   for(int col = 0; col < cols; col++)
     {
      m_colHeaders[col] = new CLabel();
      int xp = m_startX + (col + 1) * m_cellW;
      int yp = m_startY;

      if(!m_colHeaders[col].Create(chart_id, m_name + "_ch" + string(col), m_subwin, xp, yp, m_cellW, m_cellH))
         return(false);

      string obj = m_colHeaders[col].Name();
      ::ObjectSetString(chart_id, obj, OBJPROP_TEXT, TfToString(m_timeframes[col]));
      ::ObjectSetInteger(chart_id, obj, OBJPROP_COLOR, clrBlack);
      ::ObjectSetInteger(chart_id, obj, OBJPROP_ANCHOR, ANCHOR_CENTER);
      Add(m_colHeaders[col]);
     }

//--- Interior cells
   for(int row = 0; row < rows; row++)
     {
      for(int col = 0; col < cols; col++)
        {
         int idx = Index(row, col);
         int xp = m_startX + (col + 1) * m_cellW;
         int yp = m_startY + (row + 1) * m_cellH;
         m_cells[idx] = new CLabel();

         if(!m_cells[idx].Create(chart_id, m_name + "_cell" + string(idx), m_subwin, xp, yp, m_cellW, m_cellH))
            return(false);

         string obj = m_cells[idx].Name();
         ::ObjectSetString(chart_id, obj, OBJPROP_TEXT, "---");
         ::ObjectSetInteger(chart_id, obj, OBJPROP_COLOR, clrLightGray);
         ::ObjectSetInteger(chart_id, obj, OBJPROP_ANCHOR, ANCHOR_CENTER);
         Add(m_cells[idx]);
        }
     }

   CreateGuidance();
   return(true);
  }

//+------------------------------------------------------------------+
//| Creates the guidance labels at the bottom of the dashboard       |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::CreateGuidance()
  {
   int rows = ArraySize(m_symbols);
   int cols = ArraySize(m_timeframes);
   int xp = m_startX;
   int baseY = m_startY + (rows + 1) * m_cellH + 5;
   int width = (cols + 1) * m_cellW;
   int h = m_cellH;

   m_guidance1 = new CLabel();
   m_guidance1.Create(m_chart_id, m_name + "_g1", m_subwin, xp, baseY, width, h);
   m_guidance1.Text("Higher Probability Setup: All timeframes show SAME colour");
   m_guidance1.Color(clrBlack);
   Add(m_guidance1);

   m_guidance2 = new CLabel();
   m_guidance2.Create(m_chart_id, m_name + "_g2", m_subwin, xp, baseY + h, width, h);
   m_guidance2.Text("(Blue = Bullish, Red = Bearish). Intensity = strength.");
   m_guidance2.Color(clrBlack);
   Add(m_guidance2);

   m_guidance3 = new CLabel();
   m_guidance3.Create(m_chart_id, m_name + "_g3", m_subwin, xp, baseY + 2 * h, width, h);
   m_guidance3.Text("Mixed colours suggest caution.");
   m_guidance3.Color(clrBlack);
   Add(m_guidance3);

   m_guidance4 = new CLabel();
   m_guidance4.Create(m_chart_id, m_name + "_g4", m_subwin, xp, baseY + 3 * h, width, h);
   m_guidance4.Text("Press 'M' to hide/show dashboard. Hover for exact score.");
   m_guidance4.Color(clrBlack);
   Add(m_guidance4);
  }

//+------------------------------------------------------------------+
//| Returns the flat index from row and column                       |
//+------------------------------------------------------------------+
int CMultiTimeframeMatrix::Index(int row, int col) const
  {
   return(row * ArraySize(m_timeframes) + col);
  }

//+------------------------------------------------------------------+
//| Calculates the linear trend slope from a price matrix            |
//+------------------------------------------------------------------+
double CMultiTimeframeMatrix::CalculateTrend(CMatrixDouble &prices)
  {
   int n = prices.Rows();

   if(n <= 1)
      return(0.0);

   CMatrixDouble x(n, 1);
   for(int i = 0; i < n; i++)
      x.Set(i, 0, i);

   double mx = x.Mean();
   double my = prices.Mean();
   double num = 0, den = 0;

   for(int i = 0; i < n; i++)
     {
      double xi = i - mx;
      double yi = prices.Get(i, 0) - my;
      num += xi * yi;
      den += xi * xi;
     }

   return(den == 0 ? 0 : num / den);
  }

//+------------------------------------------------------------------+
//| Calculates the momentum as change over the window                |
//+------------------------------------------------------------------+
double CMultiTimeframeMatrix::CalculateMomentum(CMatrixDouble &prices)
  {
   int n = prices.Rows();

   if(n < 2)
      return(0.0);

   return(prices.Get(n - 1, 0) - prices.Get(0, 0));
  }

//+------------------------------------------------------------------+
//| Calculates the volatility (standard deviation) of prices         |
//+------------------------------------------------------------------+
double CMultiTimeframeMatrix::CalculateVolatility(CMatrixDouble &prices)
  {
   return(prices.Std());
  }

//+------------------------------------------------------------------+
//| Computes the composite score for a given symbol and timeframe    |
//+------------------------------------------------------------------+
double CMultiTimeframeMatrix::ComputeScore(string symbol, ENUM_TIMEFRAMES tf, int shift)
  {
   int avail = Bars(symbol, tf);
   int sz = (int)MathMin(m_windowSize, avail);

   if(sz < 5)
      return(0.0);

   double closes[];

   if(CopyClose(symbol, tf, shift, sz, closes) != sz)
      return(0.0);

   CMatrixDouble mat(sz, 1);
   for(int i = 0; i < sz; i++)
      mat.Set(i, 0, closes[i]);

   double t = CalculateTrend(mat);
   double m = CalculateMomentum(mat);
   double v = CalculateVolatility(mat);
   double raw = m_trendWeight * t + m_momWeight * m - m_volWeight * v;

   return(raw * m_scaleFactor);
  }

//+------------------------------------------------------------------+
//| Maps a score value to a colour (blue/red intensity)              |
//+------------------------------------------------------------------+
color CMultiTimeframeMatrix::ScoreToColor(double scr)
  {
   if(scr > 50)
      scr = 50;
   if(scr < -50)
      scr = -50;

   if(scr > 0)
     {
      int b = (int)(255 * (scr / 50));
      int r = 255 - b;
      int g = 200 - (int)(120 * (scr / 50));
      if(g < 0)
         g = 0;
      return((color)((r << 16) | (g << 8) | b));
     }
   else
      if(scr < 0)
        {
         double a = -scr;
         int r = (int)(255 * (a / 50));
         int g = 200 - (int)(120 * (a / 50));
         int b = 255 - r;
         if(g < 0)
            g = 0;
         if(b < 0)
            b = 0;
         return((color)((r << 16) | (g << 8) | b));
        }
      else
         return(clrLightGray);
  }

//+------------------------------------------------------------------+
//| Updates a single UI cell with a new score value                  |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::UpdateUICell(int idx, double val)
  {
   string obj = m_cells[idx].Name();
   ::ObjectSetString(m_chart_id, obj, OBJPROP_TEXT, DoubleToString(val, 2));
   ::ObjectSetInteger(m_chart_id, obj, OBJPROP_COLOR, ScoreToColor(val));
   ::ObjectSetString(m_chart_id, obj, OBJPROP_TOOLTIP, "Score: " + DoubleToString(val, 4));
   m_prevValues[idx] = val;
  }

//+------------------------------------------------------------------+
//| Fetches all current data and updates changed cells               |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::FetchAllData()
  {
   bool change = false;
   int rows = ArraySize(m_symbols);
   int cols = ArraySize(m_timeframes);

   for(int r = 0; r < rows; r++)
     {
      string sym = m_symbols[r];

      for(int c = 0; c < cols; c++)
        {
         int idx = Index(r, c);
         double score = ComputeScore(sym, m_timeframes[c], 0);

         if(score == 0.0 && m_lastValues[idx] == EMPTY_VALUE)
            continue;

         if(MathAbs(score - m_lastValues[idx]) > 0.001)
           {
            m_lastValues[idx] = score;
            UpdateUICell(idx, score);
            change = true;
           }
        }
     }

   if(change)
      ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Timer handler – periodically updates the dashboard               |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::OnTimer()
  {
   static bool busy = false;

   if(busy)
      return;

   busy = true;

   ulong start = GetTickCount64();
   FetchAllData();
   ulong elapsed = GetTickCount64() - start;
   m_cycleCount++;
   m_totalCycleTime += elapsed;

   if(elapsed > m_updateIntervalMs * 0.8)
      Print("Warning: update took ", elapsed, " ms");

   busy = false;
  }

//+------------------------------------------------------------------+
//| Forces an immediate data refresh                                 |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::UpdateData()
  {
   FetchAllData();
  }

//+------------------------------------------------------------------+
//| Returns the average cycle time in milliseconds                   |
//+------------------------------------------------------------------+
double CMultiTimeframeMatrix::GetAvgCycleTime() const
  {
   if(m_cycleCount == 0)
      return(0.0);

   return((double)m_totalCycleTime / m_cycleCount);
  }

//+------------------------------------------------------------------+
//| Resets performance statistics                                     |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::ResetStats()
  {
   m_cycleCount = 0;
   m_totalCycleTime = 0;
  }

//+------------------------------------------------------------------+
//| Sets the symbols array (copied internally)                       |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::SetSymbols(string &syms[])
  {
   ArrayCopy(m_symbols, syms);
  }

//+------------------------------------------------------------------+
//| Sets the timeframes array (copied internally)                    |
//+------------------------------------------------------------------+
void CMultiTimeframeMatrix::SetTimeframes(ENUM_TIMEFRAMES &tfs[])
  {
   ArrayCopy(m_timeframes, tfs);
  }
//+------------------------------------------------------------------+