//+------------------------------------------------------------------+
//| 36_Backtest_History_Evidence_v1_00.mq5                           |
//| Audits concrete M1 history facts without inventing a quality %.  |
//+------------------------------------------------------------------+
#property copyright "FX Omoshiro Lab"
#property link      "https://fx-omoshiro-lab.com/"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_plots   0
#property indicator_buffers 12

input int  InpBarsToAudit = 5000;
input bool InpShowPanel   = true;

#define PANEL_PREFIX "MQL36_EVIDENCE_"
#define AUDIT_BUFFER_COUNT 12

double g_bars[];
double g_invalid_ohlc[];
double g_volume_one_mismatch[];
double g_nonmonotonic[];
double g_gap_events[];
double g_missing_minutes[];
double g_zero_spread[];
double g_min_spread[];
double g_max_spread[];
double g_synchronized[];
double g_terminal_build[];
double g_ready[];

struct HistoryAudit
{
   int bars;
   int invalid_ohlc;
   int volume_one_mismatch;
   int nonmonotonic;
   int gap_events;
   long missing_minutes;
   int zero_spread;
   int min_spread;
   int max_spread;
   datetime first_time;
   datetime last_time;
};

void AuditRates(const MqlRates &rates[], const int count, HistoryAudit &audit);
bool LoadAudit(HistoryAudit &audit, bool &synchronized);
void StoreBuffers(const HistoryAudit &audit, const bool synchronized, const bool ready);
void SetPanel(const HistoryAudit &audit, const bool synchronized, const bool ready);
void SetPanelLine(const int row, const string text, const color text_color, const int font_size = 11);
void DeletePanel();

int OnInit()
{
   if(InpBarsToAudit < 100 || InpBarsToAudit > 100000)
   {
      Print("History evidence: InpBarsToAudit must be 100..100000");
      return INIT_PARAMETERS_INCORRECT;
   }

   SetIndexBuffer(0, g_bars, INDICATOR_CALCULATIONS);
   SetIndexBuffer(1, g_invalid_ohlc, INDICATOR_CALCULATIONS);
   SetIndexBuffer(2, g_volume_one_mismatch, INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, g_nonmonotonic, INDICATOR_CALCULATIONS);
   SetIndexBuffer(4, g_gap_events, INDICATOR_CALCULATIONS);
   SetIndexBuffer(5, g_missing_minutes, INDICATOR_CALCULATIONS);
   SetIndexBuffer(6, g_zero_spread, INDICATOR_CALCULATIONS);
   SetIndexBuffer(7, g_min_spread, INDICATOR_CALCULATIONS);
   SetIndexBuffer(8, g_max_spread, INDICATOR_CALCULATIONS);
   SetIndexBuffer(9, g_synchronized, INDICATOR_CALCULATIONS);
   SetIndexBuffer(10, g_terminal_build, INDICATOR_CALCULATIONS);
   SetIndexBuffer(11, g_ready, INDICATOR_CALCULATIONS);

   ArraySetAsSeries(g_bars, true);
   ArraySetAsSeries(g_invalid_ohlc, true);
   ArraySetAsSeries(g_volume_one_mismatch, true);
   ArraySetAsSeries(g_nonmonotonic, true);
   ArraySetAsSeries(g_gap_events, true);
   ArraySetAsSeries(g_missing_minutes, true);
   ArraySetAsSeries(g_zero_spread, true);
   ArraySetAsSeries(g_min_spread, true);
   ArraySetAsSeries(g_max_spread, true);
   ArraySetAsSeries(g_synchronized, true);
   ArraySetAsSeries(g_terminal_build, true);
   ArraySetAsSeries(g_ready, true);

   IndicatorSetString(INDICATOR_SHORTNAME, "Backtest History Evidence");
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   DeletePanel();
}

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[])
{
   if(rates_total < 1)
      return 0;

   HistoryAudit audit;
   bool synchronized = false;
   if(!LoadAudit(audit, synchronized))
   {
      if(InpShowPanel)
         SetPanelLine(0, "WAITING FOR CONFIRMED M1 HISTORY...", clrYellow, 13);
      return 0;
   }

   const bool ready = synchronized && audit.bars == InpBarsToAudit &&
                      audit.invalid_ohlc == 0 && audit.nonmonotonic == 0;
   StoreBuffers(audit, synchronized, ready);
   if(InpShowPanel)
      SetPanel(audit, synchronized, ready);
   else
      DeletePanel();
   return rates_total;
}

bool LoadAudit(HistoryAudit &audit, bool &synchronized)
{
   synchronized = (bool)SeriesInfoInteger(_Symbol, PERIOD_M1, SERIES_SYNCHRONIZED);
   if(!synchronized)
      return false;

   MqlRates rates[];
   ArraySetAsSeries(rates, false);
   ResetLastError();
   const int copied = CopyRates(_Symbol, PERIOD_M1, 1, InpBarsToAudit, rates);
   if(copied != InpBarsToAudit || ArraySize(rates) != InpBarsToAudit)
   {
      PrintFormat("History evidence: CopyRates waiting requested=%d copied=%d size=%d error=%d",
                  InpBarsToAudit, copied, ArraySize(rates), GetLastError());
      return false;
   }
   AuditRates(rates, copied, audit);
   return true;
}

void AuditRates(const MqlRates &rates[], const int count, HistoryAudit &audit)
{
   audit.bars = count;
   audit.invalid_ohlc = 0;
   audit.volume_one_mismatch = 0;
   audit.nonmonotonic = 0;
   audit.gap_events = 0;
   audit.missing_minutes = 0;
   audit.zero_spread = 0;
   audit.min_spread = 2147483647;
   audit.max_spread = 0;
   audit.first_time = 0;
   audit.last_time = 0;
   if(count < 1 || ArraySize(rates) < count)
      return;

   audit.first_time = rates[0].time;
   audit.last_time = rates[count - 1].time;
   for(int index = 0; index < count; ++index)
   {
      const MqlRates bar = rates[index];
      const bool invalid = bar.time <= 0 || bar.high < bar.low ||
                           bar.high < bar.open || bar.high < bar.close ||
                           bar.low > bar.open || bar.low > bar.close;
      if(invalid)
         ++audit.invalid_ohlc;

      if(bar.tick_volume == 1 &&
         (bar.open != bar.high || bar.open != bar.low || bar.open != bar.close))
         ++audit.volume_one_mismatch;

      if(bar.spread == 0)
         ++audit.zero_spread;
      if(bar.spread < audit.min_spread)
         audit.min_spread = bar.spread;
      if(bar.spread > audit.max_spread)
         audit.max_spread = bar.spread;

      if(index == 0)
         continue;
      const long delta_seconds = (long)bar.time - (long)rates[index - 1].time;
      if(delta_seconds <= 0)
      {
         ++audit.nonmonotonic;
      }
      else if(delta_seconds > 60)
      {
         ++audit.gap_events;
         audit.missing_minutes += delta_seconds / 60 - 1;
      }
   }
   if(audit.min_spread == 2147483647)
      audit.min_spread = 0;
}

void StoreBuffers(const HistoryAudit &audit, const bool synchronized, const bool ready)
{
   g_bars[0] = (double)audit.bars;
   g_invalid_ohlc[0] = (double)audit.invalid_ohlc;
   g_volume_one_mismatch[0] = (double)audit.volume_one_mismatch;
   g_nonmonotonic[0] = (double)audit.nonmonotonic;
   g_gap_events[0] = (double)audit.gap_events;
   g_missing_minutes[0] = (double)audit.missing_minutes;
   g_zero_spread[0] = (double)audit.zero_spread;
   g_min_spread[0] = (double)audit.min_spread;
   g_max_spread[0] = (double)audit.max_spread;
   g_synchronized[0] = synchronized ? 1.0 : 0.0;
   g_terminal_build[0] = (double)TerminalInfoInteger(TERMINAL_BUILD);
   g_ready[0] = ready ? 1.0 : 0.0;
}

void SetPanel(const HistoryAudit &audit, const bool synchronized, const bool ready)
{
   const string background = PANEL_PREFIX + "Background";
   if(ObjectFind(0, background) < 0)
      ObjectCreate(0, background, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, background, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, background, OBJPROP_XDISTANCE, 24);
   ObjectSetInteger(0, background, OBJPROP_YDISTANCE, 42);
   ObjectSetInteger(0, background, OBJPROP_XSIZE, 760);
   ObjectSetInteger(0, background, OBJPROP_YSIZE, 390);
   ObjectSetInteger(0, background, OBJPROP_BGCOLOR, C'8,20,14');
   ObjectSetInteger(0, background, OBJPROP_BORDER_COLOR, clrDarkGreen);
   ObjectSetInteger(0, background, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, background, OBJPROP_HIDDEN, true);

   SetPanelLine(0, "BACKTEST HISTORY EVIDENCE / NO QUALITY %", clrLime, 14);
   SetPanelLine(1, "Concrete facts from confirmed M1 bars; not a tester score", clrSilver, 10);
   SetPanelLine(2, StringFormat("%s M1  bars=%d  synchronized=%s",
                               _Symbol, audit.bars, synchronized ? "YES" : "NO"), clrWhite);
   SetPanelLine(3, "Range  " + TimeToString(audit.first_time, TIME_DATE | TIME_MINUTES) +
                   "  ->  " + TimeToString(audit.last_time, TIME_DATE | TIME_MINUTES), clrWhite);
   SetPanelLine(4, StringFormat("OHLC invariant violations=%d", audit.invalid_ohlc), clrWhite);
   SetPanelLine(5, StringFormat("Non-monotonic / duplicate times=%d", audit.nonmonotonic), clrWhite);
   SetPanelLine(6, StringFormat("Gap events=%d  missing clock minutes=%I64d  (sessions included)",
                               audit.gap_events, audit.missing_minutes), clrWhite);
   SetPanelLine(7, StringFormat("Tick-volume=1 with differing OHLC=%d", audit.volume_one_mismatch), clrWhite);
   SetPanelLine(8, StringFormat("Spread points  min=%d  max=%d  zero bars=%d",
                               audit.min_spread, audit.max_spread, audit.zero_spread), clrWhite);
   SetPanelLine(9, StringFormat("Terminal build=%I64d  runtime=%s",
                               TerminalInfoInteger(TERMINAL_BUILD),
                               (bool)MQLInfoInteger(MQL_TESTER) ? "TESTER" : "CHART"), clrWhite);
   SetPanelLine(10, ready ? "STRUCTURE: PASS" : "STRUCTURE: REVIEW REQUIRED",
                ready ? clrLime : clrTomato, 13);
   SetPanelLine(11, "Record mode, tick source, spread, commission, slippage.", clrAqua, 10);
   ChartRedraw(0);
}

void SetPanelLine(const int row, const string text, const color text_color, const int font_size)
{
   const string name = PANEL_PREFIX + "Line_" + IntegerToString(row);
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 44);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 58 + row * 29);
   ObjectSetInteger(0, name, OBJPROP_COLOR, text_color);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, font_size);
   ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}

void DeletePanel()
{
   ObjectsDeleteAll(0, PANEL_PREFIX);
}
