//+------------------------------------------------------------------+
//|                                              LjungBoxToolkit.mq5 |
//|         Ljung-Box Portmanteau Test for Residual Autocorrelation  |
//|                            Pure MQL5 implementation              |
//+------------------------------------------------------------------+
#property copyright "MQL5 Article Series"
#property version   "1.01"
#property script_show_inputs
#property strict

//--- data source options
enum ENUM_DATA_SOURCE
  {
   DATA_PRICE_RETURNS = 0,   // Returns from price history (Close-to-Close)
   DATA_DEAL_HISTORY  = 1,   // Returns from closed deal history (realized P/L)
   DATA_EXTERNAL_FILE = 2    // External residual series (CSV/TXT in MQL5\Files)
  };

//--- inputs
input string            InpSymbol            = "";                 // Symbol ("" = current chart symbol)
input ENUM_TIMEFRAMES   InpTimeframe         = PERIOD_CURRENT;     // Timeframe (price-return source only)
input int               InpBars              = 500;                // Bars to evaluate (price-return source only)
input ENUM_DATA_SOURCE  InpDataSource        = DATA_PRICE_RETURNS; // Data source
input bool              InpUseLogReturns     = true;               // Use log returns (price source only)
input string            InpExternalFileName  = "residuals.csv";    // External residual file (in MQL5\Files)
input int               InpMaxLag            = 20;                 // Maximum lag for ACF computation
input string            InpLagSet            = "5,10,15,20";       // Lag horizons to test (comma-separated)
input int               InpDfAdjustment      = 0;                  // df adjustment; set consistently with the fitted model used for residual diagnostics
input double            InpSignificanceLevel = 0.05;               // Significance level (alpha)

//--- gamma-function constants
#define GB_ITMAX 200
#define GB_EPS   3.0e-9
#define GB_FPMIN 1.0e-300

//+------------------------------------------------------------------+
//| Log-gamma function (Lanczos approximation)                       |
//+------------------------------------------------------------------+
double GammLn(double xx)
  {
   double cof[6] =
     {
      76.18009172947146, -86.50532032941677, 24.01409824083091,
      -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5
     };
   double x = xx;
   double y = xx;
   double tmp = x + 5.5;
   tmp -= (x + 0.5) * MathLog(tmp);
   double ser = 1.000000000190015;
   for(int j = 0; j < 6; j++)
     {
      y += 1.0;
      ser += cof[j] / y;
     }
   return -tmp + MathLog(2.5066282746310005 * ser / x);
  }

//+------------------------------------------------------------------+
//| Series expansion for the lower incomplete gamma function P(a,x)  |
//+------------------------------------------------------------------+
void GSER(double &gamser, double a, double x, double &gln)
  {
   gln = GammLn(a);
   if(x <= 0.0)
     {
      gamser = 0.0;
      return;
     }
   double ap  = a;
   double sum = 1.0 / a;
   double del = sum;
   int n;
   for(n = 1; n <= GB_ITMAX; n++)
     {
      ap += 1.0;
      del *= x / ap;
      sum += del;
      if(MathAbs(del) < MathAbs(sum) * GB_EPS)
         break;
     }
   if(n > GB_ITMAX)
      Print("LjungBoxToolkit Warning: GSER did not converge after ", GB_ITMAX,
            " iterations (a=", a, ", x=", x, "). P-value may be inaccurate.");
   gamser = sum * MathExp(-x + a * MathLog(x) - gln);
  }

//+------------------------------------------------------------------+
//| Continued fraction for the upper incomplete gamma function Q(a,x)|
//+------------------------------------------------------------------+
void GCF(double &gammcf, double a, double x, double &gln)
  {
   gln = GammLn(a);
   double b = x + 1.0 - a;
   double c = 1.0 / GB_FPMIN;
   double d = 1.0 / b;
   double h = d;
   int i;
   for(i = 1; i <= GB_ITMAX; i++)
     {
      double an = -i * (i - a);
      b += 2.0;
      d = an * d + b;
      if(MathAbs(d) < GB_FPMIN)
         d = GB_FPMIN;
      c = b + an / c;
      if(MathAbs(c) < GB_FPMIN)
         c = GB_FPMIN;
      d = 1.0 / d;
      double del = d * c;
      h *= del;
      if(MathAbs(del - 1.0) < GB_EPS)
         break;
     }
   if(i > GB_ITMAX)
      Print("LjungBoxToolkit Warning: GCF did not converge after ", GB_ITMAX,
            " iterations (a=", a, ", x=", x, "). P-value may be inaccurate.");
   gammcf = MathExp(-x + a * MathLog(x) - gln) * h;
  }

//+------------------------------------------------------------------+
//| Regularized upper incomplete gamma function Q(a,x)               |
//+------------------------------------------------------------------+
double GammQ(double a, double x)
  {
   double gamser, gammcf, gln;
   if(x < 0.0 || a <= 0.0)
      return 1.0;
   if(x < a + 1.0)
     {
      GSER(gamser, a, x, gln);
      return 1.0 - gamser;
     }
   else
     {
      GCF(gammcf, a, x, gln);
      return gammcf;
     }
  }

//+------------------------------------------------------------------+
//| Right-tail p-value of a Chi-square distribution                  |
//+------------------------------------------------------------------+
double ChiSquarePValue(double chiStat, double df)
  {
   if(df <= 0.0)
      return 1.0;
   double a = df / 2.0;
   double x = chiStat / 2.0;
   if(x <= 0.0)
      return 1.0;
   return GammQ(a, x);
  }

//+------------------------------------------------------------------+
//| Build a return series from price history                         |
//+------------------------------------------------------------------+
bool BuildReturnsFromPrice(double &out[])
  {
   string sym  = (InpSymbol == "") ? _Symbol : InpSymbol;
   int    need = InpBars + 1;

   MqlRates rates[];
   ArraySetAsSeries(rates, false);
   int copied = CopyRates(sym, InpTimeframe, 1, need, rates);
   if(copied < 11)
     {
      Print("LjungBoxToolkit: could not copy enough price data for ", sym,
            " (got ", copied, " bars). Error: ", GetLastError());
      return false;
     }

   int m = copied - 1;
   ArrayResize(out, m);
   for(int i = 1; i < copied; i++)
     {
      double p0 = rates[i - 1].close;
      double p1 = rates[i].close;
      if(p0 <= 0.0)
        {
         out[i - 1] = 0.0;
         continue;
        }
      out[i - 1] = InpUseLogReturns ? MathLog(p1 / p0) : (p1 - p0) / p0;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Build a realized P/L series from closed deal history             |
//+------------------------------------------------------------------+
bool BuildReturnsFromDeals(double &out[])
  {
   if(!HistorySelect(0, TimeCurrent()))
     {
      Print("LjungBoxToolkit: HistorySelect failed. Error: ", GetLastError());
      return false;
     }

   int total = HistoryDealsTotal();
   datetime tmp_time[];
   double   tmp_profit[];
   ArrayResize(tmp_time, 0);
   ArrayResize(tmp_profit, 0);

   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;

      long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
      if(entry != DEAL_ENTRY_OUT)
         continue; // only closing deals carry a realized result

      if(InpSymbol != "")
        {
         string dsym = HistoryDealGetString(ticket, DEAL_SYMBOL);
         if(dsym != InpSymbol)
            continue;
        }

      double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
                      + HistoryDealGetDouble(ticket, DEAL_SWAP)
                      + HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      datetime dtime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);

      int sz = ArraySize(tmp_profit);
      ArrayResize(tmp_time, sz + 1);
      ArrayResize(tmp_profit, sz + 1);
      tmp_time[sz]   = dtime;
      tmp_profit[sz] = profit;
     }

   int cnt = ArraySize(tmp_profit);
   if(cnt < 11)
     {
      Print("LjungBoxToolkit: not enough closed deals found (", cnt, ").");
      return false;
     }

//--- sort chronologically (ascending by deal time) using an index array,
//--- since ArraySort only works on flat primitive arrays, not parallel pairs
   int idx[];
   ArrayResize(idx, cnt);
   for(int i = 0; i < cnt; i++)
      idx[i] = i;

   for(int i = 0; i < cnt - 1; i++)
     {
      int min_j = i;
      for(int j = i + 1; j < cnt; j++)
        {
         if(tmp_time[idx[j]] < tmp_time[idx[min_j]])
            min_j = j;
        }
      if(min_j != i)
        {
         int t = idx[i];
         idx[i] = idx[min_j];
         idx[min_j] = t;
        }
     }

   double sorted[];
   ArrayResize(sorted, cnt);
   for(int i = 0; i < cnt; i++)
      sorted[i] = tmp_profit[idx[i]];

   int use   = MathMin(cnt, InpBars);
   int start = cnt - use;
   ArrayResize(out, use);
   for(int i = 0; i < use; i++)
      out[i] = sorted[start + i];

   return true;
  }

//+------------------------------------------------------------------+
//| Validate that a token is a well-formed numeric literal           |
//| Accepts: optional sign, digits, at most one decimal point,       |
//| optional exponent (e/E) with optional sign and required digits   |
//+------------------------------------------------------------------+
bool IsNumericToken(const string s)
  {
   int len = StringLen(s);
   if(len == 0)
      return false;

   int  i           = 0;
   bool has_digit    = false;
   bool has_dot      = false;
   bool has_exponent = false;

//--- optional leading sign
   ushort ch = StringGetCharacter(s, i);
   if(ch == '-' || ch == '+')
      i++;

//--- mantissa: digits and at most one decimal point
   for(; i < len; i++)
     {
      ch = StringGetCharacter(s, i);

      if(ch >= '0' && ch <= '9')
        {
         has_digit = true;
         continue;
        }

      if(ch == '.')
        {
         if(has_dot || has_exponent)
            return false;   // second dot, or dot after exponent started
         has_dot = true;
         continue;
        }

      if(ch == 'e' || ch == 'E')
        {
         if(has_exponent || !has_digit)
            return false;   // second exponent, or exponent with no mantissa digits yet
         has_exponent = true;

         //--- optional sign right after the exponent marker
         if(i + 1 < len)
           {
            ushort next = StringGetCharacter(s, i + 1);
            if(next == '-' || next == '+')
               i++;
           }

         //--- exponent must be followed by at least one digit
         if(i + 1 >= len || StringGetCharacter(s, i + 1) < '0' || StringGetCharacter(s, i + 1) > '9')
            return false;
         continue;
        }

      return false; // any other character disqualifies this token
     }

   return has_digit;
  }

//+------------------------------------------------------------------+
//| Load an external residual series from MQL5\Files                 |
//+------------------------------------------------------------------+
bool LoadExternalResiduals(double &out[])
  {
   int handle = FileOpen(InpExternalFileName, FILE_READ | FILE_TXT | FILE_ANSI);
   if(handle == INVALID_HANDLE)
     {
      Print("LjungBoxToolkit: cannot open file '", InpExternalFileName,
            "' in MQL5\\Files. Error: ", GetLastError());
      return false;
     }

   double tmp[];
   ArrayResize(tmp, 0);
   int skipped = 0;

   while(!FileIsEnding(handle))
     {
      string line = FileReadString(handle);
      StringTrimLeft(line);
      StringTrimRight(line);
      if(line == "")
         continue;

      string parts[];
      int p = StringSplit(line, ',', parts);
      for(int i = 0; i < p; i++)
        {
         string v = parts[i];
         StringTrimLeft(v);
         StringTrimRight(v);
         if(v == "")
            continue;

         if(!IsNumericToken(v))
           {
            skipped++;
            continue;   // e.g. a header token like "Date" or "Residual"
           }

         double val = StringToDouble(v);
         int sz = ArraySize(tmp);
         ArrayResize(tmp, sz + 1);
         tmp[sz] = val;
        }
     }
   FileClose(handle);

   if(skipped > 0)
      Print("LjungBoxToolkit: skipped ", skipped,
            " non-numeric token(s) in '", InpExternalFileName,
            "' (e.g. a header row). Verify the file if this count looks wrong.");

   int cnt = ArraySize(tmp);
   if(cnt < 11)
     {
      Print("LjungBoxToolkit: external file contained too few values (", cnt, ").");
      return false;
     }

   ArrayResize(out, cnt);
   ArrayCopy(out, tmp);
   return true;
  }

//+------------------------------------------------------------------+
//| Compute sample autocorrelations rho_1 .. rho_maxlag              |
//| Returns false if the series has zero variance (undefined ACF)    |
//+------------------------------------------------------------------+
bool ComputeAutocorrelations(const double &x[], int maxlag, double &acf[])
  {
   int n = ArraySize(x);
   double mean = 0.0;
   for(int i = 0; i < n; i++)
      mean += x[i];
   mean /= n;

   double c0 = 0.0;
   for(int i = 0; i < n; i++)
      c0 += (x[i] - mean) * (x[i] - mean);
   c0 /= n;

   if(c0 <= 0.0)
     {
      Print("LjungBoxToolkit: zero variance in series (all observations identical). ",
            "Autocorrelation is undefined for a constant series. Aborting.");
      return false;
     }

   acf[0] = 1.0;
   for(int k = 1; k <= maxlag; k++)
     {
      double s = 0.0;
      for(int t = k; t < n; t++)
         s += (x[t] - mean) * (x[t - k] - mean);
      s /= n;
      acf[k] = s / c0;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Ljung-Box Q statistic for a given lag horizon h                  |
//+------------------------------------------------------------------+
double LjungBoxQ(const double &acf[], int n, int h)
  {
   double q = 0.0;
   for(int k = 1; k <= h; k++)
     {
      double rho = acf[k];
      q += (rho * rho) / (n - k);
     }
   q *= n * (n + 2);
   return q;
  }

//+------------------------------------------------------------------+
//| Validate that a token is a well-formed integer                   |
//| Accepts: optional sign followed by one or more digits            |
//+------------------------------------------------------------------+
bool IsIntegerToken(const string s)
  {
   int len = StringLen(s);
   if(len == 0)
      return false;

   int i = 0;

//--- optional leading sign
   ushort ch = StringGetCharacter(s, i);
   if(ch == '-' || ch == '+')
     {
      i++;

      //--- sign must be followed by at least one digit
      if(i >= len)
         return false;
     }

//--- every remaining character must be a digit
   for(; i < len; i++)
     {
      ch = StringGetCharacter(s, i);

      if(ch < '0' || ch > '9')
         return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Parse a comma-separated lag list into an int array               |
//+------------------------------------------------------------------+
int ParseLagSet(string s, int maxlag, int &lags[])
  {
   string parts[];
   int cnt = StringSplit(s, ',', parts);
   ArrayResize(lags, 0);

   for(int i = 0; i < cnt; i++)
     {
      string v = parts[i];
      StringTrimLeft(v);
      StringTrimRight(v);

      //--- ignore empty tokens
      if(v == "")
         continue;

      //--- reject anything that is not a valid integer token
      if(!IsIntegerToken(v))
        {
         Print("LjungBoxToolkit: invalid lag token '", v,
               "'; expected an integer. Skipped.");
         continue;
        }

      int lag = (int)StringToInteger(v);

      //--- lag must be positive
      if(lag < 1)
        {
         Print("LjungBoxToolkit: invalid lag ", lag,
               "; lag must be >= 1. Skipped.");
         continue;
        }

      //--- lag must not exceed the computed ACF range
      if(lag > maxlag)
        {
         Print("LjungBoxToolkit: requested lag ", lag,
               " exceeds InpMaxLag (", maxlag, "); skipped.");
         continue;
        }

      //--- add valid lag to the output array
      int sz = ArraySize(lags);
      ArrayResize(lags, sz + 1);
      lags[sz] = lag;
     }

   return ArraySize(lags);
  }

//+------------------------------------------------------------------+
//| Print the formatted report to the Experts tab                    |
//+------------------------------------------------------------------+
void PrintReport(string src_desc, int n, const double &acf[], const int &lags[], int lag_count)
  {
   Print("==================================================================");
   Print(" LJUNG-BOX PORTMANTEAU TEST REPORT");
   Print("==================================================================");
   PrintFormat(" Data source     : %s", src_desc);
   PrintFormat(" Observations (n): %d", n);
   PrintFormat(" Max lag (ACF)   : %d", ArraySize(acf) - 1);
   PrintFormat(" Fitted params   : %d", InpDfAdjustment);
   PrintFormat(" Significance    : %.4f", InpSignificanceLevel);
   Print("------------------------------------------------------------------");
   Print(" Sample autocorrelations:");

   int show_max = MathMin(ArraySize(acf) - 1, 50);
   for(int k = 1; k <= show_max; k++)
      PrintFormat("    rho(%2d) = %8.5f", k, acf[k]);

   Print("------------------------------------------------------------------");
   Print(" Ljung-Box test results per lag horizon:");
   Print("------------------------------------------------------------------");

   for(int i = 0; i < lag_count; i++)
     {
      int h = lags[i];
      int df = h - InpDfAdjustment;

      if(df <= 0)
        {
         PrintFormat("    Lags 1..%-3d | Skipped (df = %d <= 0; df adjustment = %d)",
                     h, df, InpDfAdjustment);
         continue;
        }

      double q = LjungBoxQ(acf, n, h);
      double p = ChiSquarePValue(q, (double)df);

      string conclusion = (p < InpSignificanceLevel)
                          ? "Reject H0 - significant serial dependence detected"
                          : "Fail to reject H0 - no significant evidence of serial dependence";

      PrintFormat("    Lags 1..%-3d | Q = %10.4f | df = %3d | p-value = %8.6f", h, q, df, p);
      PrintFormat("       -> %s", conclusion);
     }

   Print("==================================================================");
  }

//+------------------------------------------------------------------+
//| Script entry point                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   double series[];
   string src_desc = "";
   bool   ok = false;

   switch(InpDataSource)
     {
      case DATA_PRICE_RETURNS:
         ok = BuildReturnsFromPrice(series);
         src_desc = StringFormat("Price returns (%s, %s, %s)",
                                 (InpSymbol == "" ? _Symbol : InpSymbol),
                                 EnumToString(InpTimeframe),
                                 (InpUseLogReturns ? "log" : "simple"));
         break;
      case DATA_DEAL_HISTORY:
         ok = BuildReturnsFromDeals(series);
         src_desc = "Closed deal history (realized P/L)";
         break;
      case DATA_EXTERNAL_FILE:
         ok = LoadExternalResiduals(series);
         src_desc = "External residual file: " + InpExternalFileName;
         break;
     }

   if(!ok || ArraySize(series) < 11)
     {
      Print("LjungBoxToolkit: failed to build a usable series (need at least 11 observations). Aborting.");
      return;
     }

   int n = ArraySize(series);
   if(InpMaxLag < 1 || InpMaxLag >= n)
     {
      Print("LjungBoxToolkit: InpMaxLag must be >= 1 and < number of observations (",
            n, "). Aborting.");
      return;
     }

   double acf[];
   ArrayResize(acf, InpMaxLag + 1);
   if(!ComputeAutocorrelations(series, InpMaxLag, acf))
     {
      Print("LjungBoxToolkit: cannot compute autocorrelations for a constant series. Aborting.");
      return;
     }

   int lags[];
   int lag_count = ParseLagSet(InpLagSet, InpMaxLag, lags);
   if(lag_count <= 0)
     {
      Print("LjungBoxToolkit: no valid lag horizons parsed from InpLagSet. Aborting.");
      return;
     }

   PrintReport(src_desc, n, acf, lags, lag_count);
  }
//+------------------------------------------------------------------+
