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);
}
//+------------------------------------------------------------------+