[MQL5 · Pine Script]

History Auditor

Measures the quality of the data your backtest runs on: missing bars, flat bars, bars with no ticks and weekend bars. It shows where the holes are and passes no judgement.

Published on 7 August 2026 · By SuaVar

The full source is right above and you can copy it. Downloading the file needs a free account.

//+------------------------------------------------------------------+
//|                                               HistoryAuditor.mq5  |
//|                                          https://suavar.com       |
//+------------------------------------------------------------------+
#property copyright "SuaVar"
#property link      "https://suavar.com"
#property version   "1.00"
#property description "Measures the quality of the price history a backtest would run on: missing"
#property description "bars, flat bars, bars with no ticks and bars falling on a weekend."
#property description ""
#property description "It reports counts and shows where the holes are. It passes no judgement: a"
#property description "gap can be a real market halt or a hole in the feed, and this cannot tell"
#property description "them apart. You decide what it means for your symbol."
#property description ""
#property description "Educational tool. It does not trade, it does not give signals and it does not"
#property description "tell anyone what to buy or sell."

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#property indicator_label1  "Missing bars before this one"
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  clrTomato
#property indicator_width1  2

//--- Constantes ---------------------------------------------------------------

//--- Tope del parametro "velas a examinar". Un indicador no puede examinar mas velas de las que el
//--- terminal ha cargado, y pedir cientos de miles solo alarga el barrido sin anadir informacion.
#define MAX_SCAN_BARS 20000

//--- (Aqui vivia WEEKEND_PROBE_LIMIT. Ver la nota de SameServerDay: la heuristica de fin de
//--- semana se sustituyo por el corte de dia natural, que ya no necesita sondear ranura a ranura.)

//--- Inputs -------------------------------------------------------------------
//--- Por defecto 5000 y no "todo lo que haya": el valor por defecto es el que usa la validacion
//--- automatica de MQL5 y el que va a ver la mayoria, asi que tiene que dar un resultado util en
//--- cualquier instalacion y en cualquier marco temporal, incluido M1.
input int InpScanBars = 5000;   // Bars to examine (0 = all the terminal has loaded)

//--- Buffers ------------------------------------------------------------------
double GapBuffer[];

//--- Estado del ultimo barrido ------------------------------------------------
int      g_scanned      = 0;   // velas realmente examinadas
int      g_gaps         = 0;   // interrupciones DENTRO del mismo dia: los agujeros de verdad
int      g_sessionGaps  = 0;   // interrupciones que cruzan de dia (cierre de sesion o finde)
int      g_missing      = 0;   // velas ausentes en total, solo las de huecos del mismo dia
int      g_worstGap     = 0;   // el hueco mas grande, en velas
datetime g_worstGapAt   = 0;   // cuando empieza ese hueco
int      g_flat         = 0;   // velas con maximo igual al minimo
int      g_noTicks      = 0;   // velas con volumen de ticks cero
int      g_weekendBars  = 0;   // velas fechadas en sabado o domingo

//--- Hora de apertura de la ultima vela vista. El barrido se rehace cuando NACE una vela, no en cada
//--- tick: las cifras no pueden cambiar dentro de la misma vela y rehacerlas por cotizacion seria
//--- trabajo tirado (la misma leccion que dejo el auditor de repintado al validarse sobre M1).
datetime g_lastBarTime  = 0;

//+------------------------------------------------------------------+
int OnInit()
  {
   if(InpScanBars < 0)
     {
      Print("HistoryAuditor: 'Bars to examine' cannot be negative.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(InpScanBars > MAX_SCAN_BARS)
     {
      PrintFormat("HistoryAuditor: 'Bars to examine' is capped at %d.", MAX_SCAN_BARS);
      return(INIT_PARAMETERS_INCORRECT);
     }

   SetIndexBuffer(0, GapBuffer, INDICATOR_DATA);
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   IndicatorSetInteger(INDICATOR_DIGITS, 0);
   IndicatorSetString(INDICATOR_SHORTNAME, "History quality: scanning");
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| true si esa fecha cae en sabado o domingo.                        |
//+------------------------------------------------------------------+
bool IsWeekend(const datetime t)
  {
   MqlDateTime dt;
   TimeToStruct(t, dt);
   return(dt.day_of_week == 0 || dt.day_of_week == 6);
  }

//+------------------------------------------------------------------+
//| true si dos fechas caen en el MISMO dia natural del servidor.      |
//|                                                                    |
//| ⚠️ ESTO SUSTITUYO A UNA HEURISTICA DE FIN DE SEMANA, y el cambio    |
//| vino de portar la herramienta a otra plataforma: alli el problema   |
//| salta a la vista porque un instrumento con sesion cierra CADA       |
//| NOCHE, no solo el finde. La version anterior solo perdonaba los     |
//| huecos que tocaban sabado o domingo, asi que sobre un indice o una  |
//| accion contaba cada cierre diario como defecto y el numero salia    |
//| enorme e inutil — el usuario habria concluido que sus datos estan   |
//| rotos cuando lo unico que pasa es que el mercado cierra por la      |
//| noche.                                                             |
//|                                                                    |
//| El corte por dia natural cubre los dos casos a la vez: el fin de    |
//| semana cruza de dia, y el cierre nocturno tambien.                  |
//|                                                                    |
//| ⚠️ Su limite, dicho por delante: en un mercado de 24 horas —cripto— |
//| un agujero REAL que cruce la medianoche cae en el mismo saco. Por   |
//| eso los dos recuentos se INFORMAN, no se descarta ninguno: la       |
//| herramienta mide y el que sabe que instrumento opera decide.        |
//+------------------------------------------------------------------+
bool SameServerDay(const datetime a, const datetime b)
  {
   MqlDateTime da, db;
   TimeToStruct(a, da);
   TimeToStruct(b, db);
   return(da.year == db.year && da.mon == db.mon && da.day == db.day);
  }

//+------------------------------------------------------------------+
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 < 2) return(0);

//--- Indexacion explicita de mas antigua a mas reciente. En un indicador viene asi por defecto, pero
//--- fijarlo evita que el codigo dependa de un defecto: si algun dia se copia a un contexto donde las
//--- series estan al reves, el barrido recorreria el tiempo hacia atras y todos los huecos saldrian
//--- negativos.
   ArraySetAsSeries(time, false);
   ArraySetAsSeries(high, false);
   ArraySetAsSeries(low, false);
   ArraySetAsSeries(tick_volume, false);
   ArraySetAsSeries(GapBuffer, false);

//--- Solo al nacer una vela (y en el primer calculo). Ver el comentario de g_lastBarTime.
   if(prev_calculated > 0 && time[rates_total-1] == g_lastBarTime) return(rates_total);
   g_lastBarTime = time[rates_total-1];

   int periodSeconds = PeriodSeconds();
   if(periodSeconds <= 0) return(rates_total);

//--- Ventana a examinar. Se recorre ENTERA en cada vela nueva en vez de llevar el conteo de forma
//--- incremental. Es deliberado: son como mucho 20000 comparaciones aritmeticas una vez por vela
//--- —imperceptible— y a cambio no hay estado acumulado que pueda quedarse desfasado cuando el
//--- terminal carga mas historico, que es justo cuando estas cifras tienen que cambiar.
   int scan = (InpScanBars <= 0) ? rates_total : InpScanBars;
   if(scan > rates_total) scan = rates_total;
   int start = rates_total - scan;
   if(start < 1) start = 1;

   g_scanned = 0; g_gaps = 0; g_sessionGaps = 0; g_missing = 0;
   g_worstGap = 0; g_worstGapAt = 0; g_flat = 0; g_noTicks = 0; g_weekendBars = 0;

   ArrayInitialize(GapBuffer, 0.0);

   for(int i = start; i < rates_total; i++)
     {
      g_scanned++;

      if(high[i] == low[i]) g_flat++;
      if(tick_volume[i] == 0) g_noTicks++;
      if(IsWeekend(time[i])) g_weekendBars++;

      int delta = (int)(time[i] - time[i-1]);
      int missing = (delta / periodSeconds) - 1;
      if(missing <= 0) continue;

      if(!SameServerDay(time[i-1], time[i]))
        {
         g_sessionGaps++;
         continue;
        }

      g_gaps++;
      g_missing += missing;
      GapBuffer[i] = (double)missing;
      if(missing > g_worstGap)
        {
         g_worstGap   = missing;
         g_worstGapAt = time[i-1];
        }
     }

//--- El veredicto vive en el nombre de la subventana y el detalle en el data window, igual que en el
//--- auditor de repintado: sin objetos graficos que crear ni limpiar, y sin tapar el grafico.
//---
//--- ⚠️ La cabecera de la subventana TRUNCA (medido: ~63 caracteres), asi que las CIFRAS van primero
//--- y la cola prescindible al final. Si algo se corta, que sea lo que menos informa.
   string headline;
   if(g_gaps == 0)
      headline = StringFormat("no gaps in %d bars", g_scanned);
   else
      headline = StringFormat("%d missing in %d bars | worst %d", g_missing, g_scanned, g_worstGap);

   IndicatorSetString(INDICATOR_SHORTNAME, StringFormat("History quality: %s", headline));

   string worstAt = (g_worstGapAt == 0) ? "-" : TimeToString(g_worstGapAt, TIME_DATE|TIME_MINUTES);
   PlotIndexSetString(0, PLOT_LABEL,
                      StringFormat("same-day gaps %d (%d missing bars, worst %d after %s) | across-day gaps %d | flat %d | no ticks %d | weekend bars %d",
                                   g_gaps, g_missing, g_worstGap, worstAt,
                                   g_sessionGaps, g_flat, g_noTicks, g_weekendBars));

   return(rates_total);
  }
//+------------------------------------------------------------------+

HistoryAuditor.mq5 · MetaTrader 5 · MQL5

A backtest is only as honest as the bars underneath it

A feed with holes throws no error and does not look broken. The chart simply draws one bar next to the other, and a gap of forty missing minutes looks exactly like forty minutes in which nothing happened. Your strategy is then measured over a market that never existed, and the result reads as a real edge.

Almost nobody checks, understandably: checking by hand means scrolling through thousands of bars looking for something that is, by definition, not there.

What it measures

Gaps. How many interruptions there are, how many bars are missing in total, the largest one and when it started. The histogram marks each interruption with its size, so you can see at a glance whether the holes are spread out or concentrated in one period.

Gaps are split into two counts and never mixed. Those falling within the same day are bars missing while the market was open: those are the holes worth looking at, and the only ones drawn. Those crossing a day boundary are almost always the session closing — the overnight break on an index or a stock, the weekend on forex. Counting them as defects would make every session instrument look broken, and a tool that always says the same thing informs nobody.

Neither count is discarded. On a 24/7 instrument a real hole that happens to cross midnight lands in the second bucket, so both numbers are reported and the reading is left to whoever knows the instrument.

Flat bars (high equal to low), which are often filler; bars without a single tick; and bars dated on a Saturday or Sunday.

What it cannot do, and that is not a flaw of this tool

A gap can be a real market halt — a holiday, a session break, a suspension — or a hole in your broker's feed. This cannot tell them apart, and neither can anything else that only sees the bars. It reports what is there and leaves the reading to the person who knows which symbol they trade and what its calendar looks like.

For the same reason it says nothing about whether your history is "good enough". That depends on what you are testing: a swing strategy on H4 lives with holes that would invalidate a scalping test on M1.

How to use it

Drop it on the chart of the symbol and timeframe you are about to backtest. It examines the last 5000 bars by default; set it to 0 to examine everything the terminal has loaded. The verdict appears in the subwindow header and the full breakdown in the Data Window.

If you are going to test on M1, load the M1 history first: the tool can only audit the bars the terminal actually has, and it will report a short history as a short history rather than pretending.

Educational

This is a measurement tool. It does not trade, it does not open or close positions, it does not produce buy or sell signals and it does not tell anyone what to trade.

Educational material. None of this is financial advice or an investment recommendation: these are measurement and analysis tools, not buy or sell signals.

History Auditor · SuaVar