//+------------------------------------------------------------------+
//|                   MathFidelityBenchmark_EA.mq5                   |
//|                       https://www.mql5.com                       |
//+------------------------------------------------------------------+
#property copyright "Olamide Adebayo"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property description "Numerical fidelity & latency benchmark: Native MQL5 vs ONNX Runtime vs ALGLIB"

//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+
#include <MathFidelityBenchmark\BenchmarkConfig.mqh>
#include <MathFidelityBenchmark\BenchmarkStats.mqh>
#include <MathFidelityBenchmark\WeightLoader.mqh>
#include <MathFidelityBenchmark\MathEngineNative.mqh>
#include <MathFidelityBenchmark\MathEngineAlglib.mqh>
#include <MathFidelityBenchmark\MathEngineOnnx.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int  InpWarmupIters = BENCH_WARMUP_ITERS;
input int  InpTimedIters  = BENCH_TIMED_ITERS;
input bool InpRunOnInit   = true;

//+------------------------------------------------------------------+
//| Global objects and state                                         |
//+------------------------------------------------------------------+
CWeightLoader     g_loader;
CMathEngineNative g_native;
CMathEngineAlglib g_alglib;
CMathEngineOnnx   g_onnx;

double            g_test_inputs[][BENCH_INPUT_DIM];
double            g_test_refs[];
int               g_vector_count = 0;

bool              g_benchmark_done = false;

//+------------------------------------------------------------------+
//| OnInit                                                           |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   if(!InpRunOnInit)
      return(INIT_SUCCEEDED);

   if(!LoadAllInputs())
     {
      Print("[EA] failed to load weights/test vectors -- aborting");
      return(INIT_FAILED);
     }

   if(!g_native.Init(g_loader) || !g_alglib.Init(g_loader) || !g_onnx.Init(BENCH_ONNX_FILE))
     {
      Print("[EA] one or more engines failed to initialize -- aborting");
      return(INIT_FAILED);
     }

   RunBenchmark();
   g_benchmark_done = true;
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| OnDeinit                                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }

//+------------------------------------------------------------------+
//| OnTick                                                           |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   // no trading logic here, the whole run happens once in OnInit
  }

//+------------------------------------------------------------------+
//| LoadAllInputs                                                    |
//| Loads the weights and test vectors, validates shapes and rows.   |
//+------------------------------------------------------------------+
bool LoadAllInputs(void)
  {
   if(!g_loader.Load(BENCH_WEIGHTS_FILE))
      return false;

   int fh = FileOpen(BENCH_VECTORS_FILE, FILE_READ | FILE_CSV | FILE_ANSI, ',');
   if(fh == INVALID_HANDLE)
     {
      PrintFormat("[EA] failed to open %s, error %d", BENCH_VECTORS_FILE, GetLastError());
      return false;
     }

   int header_dim = (int)FileReadNumber(fh);
   if(header_dim != BENCH_INPUT_DIM)
     {
      PrintFormat("[EA] test vector file declares dim=%d, expected %d", header_dim, BENCH_INPUT_DIM);
      FileClose(fh);
      return false;
     }

   int row_count = (int)FileReadNumber(fh);
   ArrayResize(g_test_inputs, row_count);
   ArrayResize(g_test_refs, row_count);

   for(int r = 0; r < row_count; r++)
     {
      for(int c = 0; c < BENCH_INPUT_DIM; c++)
         g_test_inputs[r][c] = FileReadNumber(fh);
      g_test_refs[r] = FileReadNumber(fh);
     }

   FileClose(fh);
   g_vector_count = row_count;
   PrintFormat("[EA] loaded %d test vectors", g_vector_count);
   return (g_vector_count > 0);
  }

//+------------------------------------------------------------------+
//| CopyRow                                                          |
//| Copies row r of g_test_inputs into a flat dynamic array. A       |
//| row of a 2D array can't be passed directly to a function         |
//| expecting a 1D array parameter, or into vector::Assign(), so     |
//| every engine call goes through this copy first.                  |
//+------------------------------------------------------------------+
void CopyRow(const int r, double &buf[])
  {
   ArrayResize(buf, BENCH_INPUT_DIM);
   for(int c = 0; c < BENCH_INPUT_DIM; c++)
      buf[c] = g_test_inputs[r][c];
  }

//+------------------------------------------------------------------+
//| RunBenchmark                                                     |
//| Warmup, then one timed pass per engine, then Journal summary     |
//| and results CSV.                                                 |
//+------------------------------------------------------------------+
void RunBenchmark(void)
  {
   CLatencyStats  lat_native, lat_alglib, lat_onnx;
   CFidelityStats fid_native, fid_alglib, fid_onnx;

   lat_native.Reset(InpTimedIters * g_vector_count);
   lat_alglib.Reset(InpTimedIters * g_vector_count);
   lat_onnx.Reset(InpTimedIters * g_vector_count);

   double first_native[], first_alglib[], first_onnx[];
   ArrayResize(first_native, g_vector_count);
   ArrayResize(first_alglib, g_vector_count);
   ArrayResize(first_onnx,   g_vector_count);

   // warmup, not timed: first-call setup and cold caches stay out of the numbers
   for(int w = 0; w < InpWarmupIters; w++)
      for(int r = 0; r < g_vector_count; r++)
        {
         double buf[];
         CopyRow(r, buf);
         vector<double> vin;
         vin.Assign(buf);
         g_native.Predict(vin);
         g_alglib.Predict(buf);
         g_onnx.Predict(buf);
        }

   // timed passes run one engine at a time so one engine's cache
   // footprint can't leak into the next engine's numbers
   for(int t = 0; t < InpTimedIters; t++)
      for(int r = 0; r < g_vector_count; r++)
        {
         double buf[];
         CopyRow(r, buf);
         vector<double> vin;
         vin.Assign(buf);

         ulong t0 = GetMicrosecondCount();
         double pred = g_native.Predict(vin);
         lat_native.Add(GetMicrosecondCount() - t0);
         fid_native.Add(pred, g_test_refs[r]);
         if(t == 0)
            first_native[r] = pred;
        }

   for(int t = 0; t < InpTimedIters; t++)
      for(int r = 0; r < g_vector_count; r++)
        {
         double buf[];
         CopyRow(r, buf);

         ulong t0 = GetMicrosecondCount();
         double pred = g_alglib.Predict(buf);
         lat_alglib.Add(GetMicrosecondCount() - t0);
         fid_alglib.Add(pred, g_test_refs[r]);
         if(t == 0)
            first_alglib[r] = pred;
        }

   for(int t = 0; t < InpTimedIters; t++)
      for(int r = 0; r < g_vector_count; r++)
        {
         double buf[];
         CopyRow(r, buf);

         ulong t0 = GetMicrosecondCount();
         double pred = g_onnx.Predict(buf);
         lat_onnx.Add(GetMicrosecondCount() - t0);
         fid_onnx.Add(pred, g_test_refs[r]);
         if(t == 0)
            first_onnx[r] = pred;
        }

   PrintSummary("Native MQL5", lat_native, fid_native);
   PrintSummary("ALGLIB",      lat_alglib, fid_alglib);
   PrintSummary("ONNX Runtime", lat_onnx,  fid_onnx);

   WriteResultsCsv(lat_native, fid_native, lat_alglib, fid_alglib, lat_onnx, fid_onnx);
   WriteErrorDetail(first_native, first_alglib, first_onnx);
  }

//+------------------------------------------------------------------+
//| PrintSummary                                                     |
//+------------------------------------------------------------------+
void PrintSummary(const string name, CLatencyStats &lat, CFidelityStats &fid)
  {
   PrintFormat("--- %s ---", name);
   PrintFormat("  latency (us)  mean=%.2f  median=%.2f  p95=%.2f  p99=%.2f  n=%d",
               lat.Mean(), lat.Median(), lat.Percentile(0.95), lat.Percentile(0.99), lat.Count());
   PrintFormat("  fidelity      abs_mean=%.8f  abs_max=%.8f  rel_mean=%.6f%%  rel_max=%.6f%%",
               fid.AbsMean(), fid.AbsMax(), fid.RelMean() * 100.0, fid.RelMax() * 100.0);
  }

//+------------------------------------------------------------------+
//| WriteResultsCsv                                                  |
//+------------------------------------------------------------------+
void WriteResultsCsv(CLatencyStats &ln, CFidelityStats &fn,
                      CLatencyStats &la, CFidelityStats &fa,
                      CLatencyStats &lo, CFidelityStats &fo)
  {
   int fh = FileOpen(BENCH_RESULTS_FILE, FILE_WRITE | FILE_CSV | FILE_ANSI, ',');
   if(fh == INVALID_HANDLE)
     {
      PrintFormat("[EA] failed to open results file for writing, error %d", GetLastError());
      return;
     }

   FileWrite(fh, "engine", "mean_us", "median_us", "p95_us", "p99_us",
             "abs_err_mean", "abs_err_max", "rel_err_mean_pct");

   WriteRow(fh, "Native", ln, fn);
   WriteRow(fh, "ALGLIB", la, fa);
   WriteRow(fh, "ONNX",   lo, fo);

   FileClose(fh);
   Print("[EA] results written to ", BENCH_RESULTS_FILE);
  }

//+------------------------------------------------------------------+
//| WriteRow                                                         |
//+------------------------------------------------------------------+
void WriteRow(int fh, const string name, CLatencyStats &lat, CFidelityStats &fid)
  {
   FileWrite(fh, name,
             DoubleToString(lat.Mean(), 2),
             DoubleToString(lat.Median(), 2),
             DoubleToString(lat.Percentile(0.95), 2),
             DoubleToString(lat.Percentile(0.99), 2),
             DoubleToString(fid.AbsMean(), 8),
             DoubleToString(fid.AbsMax(), 8),
             DoubleToString(fid.RelMean() * 100.0, 6));
  }

//+------------------------------------------------------------------+
//| WriteErrorDetail                                                 |
//| One row per test vector: the reference and each engine's         |
//| prediction from the first timed pass. Predictions are            |
//| deterministic, so later passes repeat the same values.           |
//+------------------------------------------------------------------+
void WriteErrorDetail(const double &pn[], const double &pa[], const double &po[])
  {
   int fh = FileOpen(BENCH_ERRORS_FILE, FILE_WRITE | FILE_CSV | FILE_ANSI, ',');
   if(fh == INVALID_HANDLE)
     {
      PrintFormat("[EA] failed to open error file for writing, error %d", GetLastError());
      return;
     }

   FileWrite(fh, "vector", "reference", "native", "alglib", "onnx");
   for(int r = 0; r < g_vector_count; r++)
      FileWrite(fh, r,
                StringFormat("%.17e", g_test_refs[r]),
                StringFormat("%.17e", pn[r]),
                StringFormat("%.17e", pa[r]),
                StringFormat("%.17e", po[r]));

   FileClose(fh);
   Print("[EA] per-vector predictions written to ", BENCH_ERRORS_FILE);
  }
