//+------------------------------------------------------------------+
//|                                     TestCorrelationAnalytics.mq5 |
//+------------------------------------------------------------------+
#property description "Verifies return computation, Pearson correlation, and matrix "
#property description "index mapping using synthetic price and return data, "
#property description "independent of any live account or open positions."
#property script_show_inputs

#include <SymbolCorrelationMonitor/CorrelationTypes.mqh>
#include <SymbolCorrelationMonitor/ReturnSeriesReader.mqh>
#include <SymbolCorrelationMonitor/PearsonCorrelationCalculator.mqh>

//--- assertion counters, updated by the ASSERT macro used throughout this script
int g_assertion_passes   = 0;
int g_assertion_failures = 0;

//+---------------------------------------------------------- ---------+
//| ASSERT                                                             |
//| Records a pass or a failure for one checked condition and prints a |
//| diagnostic message, including the source line number, whenever the |
//| condition is false.                                                |
//+--------------------------------------------------------------------+
#define ASSERT(condition, message)                                          \
   if(!(condition))                                                         \
     {                                                                      \
      ::PrintFormat("ASSERTION FAILED: %s (line %d)", (message), __LINE__); \
      g_assertion_failures++;                                               \
     }                                                                      \
   else                                                                     \
     {                                                                      \
      g_assertion_passes++;                                                 \
     }

//+------------------------------------------------------------------------+
//| TestComputeReturnsBasic                                                |
//| Verifies that ComputeReturns() converts a small synthetic close series |
//| into the expected bar-to-bar percentage returns, with index 0 the most |
//| recent return.                                                         |
//+------------------------------------------------------------------------+
void TestComputeReturnsBasic(void)
  {
   double closes[];
   ArrayResize(closes, 3);
   closes[0] = 110.0; // most recent close
   closes[1] = 100.0;
   closes[2] = 95.0;  // oldest close

   CReturnSeriesReader reader;
   double returns[];
   bool ok = reader.ComputeReturns(closes, ArraySize(closes), returns);

   ASSERT(ok, "ComputeReturns must succeed for at least two closes.");
   ASSERT(ArraySize(returns) == 2, "Three closes must produce exactly two returns.");
   ASSERT(MathAbs(returns[0] - 0.10) < 0.0001, "The newest return must equal (110 - 100) / 100.");
   ASSERT(MathAbs(returns[1] - (5.0 / 95.0)) < 0.0001, "The older return must equal (100 - 95) / 95.");
  }

//+------------------------------------------------------------------------+
//| TestComputeReturnsInsufficientData                                     |
//| Verifies that a single close, with no older close to compare against,  |
//| is reported as a failure rather than an empty or invalid return.       |
//+------------------------------------------------------------------------+
void TestComputeReturnsInsufficientData(void)
  {
   double closes[];
   ArrayResize(closes, 1);
   closes[0] = 100.0;

   CReturnSeriesReader reader;
   double returns[];
   bool ok = reader.ComputeReturns(closes, ArraySize(closes), returns);
   ASSERT(!ok, "ComputeReturns must fail when fewer than two closes are supplied.");
  }

//+----------------------------------------------------------------------+
//| TestComputeReturnsZeroDivision                                       |
//| Verifies that a zero-priced older close is treated as invalid data   |
//| rather than allowed to divide by zero.                               |
//+----------------------------------------------------------------------+
void TestComputeReturnsZeroDivision(void)
  {
   double closes[];
   ArrayResize(closes, 2);
   closes[0] = 10.0;
   closes[1] = 0.0;

   CReturnSeriesReader reader;
   double returns[];
   bool ok = reader.ComputeReturns(closes, ArraySize(closes), returns);
   ASSERT(!ok, "ComputeReturns must fail rather than divide by a zero-priced close.");
  }

//+-----------------------------------------------------------------------+
//| TestComputePairCorrelationPerfectPositive                             |
//| Verifies that two series related by an exact positive linear formula  |
//| produce a correlation of exactly 1.0.                                 |
//+-----------------------------------------------------------------------+
void TestComputePairCorrelationPerfectPositive(void)
  {
   double series_a[] = {1.0, 2.0, 3.0, 4.0, 5.0};
   double series_b[] = {3.0, 5.0, 7.0, 9.0, 11.0}; // series_b = 2 * series_a + 1

   CPearsonCorrelationCalculator calculator;
   double correlation = 0.0;
   bool ok = calculator.ComputePairCorrelation(series_a, series_b, ArraySize(series_a), correlation);

   ASSERT(ok, "ComputePairCorrelation must succeed when both series have nonzero variance.");
   ASSERT(MathAbs(correlation - 1.0) < 0.0001, "An exact positive linear relationship must give a correlation of 1.0.");
  }

//+-----------------------------------------------------------------------+
//| TestComputePairCorrelationPerfectNegative                             |
//| Verifies that two series related by an exact negative linear formula  |
//| produce a correlation of exactly -1.0.                                |
//+-----------------------------------------------------------------------+
void TestComputePairCorrelationPerfectNegative(void)
  {
   double series_a[] = {1.0, 2.0, 3.0, 4.0, 5.0};
   double series_b[] = {-1.0, -2.0, -3.0, -4.0, -5.0}; // series_b = -series_a

   CPearsonCorrelationCalculator calculator;
   double correlation = 0.0;
   bool ok = calculator.ComputePairCorrelation(series_a, series_b, ArraySize(series_a), correlation);

   ASSERT(ok, "ComputePairCorrelation must succeed when both series have nonzero variance.");
   ASSERT(MathAbs(correlation - (-1.0)) < 0.0001, "An exact negative linear relationship must give a correlation of -1.0.");
  }

//+-----------------------------------------------------------------------+
//| TestComputePairCorrelationZeroVariance                                |
//| Verifies that a constant series, one with zero variance, is reported  |
//| as undefined rather than as a computed correlation of zero.           |
//+-----------------------------------------------------------------------+
void TestComputePairCorrelationZeroVariance(void)
  {
   double series_a[] = {1.0, 2.0, 3.0};
   double series_b[] = {5.0, 5.0, 5.0}; // constant, zero variance

   CPearsonCorrelationCalculator calculator;
   double correlation = 0.0;
   bool ok = calculator.ComputePairCorrelation(series_a, series_b, ArraySize(series_a), correlation);
   ASSERT(!ok, "ComputePairCorrelation must report undefined when either series has zero variance.");
  }

//+-----------------------------------------------------------------------+
//| TestComputePairCorrelationInsufficientData                            |
//| Verifies that a single data point, which has no variance to speak of, |
//| is reported as a failure.                                             |
//+-----------------------------------------------------------------------+
void TestComputePairCorrelationInsufficientData(void)
  {
   double series_a[] = {1.0};
   double series_b[] = {2.0};

   CPearsonCorrelationCalculator calculator;
   double correlation = 0.0;
   bool ok = calculator.ComputePairCorrelation(series_a, series_b, ArraySize(series_a), correlation);
   ASSERT(!ok, "ComputePairCorrelation must fail when fewer than two data points are supplied.");
  }

//+----------------------------------------------------------------------------+
//| TestMatrixIndexBoundaries                                                  |
//| Verifies the flat-array index mapping at the corners of a small            |
//| corr_matrix, which is exactly where an off-by-one error would first appear.|
//+----------------------------------------------------------------------------+
void TestMatrixIndexBoundaries(void)
  {
   string symbols[] = {"AAA", "BBB", "CCC"};
   CCorrelationMatrix corr_matrix;
   corr_matrix.Initialize(symbols, 3);

   ASSERT(corr_matrix.Index(0, 0) == 0, "The first row and first column must map to flat index 0.");
   ASSERT(corr_matrix.Index(0, 2) == 2, "The first row, last column must map to flat index 2.");
   ASSERT(corr_matrix.Index(1, 0) == 3, "The second row, first column must map to flat index 3.");
   ASSERT(corr_matrix.Index(2, 2) == 8, "The last row, last column must map to flat index 8, the final slot in a 3x3 corr_matrix.");
  }

//+------------------------------------------------------------------------+
//| TestBuildMatrixSymmetryAndDiagonal                                     |
//| Verifies that BuildMatrix() produces a symmetric corr_matrix, that the |
//| diagonal comes out defined and equal to 1.0 for a symbol with genuine  |
//| variance, and that the mirrored cell matches the originally computed   |
//| cell exactly.                                                          |
//+------------------------------------------------------------------------+
void TestBuildMatrixSymmetryAndDiagonal(void)
  {
   CSymbolReturnSeries series[];
   ArrayResize(series, 2);
   series[0].m_symbol = "SYM1";
   double returns0[]  = {1.0, 2.0, 3.0, 4.0, 5.0};
   ArrayCopy(series[0].m_returns, returns0);
   series[0].m_count = ArraySize(returns0);

   series[1].m_symbol = "SYM2";
   double returns1[]  = {3.0, 5.0, 7.0, 9.0, 11.0}; // perfectly correlated with series[0]
   ArrayCopy(series[1].m_returns, returns1);
   series[1].m_count = ArraySize(returns1);

   CPearsonCorrelationCalculator calculator;
   CCorrelationMatrix corr_matrix;
   calculator.BuildMatrix(series, ArraySize(series), corr_matrix);

   ASSERT(corr_matrix.IsDefined(0, 1), "A pair of series with genuine variance must produce a defined correlation.");
   ASSERT(MathAbs(corr_matrix.GetValue(0, 1) - corr_matrix.GetValue(1, 0)) < 0.0001, "The corr_matrix must be symmetric across the diagonal.");
   ASSERT(corr_matrix.IsDefined(0, 0) && MathAbs(corr_matrix.GetValue(0, 0) - 1.0) < 0.0001, "A symbol with variance must correlate with itself at exactly 1.0.");
   ASSERT(corr_matrix.IsDefined(1, 1) && MathAbs(corr_matrix.GetValue(1, 1) - 1.0) < 0.0001, "The second symbol must also self-correlate at exactly 1.0.");
  }

//+-------------------------------------------------------------------------+
//| TestBuildMatrixUndefinedPair                                            |
//| Verifies that a symbol with zero-variance returns produces an undefined |
//| correlation both against itself and against a normal symbol, while the  |
//| normal symbol's own self-correlation remains defined.                   |
//+-------------------------------------------------------------------------+
void TestBuildMatrixUndefinedPair(void)
  {
   CSymbolReturnSeries series[];
   ArrayResize(series, 2);
   series[0].m_symbol = "FLAT";
   double flat_returns[] = {5.0, 5.0, 5.0};
   ArrayCopy(series[0].m_returns, flat_returns);
   series[0].m_count = ArraySize(flat_returns);

   series[1].m_symbol = "NORMAL";
   double normal_returns[] = {1.0, 2.0, 3.0};
   ArrayCopy(series[1].m_returns, normal_returns);
   series[1].m_count = ArraySize(normal_returns);

   CPearsonCorrelationCalculator calculator;
   CCorrelationMatrix corr_matrix;
   calculator.BuildMatrix(series, ArraySize(series), corr_matrix);

   ASSERT(!corr_matrix.IsDefined(0, 0), "A zero-variance symbol must be undefined even against itself.");
   ASSERT(!corr_matrix.IsDefined(0, 1), "A zero-variance symbol paired with a normal symbol must be undefined.");
   ASSERT(corr_matrix.IsDefined(1, 1), "The normal symbol must still self-correlate at a defined value.");
  }

//+------------------------------------------------------------------------+
//| Script program start function                                          |
//| Runs every test function in sequence and prints a final pass/fail      |
//| summary. This script performs no chart rendering and requires no       |
//| live account, open positions, or bar history: every input is synthetic.|
//+------------------------------------------------------------------------+
void OnStart(void)
  {
   g_assertion_passes   = 0;
   g_assertion_failures = 0;

   ::Print("Running TestCorrelationAnalytics...");

   TestComputeReturnsBasic();
   TestComputeReturnsInsufficientData();
   TestComputeReturnsZeroDivision();
   TestComputePairCorrelationPerfectPositive();
   TestComputePairCorrelationPerfectNegative();
   TestComputePairCorrelationZeroVariance();
   TestComputePairCorrelationInsufficientData();
   TestMatrixIndexBoundaries();
   TestBuildMatrixSymmetryAndDiagonal();
   TestBuildMatrixUndefinedPair();

   ::PrintFormat("TestCorrelationAnalytics complete: %d passed, %d failed.", g_assertion_passes, g_assertion_failures);
   if(g_assertion_failures == 0)
      ::Print("TEST SUITE PASSED");
   else
      ::Print("TEST SUITE FAILED");
  }
//+------------------------------------------------------------------+