[MQL5 · Pine Script]

Auditor de histórico

Mide la calidad de los datos sobre los que corre tu backtest: velas ausentes, planas, sin ticks y en fin de semana. Enseña dónde están los agujeros y no juzga.

Publicado el 7 de agosto de 2026 · Por SuaVar

Código fuente

El código está completo aquí arriba y puedes copiarlo. Para descargar el archivo hace falta una cuenta gratuita.

//+------------------------------------------------------------------+
//|                                               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

Un backtest vale lo que valgan las velas de debajo

Un histórico con agujeros no da ningún error y no parece roto. El gráfico dibuja una vela junto a la siguiente, y un hueco de cuarenta minutos ausentes se ve exactamente igual que cuarenta minutos en los que no pasó nada. Tu estrategia se mide entonces contra un mercado que no existió, y el resultado se lee como una ventaja real.

Casi nadie lo comprueba, y es entendible: comprobarlo a mano significa recorrer miles de velas buscando algo que, por definición, no está.

Qué mide

Huecos. Cuántas interrupciones hay, cuántas velas faltan en total, cuál fue el mayor y cuándo empezó. El histograma marca cada interrupción con su tamaño, así que de un vistazo se ve si los agujeros están repartidos o concentrados en una época.

Los huecos se parten en dos recuentos, y no se mezclan nunca. Los que caen dentro del mismo día son velas que faltan con el mercado abierto: esos son los agujeros que interesan, y son los únicos que se dibujan. Los que cruzan de día son casi siempre el cierre de sesión — la parada nocturna de un índice o de una acción, el fin de semana en divisas—. Contarlos como defecto haría que todo instrumento con sesión saliera roto, y una herramienta que siempre dice lo mismo no informa de nada.

No se descarta ninguno de los dos. En un instrumento de 24 horas, un agujero real que cruce la medianoche cae en el segundo saco, así que las dos cifras se informan y la lectura la hace quien conoce su instrumento.

Velas planas (máximo igual al mínimo), que suelen ser relleno; velas sin un solo tick; y velas fechadas en sábado o domingo.

Lo que no puede hacer, y no es un defecto de esta herramienta

Un hueco puede ser una parada real del mercado —un festivo, un corte de sesión, una suspensión— o un agujero en el feed de tu bróker. Esto no puede distinguirlos, y ninguna herramienta que solo vea las velas puede. Informa de lo que hay y deja la lectura a quien sabe qué símbolo opera y qué calendario tiene.

Por el mismo motivo no dice si tu histórico es «suficientemente bueno». Eso depende de lo que estés probando: una estrategia de swing en H4 convive con agujeros que invalidarían una prueba de scalping en M1.

Cómo se usa

Se pone en el gráfico del símbolo y el marco temporal que vas a backtestear. Examina las últimas 5000 velas por defecto; a 0 examina todo lo que el terminal tenga cargado. El veredicto sale en la cabecera de la subventana y el desglose completo en la Ventana de Datos.

Si vas a probar en M1, carga antes el histórico de M1: la herramienta solo puede auditar las velas que el terminal tiene de verdad, y un histórico corto lo informará como corto en vez de disimularlo.

Educativo

Es una herramienta de medición. No opera, no abre ni cierra posiciones, no produce señales de compra o venta y no indica a nadie qué operar.

Material educativo. Nada de esto constituye asesoramiento financiero ni una recomendación de inversión: son herramientas de medición y análisis, no señales de compra o venta.

Auditor de histórico · SuaVar