//+------------------------------------------------------------------+
//|                                          DM_RollingMonitor.mq5   |
//|                                       https://www.mql5.com       |
//+------------------------------------------------------------------+
#property copyright "Olamide Adebayo"
#property version   "1.00"
#property strict
#property indicator_separate_window
#property indicator_buffers 3
#property indicator_plots   3
#property indicator_label1  "DM Statistic"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_label2  "+Critical"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGray
#property indicator_style2  STYLE_DOT
#property indicator_label3  "-Critical"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrGray
#property indicator_style3  STYLE_DOT

#include <DMTest\DMTest.mqh>

//+------------------------------------------------------------------+
//| Inputs — fallbacks only. dm_manifest.json, when present,         |
//| overrides every one of these at OnInit so the indicator is       |
//| driven by the same configuration file as DM_Test_EA.mq5 rather   |
//| than its own, independently-set defaults.                        |
//+------------------------------------------------------------------+
input string        InpManifestFile  = "DMTest\\dm_manifest.json";
input string         InpOnnxModel     = "DMTest\\dm_forecaster.onnx";
input int            InpHorizon       = 1;
input int            InpRollingWindow = 250;
input int            InpMinSample     = 30;
input ENUM_DM_LOSS   InpLossType      = DM_LOSS_SQUARED;
input int            InpEmaPeriod     = 20;
input int            InpAtrPeriod     = 14;
input int            InpRsiPeriod     = 14;
input int            InpMacdFast      = 12;
input int            InpMacdSlow      = 26;
input int            InpMacdSignal    = 9;
input double         InpAlpha         = 0.10;

//+------------------------------------------------------------------+
//| Indicator buffers (plotted series)                               |
//+------------------------------------------------------------------+
double BufDM[], BufUpperCrit[], BufLowerCrit[];

//+------------------------------------------------------------------+
//| Globals — populated from the manifest at OnInit (falling back to |
//| the inputs above when the manifest is absent), so g_* is what the|
//| rest of this file actually runs on, not the raw Inp* values      |
//+------------------------------------------------------------------+
long   g_onnx_handle = INVALID_HANDLE;
int    g_h_ema, g_h_atr, g_h_rsi, g_h_macd;
int    g_horizon, g_window, g_min_sample;
double g_alpha;
ENUM_DM_LOSS g_loss_type;
double g_err_a[];   // per-bar Model A error, index-aligned to time[] (non-series)
double g_err_b[];   // per-bar Model B error, same alignment

//+------------------------------------------------------------------+
//| Manifest loader — same simple key:value JSON scan as             |
//| DM_Test_EA.mq5, kept in each file rather than shared to avoid a  |
//| dependency on the string-parsing helper existing in both spots   |
//+------------------------------------------------------------------+
bool LoadManifestValue(const string &raw, const string key, string &value_out)
  {
   int pos = StringFind(raw, "\"" + key + "\"");
   if(pos < 0)
      return(false);
   int colon = StringFind(raw, ":", pos);
   int comma = StringFind(raw, ",", colon);
   int brace = StringFind(raw, "}", colon);
   int end = (comma >= 0 && (comma < brace || brace < 0)) ? comma : brace;
   if(colon < 0 || end < 0)
      return(false);
   string v = StringSubstr(raw, colon + 1, end - colon - 1);
   StringReplace(v, "\"", "");
   StringReplace(v, " ", "");
   StringReplace(v, "\n", "");
   StringReplace(v, "\r", "");
   value_out = v;
   return(true);
  }

void LoadManifest()
  {
   g_horizon    = InpHorizon;
   g_window     = InpRollingWindow;
   g_min_sample = InpMinSample;
   g_alpha      = InpAlpha;
   g_loss_type  = InpLossType;

   int fh = FileOpen(InpManifestFile, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON);
   if(fh == INVALID_HANDLE)
      fh = FileOpen(InpManifestFile, FILE_READ | FILE_TXT | FILE_ANSI);
   if(fh == INVALID_HANDLE)
     {
      PrintFormat("DM_RollingMonitor: manifest not found (%s), using input defaults.", InpManifestFile);
      return;
     }

   string raw = "";
   while(!FileIsEnding(fh))
      raw += FileReadString(fh) + "\n";
   FileClose(fh);

   string v;
   if(LoadManifestValue(raw, "forecast_horizon_h", v))    g_horizon    = (int)StringToInteger(v);
   if(LoadManifestValue(raw, "rolling_window_bars", v))   g_window     = (int)StringToInteger(v);
   if(LoadManifestValue(raw, "min_sample_size", v))       g_min_sample = (int)StringToInteger(v);
   if(LoadManifestValue(raw, "significance_alpha", v))    g_alpha      = StringToDouble(v);
   if(LoadManifestValue(raw, "loss_type", v))
      g_loss_type = (v == "ABSOLUTE") ? DM_LOSS_ABSOLUTE : DM_LOSS_SQUARED;

   PrintFormat("DM_RollingMonitor: manifest loaded — h=%d window=%d min_n=%d alpha=%.2f loss=%s",
               g_horizon, g_window, g_min_sample, g_alpha, (g_loss_type == DM_LOSS_SQUARED ? "SQUARED" : "ABSOLUTE"));
  }

//+------------------------------------------------------------------+
//| Indicator initialization                                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   LoadManifest();

   SetIndexBuffer(0, BufDM, INDICATOR_DATA);
   SetIndexBuffer(1, BufUpperCrit, INDICATOR_DATA);
   SetIndexBuffer(2, BufLowerCrit, INDICATOR_DATA);
   //--- deliberately not series: OnCalculate() below walks forward,
   //--- index-aligned with the non-series time[]/close[] arrays

   g_h_ema  = iMA(_Symbol, PERIOD_CURRENT, InpEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
   g_h_atr  = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
   g_h_rsi  = iRSI(_Symbol, PERIOD_CURRENT, InpRsiPeriod, PRICE_CLOSE);
   g_h_macd = iMACD(_Symbol, PERIOD_CURRENT, InpMacdFast, InpMacdSlow, InpMacdSignal, PRICE_CLOSE);

   if(g_h_ema == INVALID_HANDLE || g_h_atr == INVALID_HANDLE ||
      g_h_rsi == INVALID_HANDLE || g_h_macd == INVALID_HANDLE)
     {
      Print("DM_RollingMonitor: failed to create indicator handles.");
      return(INIT_FAILED);
     }

   //--- ONNX_COMMON_FOLDER: same reasoning as DM_Test_EA.mq5 - this indicator
   //--- also runs inside a tester-agent sandbox with its own private
   //--- MQL5\Files, so the model has to come from the shared Common folder
   g_onnx_handle = OnnxCreate(InpOnnxModel, ONNX_COMMON_FOLDER);
   if(g_onnx_handle == INVALID_HANDLE)
     {
      PrintFormat("DM_RollingMonitor: ONNX model load failed, error %d.", GetLastError());
      return(INIT_FAILED);
     }

   const long input_shape[]  = {1, NUM_FEATURES};
   const long output_shape[] = {1, 1};
   if(!OnnxSetInputShape(g_onnx_handle, 0, input_shape) ||
      !OnnxSetOutputShape(g_onnx_handle, 0, output_shape))
     {
      Print("DM_RollingMonitor: failed to set ONNX input/output shape.");
      return(INIT_FAILED);
     }

   IndicatorSetString(INDICATOR_SHORTNAME, "DM Rolling Monitor");
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Two-tailed Student-t critical value at the given alpha           |
//+------------------------------------------------------------------+
double CriticalTValue(const int df, const double alpha)
  {
   return(DM_StudentQuantile(1.0 - alpha / 2.0, (double)MathMax(df, 1)));
  }

//+------------------------------------------------------------------+
//| Model A forecast via the shared ONNX model — same contract as    |
//| DM_Test_EA.mq5, run here for real instead of a proxy             |
//+------------------------------------------------------------------+
double ForecastModelA(const int shift)
  {
   float features[];
   if(!DM_BuildFeatureVector(_Symbol, g_h_ema, g_h_atr, g_h_rsi, g_h_macd, shift, features))
      return(0.0);

   float input_data[1][NUM_FEATURES];
   for(int i = 0; i < NUM_FEATURES; i++)
      input_data[0][i] = features[i];

   float output_data[1][1];
   if(!OnnxRun(g_onnx_handle, ONNX_DEFAULT, input_data, output_data))
      return(0.0);
   if(!MathIsValidNumber(output_data[0][0]))
      return(0.0);
   return((double)output_data[0][0]);
  }

//+------------------------------------------------------------------+
//| Indicator recalculation                                          |
//| Walks forward over newly-formed bars only (incremental via       |
//| prev_calculated), builds both forecasters' per-bar error, then   |
//| runs a real DM_RunTest() over the trailing g_window bars ending  |
//| at each one — this per-bar windowed re-test is what makes the    |
//| line "rolling" rather than a single static verdict.              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[],
                 const double &open[], const double &high[], const double &low[], const double &close[],
                 const long &tick_volume[], const long &volume[], const int &spread[])
  {
   //--- headroom: BuildFeatureVector needs up to 5 closes / 3 EMA points
   //--- behind whichever bar it is building for
   const int warmup = 10;
   if(rates_total < g_window + warmup)
      return(0);

   ArrayResize(g_err_a, rates_total);
   ArrayResize(g_err_b, rates_total);
   ArrayResize(BufDM, rates_total);
   ArrayResize(BufUpperCrit, rates_total);
   ArrayResize(BufLowerCrit, rates_total);

   int start = (prev_calculated > 1) ? prev_calculated - 1 : warmup;
   start = MathMax(start, 1);

   //--- stop one bar short of rates_total: the last bar is still forming
   for(int i = start; i < rates_total - 1; i++)
     {
      //--- forecast for bar i was made using data as of bar i-1; CopyBuffer's
      //--- "shift" argument counts bars back from the current live bar, so
      //--- the build-shift for bar (i-1) is (rates_total - i)
      int shift_predict = rates_total - i;

      double predA = ForecastModelA(shift_predict);
      double predB = DM_ForecastModelB(g_h_ema, shift_predict);
      double realized = close[i] - close[i - 1];

      g_err_a[i] = realized - predA;
      g_err_b[i] = realized - predB;

      if(i < g_window + warmup)
        {
         BufDM[i]        = EMPTY_VALUE;
         BufUpperCrit[i] = EMPTY_VALUE;
         BufLowerCrit[i] = EMPTY_VALUE;
         continue;
        }

      int win_start = i - g_window + 1;
      int win_len   = g_window;
      double slice_a[], slice_b[];
      ArrayResize(slice_a, win_len);
      ArrayResize(slice_b, win_len);
      for(int k = 0; k < win_len; k++)
        {
         slice_a[k] = g_err_a[win_start + k];
         slice_b[k] = g_err_b[win_start + k];
        }

      DMResult res;
      bool ok = DM_RunTest(slice_a, slice_b, g_horizon, g_loss_type, res, g_min_sample, g_alpha);

      //--- draw the critical bands only when DM_RunTest returned a result it
      //--- actually stands behind; INSUFFICIENT_SAMPLE / INVALID_HORIZON /
      //--- INVALID_VARIANCE all leave res.hln_stat at 0.0 and must not be
      //--- plotted as if they were a genuine "no difference" reading
      if(!ok)
        {
         BufDM[i]        = EMPTY_VALUE;
         BufUpperCrit[i] = EMPTY_VALUE;
         BufLowerCrit[i] = EMPTY_VALUE;
         continue;
        }

      //--- critical value's degrees of freedom must track the sample size
      //--- DM_RunTest() actually used (res.n), not win_len - the window length
      //--- requested for the slice. They happen to be equal today because the
      //--- slice above is always built to exactly win_len, but tying the
      //--- critical value to res.n directly removes that assumption instead of
      //--- relying on it staying true if the slicing logic ever changes.
      double crit = CriticalTValue(res.n - 1, g_alpha);
      BufDM[i]        = res.hln_stat;
      BufUpperCrit[i] = crit;
      BufLowerCrit[i] = -crit;
     }

   return(rates_total - 1);
  }

void OnDeinit(const int reason)
  {
   if(g_onnx_handle != INVALID_HANDLE)
      OnnxRelease(g_onnx_handle);
  }
//+------------------------------------------------------------------+
