//+------------------------------------------------------------------+
//|                                  SpectralEntropyCalculator.mq5   |
//|                    Measuring What Matters: Portfolio Risk        |
//|                    Decomposition in MQL5 — Part 4                |
//|                                                                  |
//|  PURPOSE: Compute the spectral entropy of a multi-symbol         |
//|           portfolio's covariance matrix to produce a single-     |
//|           number diversification score. Compares two portfolio   |
//|           configurations side by side — one concentrated, one    |
//|           well-distributed — and flags low-entropy portfolios    |
//|           with a clear warning.                                  |
//+------------------------------------------------------------------+
#property copyright   "Measuring What Matters Series — Part 4"
#property link        ""
#property version     "1.01"
#property script_show_inputs

//--- Portfolio A inputs (concentrated — three correlated FX USD pairs)
input string          A_Symbol1     = "EURUSD";   // Portfolio A: Symbol 1
input string          A_Symbol2     = "GBPUSD";   // Portfolio A: Symbol 2
input string          A_Symbol3     = "AUDUSD";   // Portfolio A: Symbol 3

//--- Portfolio B inputs (diversified — FX, Gold, and an inverse pair)
input string          B_Symbol1     = "EURUSD";   // Portfolio B: Symbol 1
input string          B_Symbol2     = "XAUUSD";   // Portfolio B: Symbol 2
input string          B_Symbol3     = "USDJPY";   // Portfolio B: Symbol 3

//--- Shared parameters
input ENUM_TIMEFRAMES TF            = PERIOD_H1;  // Timeframe
input int             LookbackBars  = 100;        // Lookback period (bars)

//--- Entropy threshold: below this value the portfolio is flagged
input double          EntropyWarnThreshold = 0.75; // Entropy warning threshold (0-1)

//+------------------------------------------------------------------+
//| Helper: Print a formatted divider line                           |
//+------------------------------------------------------------------+
void PrintDivider(string ch = "-", int len = 55)
  {
   string line = "";
   for(int i = 0; i < len; i++)
      line += ch;
   Print(line);
  }

//+------------------------------------------------------------------+
//| Helper: Compute log returns                                      |
//+------------------------------------------------------------------+
bool ComputeLogReturns(const string symbol,
                       ENUM_TIMEFRAMES tf,
                       int bars,
                       vector &log_returns)
  {
   vector prices;
   if(!prices.CopyRates(symbol, tf, COPY_RATES_CLOSE, 1, bars + 1))
     {
      PrintFormat("ERROR: Could not copy rates for %s. Error: %d",
                  symbol, GetLastError());
      return false;
     }

   log_returns.Init(bars);

   for(int i = 0; i < bars; i++)
     {
      if(prices[i] <= 0.0 || prices[i + 1] <= 0.0)
        {
         PrintFormat("ERROR: Invalid price encountered for %s at index %d.",
                     symbol, i);
         return false;
        }

      log_returns[i] = MathLog(prices[i] / prices[i + 1]);
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Build covariance matrix from three symbol return vectors         |
//+------------------------------------------------------------------+
bool BuildCovMatrix(const string &symbols[], int n,
                    ENUM_TIMEFRAMES tf, int bars,
                    matrix &cov_out)
  {
   vector returns[];
   ArrayResize(returns, n);

   for(int i = 0; i < n; i++)
      if(!ComputeLogReturns(symbols[i], tf, bars, returns[i]))
         return false;

   ulong cols = returns[0].Size();
   matrix all_returns;
   all_returns.Init(n, cols);
   for(int i = 0; i < n; i++)
      all_returns.Row(returns[i], i);

   cov_out = all_returns.Cov();

   if(cov_out.Rows() != (ulong)n || cov_out.Cols() != (ulong)n)
     {
      PrintFormat("ERROR: Unexpected matrix shape [%dx%d].",
                  cov_out.Rows(), cov_out.Cols());
      return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Compute spectral entropy from a covariance matrix                |
//|                                                                  |
//| Spectral entropy measures how evenly variance is distributed     |
//| across eigenvalue factors. Formula:                              |
//|   1. Decompose cov into eigenvalues lambda[]                     |
//|   2. Normalize: p[i] = lambda[i] / sum(lambda)                   |
//|   3. Shannon entropy: H = -sum( p[i] * log(p[i]) )               |
//|   4. Normalize to [0,1]: H_norm = H / log(n)                     |
//|                                                                  |
//| H_norm = 1.0 -> perfectly uniform (maximum diversification)      |
//| H_norm = 0.0 -> all variance in one factor (full concentration)  |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Compute spectral entropy from a covariance matrix                |
//+------------------------------------------------------------------+
bool ComputeSpectralEntropy(const matrix &cov, int n,
                            vector &eigenvalues_out,
                            vector &proportions_out,
                            double &entropy_out,
                            double &entropy_norm_out)
  {
   matrix eigenvectors;
   vector eigenvalues;

   if(!cov.Eig(eigenvectors, eigenvalues))
     {
      Print("ERROR: Eigenvalue decomposition failed.");
      return false;
     }

//--- Compute total variance using native vector summation
   double total = eigenvalues.Sum();

   if(total <= 0.0)
     {
      Print("ERROR: Total variance is zero or negative.");
      return false;
     }

//--- Workaround: Use MQL5's native ArraySort() to avoid buggy vector.Sort()
   double eig_arr[];
   ArrayResize(eig_arr, n);
   for(int i = 0; i < n; i++)
      eig_arr[i] = eigenvalues[i];

   ArraySort(eig_arr); // Sorts ascending

//--- Read ascending array back into vector in descending order
   vector sorted_eigenvalues;
   sorted_eigenvalues.Init(n);
   for(int i = 0; i < n; i++)
      sorted_eigenvalues[i] = eig_arr[n - 1 - i];

//--- Compute proportions natively via element-wise vector division
   vector proportions = sorted_eigenvalues / total;

//--- Calculate Shannon entropy
   double entropy = 0.0;
   for(int i = 0; i < n; i++)
     {
      if(proportions[i] > 0.0)
         entropy -= proportions[i] * MathLog(proportions[i]);
     }

//--- Normalize entropy to [0, 1] by dividing by log(n)
   double max_entropy = MathLog((double)n);
   double entropy_norm = (max_entropy > 0.0) ? entropy / max_entropy : 0.0;

//--- Assign output vectors
   eigenvalues_out  = sorted_eigenvalues;
   proportions_out  = proportions;
   entropy_out      = entropy;
   entropy_norm_out = entropy_norm;

   return true;
  }

//+------------------------------------------------------------------+
//| Print full spectral entropy report for one portfolio             |
//+------------------------------------------------------------------+
void PrintEntropyReport(const string &symbols[], int n,
                        const string portfolio_label,
                        const vector &eigenvalues,
                        const vector &proportions,
                        double entropy,
                        double entropy_norm,
                        double warn_threshold)
  {
   PrintDivider("=");
   PrintFormat("  SPECTRAL ENTROPY REPORT — %s", portfolio_label);
   PrintDivider();

//--- Eigenvalue distribution table
   double total = eigenvalues.Sum();

   PrintFormat("  Total Portfolio Variance: %.8f", total);
   PrintDivider();
   Print("  Eigenvalue Distribution:");
   for(int i = 0; i < n; i++)
     {
      //--- ASCII bar: each * = 5% of variance
      int bar_len = (int)MathRound(proportions[i] * 100.0 / 5.0);
      string bar  = "";
      for(int b = 0; b < bar_len; b++)
         bar += "*";

      PrintFormat("  Factor %d | lambda = %+.8f | %6.2f%% | %s",
                  i + 1, eigenvalues[i], proportions[i] * 100.0, bar);
     }
   PrintDivider();

//--- Entropy calculation steps
   Print("  Entropy Calculation Steps:");
   PrintFormat("  Raw Shannon Entropy H      = %.6f nats", entropy);
   PrintFormat("  Maximum Entropy log(%d)     = %.6f nats", n, MathLog((double)n));
   PrintFormat("  Normalized Entropy H_norm  = %.6f  (%.2f%%)",
               entropy_norm, entropy_norm * 100.0);
   PrintDivider();

//--- Verdict
   if(entropy_norm < warn_threshold)
     {
      PrintFormat("  WARNING: Portfolio is CONCENTRATED.");
      PrintFormat("  H_norm = %.4f — below threshold of %.2f.",
                  entropy_norm, warn_threshold);
      PrintFormat("  Dominant factor absorbs %.2f%% of total variance.",
                  proportions[0] * 100.0);
      PrintFormat("  Diversification is weaker than position sizing assumes.");
     }
   else
     {
      PrintFormat("  RESULT: Portfolio is DIVERSIFIED.");
      PrintFormat("  H_norm = %.4f — above threshold of %.2f.",
                  entropy_norm, warn_threshold);
      PrintFormat("  Variance is distributed across factors.");
     }

   PrintDivider("=");
  }

//+------------------------------------------------------------------+
//| Print side-by-side comparison of two portfolios                  |
//+------------------------------------------------------------------+
void PrintComparison(double entropy_a, double entropy_b,
                     const string label_a, const string label_b)
  {
   PrintDivider("=");
   Print("  PORTFOLIO COMPARISON SUMMARY");
   PrintDivider();
   PrintFormat("  %-30s | H_norm = %.4f (%.2f%%)",
               label_a, entropy_a, entropy_a * 100.0);
   PrintFormat("  %-30s | H_norm = %.4f (%.2f%%)",
               label_b, entropy_b, entropy_b * 100.0);
   PrintDivider();

   double diff = entropy_b - entropy_a;
   if(MathAbs(diff) < 0.01)
      Print("  RESULT: Both portfolios have similar diversification.");
   else
      if(diff > 0)
         PrintFormat("  RESULT: %s is MORE diversified by %.4f entropy units.",
                     label_b, diff);
      else
         PrintFormat("  RESULT: %s is MORE diversified by %.4f entropy units.",
                     label_a, MathAbs(diff));

   PrintDivider("=");
  }

//+------------------------------------------------------------------+
//| Script entry point                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   PrintDivider("=");
   Print("  SPECTRAL ENTROPY CALCULATOR — Part 4 Script");
   Print("  Measuring What Matters: Portfolio Risk Decomposition");
   PrintDivider("=");
   PrintFormat("  Portfolio A: %s | %s | %s",
               A_Symbol1, A_Symbol2, A_Symbol3);
   PrintFormat("  Portfolio B: %s | %s | %s",
               B_Symbol1, B_Symbol2, B_Symbol3);
   PrintFormat("  Timeframe  : %s", EnumToString(TF));
   PrintFormat("  Lookback   : %d bars", LookbackBars);
   PrintFormat("  Entropy Warning Threshold: %.2f", EntropyWarnThreshold);
   PrintDivider();

//--- Build symbol arrays for both portfolios
   string sym_a[3];
   sym_a[0]=A_Symbol1;
   sym_a[1]=A_Symbol2;
   sym_a[2]=A_Symbol3;
   string sym_b[3];
   sym_b[0]=B_Symbol1;
   sym_b[1]=B_Symbol2;
   sym_b[2]=B_Symbol3;

//--- Step 1: Build covariance matrices
   Print("Step 1: Building covariance matrices...");

   matrix cov_a, cov_b;
   if(!BuildCovMatrix(sym_a, 3, TF, LookbackBars, cov_a))
      return;
   if(!BuildCovMatrix(sym_b, 3, TF, LookbackBars, cov_b))
      return;
   Print("  Both covariance matrices built successfully.");
   PrintDivider();

//--- Step 2: Compute spectral entropy for Portfolio A
   Print("Step 2: Computing spectral entropy for Portfolio A...");

   vector eig_a, prop_a;
   double entropy_a, entropy_norm_a;
   if(!ComputeSpectralEntropy(cov_a, 3, eig_a, prop_a,
                              entropy_a, entropy_norm_a))
      return;

//--- Step 3: Compute spectral entropy for Portfolio B
   Print("Step 3: Computing spectral entropy for Portfolio B...");

   vector eig_b, prop_b;
   double entropy_b, entropy_norm_b;
   if(!ComputeSpectralEntropy(cov_b, 3, eig_b, prop_b,
                              entropy_b, entropy_norm_b))
      return;

//--- Step 4: Print full reports for both portfolios
   string label_a = A_Symbol1+"/"+A_Symbol2+"/"+A_Symbol3;
   string label_b = B_Symbol1+"/"+B_Symbol2+"/"+B_Symbol3;

   PrintEntropyReport(sym_a, 3, "Portfolio A: " + label_a,
                      eig_a, prop_a, entropy_a, entropy_norm_a,
                      EntropyWarnThreshold);

   PrintEntropyReport(sym_b, 3, "Portfolio B: " + label_b,
                      eig_b, prop_b, entropy_b, entropy_norm_b,
                      EntropyWarnThreshold);

//--- Step 5: Side-by-side comparison
   PrintComparison(entropy_norm_a, entropy_norm_b, label_a, label_b);

   PrintDivider("=");
   Print("  Script complete. See article Part 4 for full breakdown.");
   PrintDivider("=");
  }
//+------------------------------------------------------------------+
