//+------------------------------------------------------------------+
//|                                    SymbolCorrelationMonitor.mq5  |
//+------------------------------------------------------------------+
#property description "Computes a rolling Pearson correlation corr_matrix across the "
#property description "symbols of every currently open position and renders it as "
#property description "a live CCanvas heatmap, redrawn on every new bar."
#property strict

#include <SymbolCorrelationMonitor/CorrelationTypes.mqh>
#include <SymbolCorrelationMonitor/PositionSymbolCollector.mqh>
#include <SymbolCorrelationMonitor/ReturnSeriesReader.mqh>
#include <SymbolCorrelationMonitor/PearsonCorrelationCalculator.mqh>
#include <SymbolCorrelationMonitor/CorrelationHeatmapChart.mqh>
#include <SymbolCorrelationMonitor/CorrelationSummaryPrinter.mqh>

input ENUM_TIMEFRAMES InpTimeframe             = PERIOD_H1;  // timeframe the rolling return window is measured on
input int             InpBarCount              = 50;         // number of bar-to-bar returns in the rolling window
input double          InpCorrelationThreshold  = 0.8;        // absolute correlation at or above which a pair is flagged
input int             InpMaxSymbols            = 12;         // safety cap on distinct open-position symbols handled

//--- the heatmap panel is declared at global scope, not created fresh on every
//--- call, since an Expert Advisor's canvas must persist and simply be redrawn
//--- across many bars rather than recreated the way a one-shot script would
CCorrelationHeatmapChart g_heatmap_chart("CorrelationMonitor_Heatmap");
datetime                 g_last_bar_time = 0;
string                   g_last_symbols[]; // the open-position symbol set as of the last update

//+-------------------------------------------------------------------------+
//| SymbolListsEqual                                                        |
//| Compares two symbol lists as sets rather than as ordered sequences,     |
//| since CPositionSymbolCollector::Collect() walks positions in whatever   |
//| order the terminal happens to report them in, and that order can change |
//| between calls even when the underlying set of symbols has not. Two lists|
//| of different sizes are never equal; otherwise, every symbol in the first|
//| list must also appear somewhere in the second.                          |
//+-------------------------------------------------------------------------+
bool SymbolListsEqual(const string &list_a[],const string &list_b[])
  {
   int size_a = ::ArraySize(list_a);
   int size_b = ::ArraySize(list_b);
   if(size_a != size_b)
      return(false);
   for(int i = 0; i < size_a; i++)
     {
      bool found = false;
      for(int j = 0; j < size_b; j++)
        {
         if(list_a[i] == list_b[j])
           {
            found = true;
            break;
           }
        }
      if(!found)
         return(false);
     }
   return(true);
  }

//+-------------------------------------------------------------------------+
//| UpdateAndRender                                                         |
//| Runs the full pipeline once: collect the open positions' symbols,       |
//| cap the list at InpMaxSymbols, build a return series per symbol,        |
//| drop any symbol whose series could not be built, compute the correlation|
//| corr_matrix from whatever symbols remain, print the summary, and redraw |
//| the heatmap. A symbol dropped for insufficient bar history simply does  |
//| not appear in the corr_matrix; it is not treated as a fatal error.      |
//+-------------------------------------------------------------------------+
void UpdateAndRender(void)
  {
   CPositionSymbolCollector collector;
   string symbols[];
   collector.Collect(symbols);

   int total_symbols = ::ArraySize(symbols);
   if(total_symbols > InpMaxSymbols)
     {
      ::PrintFormat("SymbolCorrelationMonitor: %d open-position symbols found, truncating to the first %d.",
                    total_symbols, InpMaxSymbols);
      ::ArrayResize(symbols, InpMaxSymbols);
      total_symbols = InpMaxSymbols;
     }

//--- build a return series per symbol, keeping only the symbols that succeed
   CReturnSeriesReader   reader;
   CSymbolReturnSeries   built_series[];
   for(int i = 0; i < total_symbols; i++)
     {
      CSymbolReturnSeries candidate;
      if(reader.BuildSeries(symbols[i], InpTimeframe, InpBarCount, candidate))
        {
         int index = ::ArraySize(built_series);
         ::ArrayResize(built_series, index + 1);
         built_series[index].m_symbol = candidate.m_symbol;
         built_series[index].m_count  = candidate.m_count;
         ::ArrayCopy(built_series[index].m_returns, candidate.m_returns);
        }
      else
        {
         ::PrintFormat("SymbolCorrelationMonitor: skipping %s, insufficient bar history for a %d-bar window.",
                       symbols[i], InpBarCount);
        }
     }

   CPearsonCorrelationCalculator calculator;
   CCorrelationMatrix            corr_matrix;
   bool matrix_built = calculator.BuildMatrix(built_series, ::ArraySize(built_series), corr_matrix);

//--- only a genuine data failure short-circuits the update: symbols were open,
//--- but none of them produced a usable return series. Zero open positions is
//--- not a failure, and falls through to the normal empty-matrix handling in
//--- the printer and the chart window.
   if(!matrix_built && total_symbols > 0)
     {
      ::PrintFormat("SymbolCorrelationMonitor: %d open-position symbols found, but no return series could be built from them.",
                    total_symbols);
      return;
     }

   CCorrelationSummaryPrinter printer;
   printer.Print(corr_matrix, InpCorrelationThreshold);

   int panel_size = 90 + corr_matrix.Size() * 60 + 40;
   g_heatmap_chart.Draw(corr_matrix, InpCorrelationThreshold, 20, 20, panel_size, panel_size);
   ::ChartRedraw(0);
  }

//+---------------------------------------------------------------------------+
//| Expert initialization function                                            |
//| Renders the heatmap once immediately, rather than waiting for the         |
//| first new bar, so the panel is visible as soon as the Expert Advisor      |
//| is attached, then records the open-position symbol set so the next        |
//| tick's comparison in OnTick() has something meaningful to compare against.|
//+---------------------------------------------------------------------------+
int OnInit(void)
  {
//--- reject configurations that cannot produce a meaningful result
   if(InpBarCount < 2)
     {
      ::Print("SymbolCorrelationMonitor: InpBarCount must be at least 2.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(InpCorrelationThreshold < 0.0 || InpCorrelationThreshold > 1.0)
     {
      ::Print("SymbolCorrelationMonitor: InpCorrelationThreshold must be between 0.0 and 1.0.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(InpMaxSymbols < 1)
     {
      ::Print("SymbolCorrelationMonitor: InpMaxSymbols must be at least 1.");
      return(INIT_PARAMETERS_INCORRECT);
     }

   ::ArrayResize(g_last_symbols, 0);
   UpdateAndRender();
//--- record the bar this initial render was based on, so the first OnTick()
//--- after attaching does not mistake a real bar time for something different
//--- from the uninitialized zero and immediately rerun the whole pipeline a
//--- second time for no actual reason.
   g_last_bar_time = ::iTime(_Symbol, InpTimeframe, 0);
   CPositionSymbolCollector collector;
   collector.Collect(g_last_symbols);
   return(INIT_SUCCEEDED);
  }

//+-----------------------------------------------------------------------+
//| Expert deinitialization function                                      |
//| Explicitly tears down the heatmap panel. An Expert Advisor persists   |
//| for as long as it stays attached to the chart, unlike a script that   |
//| runs once and finishes, so its own removal is the natural, deliberate |
//| moment to call Clear().                                               |
//+-----------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   g_heatmap_chart.Clear();
  }

//+-------------------------------------------------------------------------+
//| Expert tick function                                                    |
//| Recomputes and redraws whenever either of two things happens: a new     |
//| InpTimeframe bar opens on this chart's own symbol, used purely as a     |
//| timing signal, or the set of symbols currently held in open positions   |
//| changes, whether from opening a new position, closing one, or reversing |
//| between symbols. Checking the position set on every tick is deliberate: |
//| a trader who just opened a new position expects the heatmap to reflect  |
//| that immediately, not only after the next InpTimeframe bar happens to   |
//| close, which on a higher timeframe could be a long wait. Scanning the   |
//| open positions every tick is inexpensive compared to the return-series  |
//| and correlation work that follows, so this check costs little even when |
//| nothing has actually changed.                                           |
//+-------------------------------------------------------------------------+
void OnTick(void)
  {
   datetime current_bar_time = ::iTime(_Symbol, InpTimeframe, 0);
   bool     is_new_bar       = (current_bar_time != g_last_bar_time);

   CPositionSymbolCollector collector;
   string current_symbols[];
   collector.Collect(current_symbols);
   bool positions_changed = !SymbolListsEqual(current_symbols, g_last_symbols);

   if(!is_new_bar && !positions_changed)
      return;

   g_last_bar_time = current_bar_time;
//--- ArrayCopy() never shrinks a destination array, only grows it when needed,
//--- so g_last_symbols is reset to empty first; otherwise a symbol count that
//--- just decreased (a position closed) would leave stale trailing entries
//--- behind, which would make every subsequent tick look like a false change
//--- and trigger UpdateAndRender() forever.
   ::ArrayResize(g_last_symbols, 0);
   ::ArrayCopy(g_last_symbols, current_symbols);
   UpdateAndRender();
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
