The full source is right above and you can copy it. Downloading the file needs a free account.
//+------------------------------------------------------------------+
//| RepaintAuditor.mq5 |
//| https://suavar.com |
//+------------------------------------------------------------------+
#property copyright "SuaVar"
#property link "https://suavar.com"
#property version "1.00"
#property description "Measures whether a custom indicator changes the values it already showed on"
#property description "bars that have CLOSED, and reports that separately from the normal movement"
#property description "that happens while a bar is still forming."
#property description ""
#property description "Works out of the box: with no indicator name it audits a built-in SMA(14),"
#property description "which is also the sanity check (a closed SMA bar can never change)."
#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 "Bars changed after close"
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrTomato
#property indicator_width1 2
//--- Constantes ---------------------------------------------------------------
//--- Periodo de la media integrada que se audita cuando no se indica ningun indicador.
#define SELF_CHECK_PERIOD 14
//--- Cuantas veces se vuelve a intentar abrir un handle que fallo, ANTES de rendirse.
//---
//--- ⚠️ Antes se reintentaba una vez por vela, indefinidamente. Parecia inofensivo y no lo era: cada
//--- iCustom fallido escribe varias lineas de error en el log del terminal, asi que un nombre mal
//--- escrito convertia UN error en uno POR VELA — miles en una pasada del probador. Un handle no se
//--- arregla solo: si el nombre esta mal seguira mal, y si el usuario lo corrige MetaTrader vuelve a
//--- llamar a OnInit de todas formas. Los reintentos solo cubren el caso raro de que compile el
//--- indicador que falta sin quitar este del grafico, y para eso bastan unos pocos.
#define OPEN_RETRIES 3
//--- Tope del parametro "velas cerradas bajo auditoria". Ver la comprobacion de OnInit.
#define MAX_AUDIT_BARS 5000
//--- Inputs -------------------------------------------------------------------
//--- ⚠️ El valor por defecto es VACIO, y eso NO es un descuido.
//---
//--- Apuntaba a "Examples\\Custom Moving Average", que viene con MetaTrader pero SIN COMPILAR: en un
//--- terminal recien instalado ese .ex5 no existe. Cualquiera que lo instalara y pulsara OK sin tocar
//--- nada se encontraba con un indicador que no cargaba, y la validacion automatica de MQL5 —que corre
//--- justamente en un terminal limpio y con los parametros por defecto— lo rechazo por eso.
//---
//--- Vacio significa "audita una MEDIA INTEGRADA del terminal" (ver OpenAudited). No hace falta ningun
//--- fichero, funciona en cualquier instalacion, y ademas es la PRUEBA DE CORDURA que la propia
//--- documentacion recomienda hacer primero: la media de una vela cerrada no puede cambiar, asi que el
//--- contador tiene que quedarse en 0. Si no se queda en 0, la tolerancia esta demasiado apretada.
input string InpIndicatorName = ""; // Indicator to audit (empty = built-in SMA(14), a self-check)
input int InpBufferIndex = 0; // Buffer index of that indicator
input int InpParamCount = 0; // How many of the parameters below to pass (0 = its defaults)
input double InpParam1 = 0; // Parameter 1
input double InpParam2 = 0; // Parameter 2
input double InpParam3 = 0; // Parameter 3
input double InpParam4 = 0; // Parameter 4
input double InpTolerancePct = 0.001; // Change tolerance (%)
input int InpAuditBars = 100; // Closed bars kept under audit
//--- State --------------------------------------------------------------------
double ChangedBuffer[];
int g_handle = INVALID_HANDLE;
// Una entrada por vela cerrada OBSERVADA EN VIVO: su hora de apertura, el valor que el indicador
// mostraba justo al cerrarla, y si ya se contó un cambio (para no contarlo dos veces).
datetime g_time[];
double g_value[];
bool g_flagged[];
int g_stored = 0;
// Hora de apertura de la ULTIMA vela vista, no el numero de velas. Ver el comentario de OnCalculate:
// contar velas confunde "ha nacido una vela" con "se ha cargado mas historico".
datetime g_lastBarTime = 0;
int g_observed = 0;
int g_changed = 0;
double g_worstChange = 0.0;
double g_worstSwing = 0.0;
double g_devMin = 0.0;
double g_devMax = 0.0;
datetime g_devBar = 0;
// Ultima vela en la que se reintento abrir el handle, y cuantos reintentos quedan. Ver OPEN_RETRIES.
datetime g_retryBar = 0;
int g_retriesLeft = OPEN_RETRIES;
// Capacidad REAL de los tres arrays paralelos, no la pedida. Ver OnInit.
int g_capacity = 0;
// El rotulo solo se rehace cuando hay algo nuevo que decir. Ver el final de OnCalculate.
bool g_dirty = true;
//+------------------------------------------------------------------+
//| Abre el handle del indicador auditado. |
//| |
//| Sin nombre se audita una media integrada: iMA vive DENTRO del |
//| terminal, asi que no depende de ningun .ex5 y no puede faltar. |
//| |
//| iCustom no admite una lista de parametros de longitud variable, de |
//| ahi las ramas: es la forma habitual de cubrir varios casos sin |
//| obligar al usuario a tocar el codigo. |
//+------------------------------------------------------------------+
int OpenAudited()
{
if(StringLen(InpIndicatorName) == 0)
return(iMA(_Symbol, _Period, SELF_CHECK_PERIOD, 0, MODE_SMA, PRICE_CLOSE));
switch(InpParamCount)
{
case 0: return(iCustom(_Symbol, _Period, InpIndicatorName));
case 1: return(iCustom(_Symbol, _Period, InpIndicatorName, InpParam1));
case 2: return(iCustom(_Symbol, _Period, InpIndicatorName, InpParam1, InpParam2));
case 3: return(iCustom(_Symbol, _Period, InpIndicatorName, InpParam1, InpParam2, InpParam3));
default: return(iCustom(_Symbol, _Period, InpIndicatorName, InpParam1, InpParam2, InpParam3, InpParam4));
}
}
//+------------------------------------------------------------------+
//| Nombre corto de lo que se esta auditando, para la cabecera. |
//+------------------------------------------------------------------+
string AuditedLabel()
{
if(StringLen(InpIndicatorName) == 0)
return(StringFormat("SMA(%d)", SELF_CHECK_PERIOD));
string leaf = InpIndicatorName;
int slash = StringLen(leaf) - 1;
while(slash >= 0 && StringGetCharacter(leaf, slash) != '\\')
slash--;
if(slash >= 0)
leaf = StringSubstr(leaf, slash + 1);
return(leaf);
}
//+------------------------------------------------------------------+
//| Lee el valor del indicador auditado en una posicion de SERIE |
//| (0 = vela en formacion). Devuelve false si aun no hay dato. |
//+------------------------------------------------------------------+
bool ReadAudited(const int shift, double &out)
{
double tmp[1];
if(CopyBuffer(g_handle, InpBufferIndex, shift, 1, tmp) != 1)
return(false);
if(tmp[0] == EMPTY_VALUE || !MathIsValidNumber(tmp[0]))
return(false);
out = tmp[0];
return(true);
}
//+------------------------------------------------------------------+
int OnInit()
{
SetIndexBuffer(0, ChangedBuffer, INDICATOR_DATA);
ArraySetAsSeries(ChangedBuffer, false);
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
//--- El tope de arriba no es decorativo: sin el, un valor absurdo pide una reserva de memoria absurda,
//--- y auditar mas velas de las que caben en la sesion no aporta nada.
if(InpAuditBars < 1 || InpAuditBars > MAX_AUDIT_BARS || InpTolerancePct < 0.0)
{
Print("RepaintAuditor: invalid inputs. 'Closed bars kept under audit' must be 1..",
MAX_AUDIT_BARS, " and the tolerance cannot be negative.");
return(INIT_PARAMETERS_INCORRECT);
}
//--- ⚠️ Un handle que no se puede crear NO puede tumbar el arranque.
//---
//--- Con un nombre puesto, el indicador auditado es un archivo del usuario que puede no existir, no
//--- estar compilado o llamarse de otra forma. Devolver INIT_FAILED ahi deja un indicador muerto y sin
//--- explicacion. Arranca siempre y dice en su cabecera lo que le falta.
//---
//--- Sin nombre (el defecto) esto no puede fallar: iMA es del terminal.
g_handle = OpenAudited();
g_retriesLeft = OPEN_RETRIES;
if(g_handle == INVALID_HANDLE)
Print("RepaintAuditor: \"", InpIndicatorName,
"\" could not be loaded. Check the path (relative to MQL5\\Indicators, no extension) ",
"and the parameter count. Leave the name empty to audit a built-in SMA instead.");
//--- ⚠️ Se manda en la capacidad REAL, no en la pedida. ArrayResize puede devolver menos de lo que se
//--- le pide, y todo lo que indexara por InpAuditBars escribiria entonces fuera de rango: "array out of
//--- range" es de los rechazos clasicos de la validacion y, sobre todo, es un fallo que revienta en la
//--- maquina del usuario y no en la de quien lo escribe.
g_capacity = ArrayResize(g_time, InpAuditBars);
g_capacity = MathMin(g_capacity, ArrayResize(g_value, InpAuditBars));
g_capacity = MathMin(g_capacity, ArrayResize(g_flagged, InpAuditBars));
if(g_capacity < 0) g_capacity = 0;
IndicatorSetString(INDICATOR_SHORTNAME, "Repaint audit: waiting for live bars");
IndicatorSetInteger(INDICATOR_DIGITS, 0);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_handle != INVALID_HANDLE)
{
IndicatorRelease(g_handle);
g_handle = INVALID_HANDLE;
}
}
//+------------------------------------------------------------------+
//| Guarda una vela recien cerrada, descartando la mas antigua. |
//+------------------------------------------------------------------+
void StoreClosed(const datetime openTime, const double value)
{
if(g_capacity < 1) return;
if(g_stored < g_capacity)
{
g_time[g_stored] = openTime;
g_value[g_stored] = value;
g_flagged[g_stored] = false;
g_stored++;
return;
}
for(int i = 1; i < g_capacity; i++)
{
g_time[i-1] = g_time[i];
g_value[i-1] = g_value[i];
g_flagged[i-1] = g_flagged[i];
}
g_time[g_capacity-1] = openTime;
g_value[g_capacity-1] = value;
g_flagged[g_capacity-1] = false;
}
//+------------------------------------------------------------------+
//| Localiza la posicion de SERIE de una vela por su hora de apertura. |
//| |
//| Se busca por HORA y no por indice a proposito: el indice de una |
//| vela cambia si el terminal carga mas historico, y comparar contra |
//| la vela equivocada daria un falso positivo — justo lo que esta |
//| herramienta no se puede permitir. |
//+------------------------------------------------------------------+
int SeriesShiftOf(const datetime openTime, const datetime &time[], const int rates_total)
{
int limit = g_capacity + 50;
if(limit > rates_total) limit = rates_total;
for(int shift = 0; shift < limit; shift++)
{
int idx = rates_total - 1 - shift;
if(idx < 0) break;
if(time[idx] == openTime) return(shift);
}
return(-1);
}
//+------------------------------------------------------------------+
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);
//--- Sin handle util no hay nada que auditar, pero el indicador sigue vivo y lo dice. Se reintenta
//--- como mucho OPEN_RETRIES veces y una vez por VELA: cada intento fallido escribe en el log, asi que
//--- reintentar para siempre convierte un error en un torrente. Ver el comentario de OPEN_RETRIES.
if(g_handle == INVALID_HANDLE)
{
if(g_retriesLeft > 0 && g_retryBar != time[rates_total-1])
{
g_retryBar = time[rates_total-1];
g_retriesLeft--;
g_handle = OpenAudited();
}
if(g_handle == INVALID_HANDLE)
{
IndicatorSetString(INDICATOR_SHORTNAME,
StringFormat("Repaint: cannot load \"%s\" — clear it to audit a built-in SMA",
InpIndicatorName));
return(rates_total);
}
}
if(BarsCalculated(g_handle) < rates_total) return(prev_calculated);
//--- La linea dibujada es el contador acumulado. En historico no hay nada que
//--- auditar (solo se ven valores ya definitivos), asi que vale 0 hasta que el
//--- indicador empieza a observar en vivo.
int start = (prev_calculated > 0) ? prev_calculated - 1 : 0;
for(int i = start; i < rates_total; i++)
ChangedBuffer[i] = (double)g_changed;
//--- Movimiento intrabar de la vela en formacion.
double live = 0.0;
if(ReadAudited(0, live))
{
if(g_devBar != time[rates_total-1])
{
g_devBar = time[rates_total-1];
g_devMin = live;
g_devMax = live;
}
else
{
if(live < g_devMin) g_devMin = live;
if(live > g_devMax) g_devMax = live;
}
}
//--- Primera pasada: solo se anota donde estamos. El historico NO se audita porque
//--- la informacion necesaria —que mostraba el indicador en su momento— ya no existe
//--- ahi; solo quedan valores definitivos.
if(g_lastBarTime == 0)
{
g_lastBarTime = time[rates_total-1];
return(rates_total);
}
//--- Se compara la HORA de la ultima vela, no el numero de velas.
//---
//--- `rates_total` crece por dos motivos distintos: porque nace una vela al final, o
//--- porque el terminal carga mas historico, que se añade por el otro extremo. Contar
//--- velas mezcla los dos, y entonces una carga de historico se toma por un cierre en
//--- vivo. No genera falsos positivos (esa vela ya tenia su valor definitivo, asi que
//--- nunca "cambia"), pero INFLA el numero de velas observadas y hace que un veredicto
//--- limpio parezca mas respaldado de lo que esta. En una herramienta cuyo unico
//--- producto es un veredicto, eso es peor que un fallo ruidoso.
if(time[rates_total-1] > g_lastBarTime)
{
g_lastBarTime = time[rates_total-1];
int closedIdx = rates_total - 2;
double closedValue = 0.0;
if(ReadAudited(1, closedValue))
{
//--- 1) Re-verificar lo que ya dabamos por cerrado.
for(int s = 0; s < g_stored; s++)
{
if(g_flagged[s]) continue;
int shift = SeriesShiftOf(g_time[s], time, rates_total);
if(shift <= 0) continue; // -1 = no localizada; 0 = la vela viva, no se audita
double now = 0.0;
if(!ReadAudited(shift, now)) continue;
// Denominador SIMETRICO: el mayor de los dos valores, no solo el antiguo. Muchos
// indicadores usan 0.0 para "aqui no hay nada" (zigzags, fractales, flechas), asi que
// dividir solo por el valor antiguo daria cifras astronomicas la primera vez que una
// vela pasa de vacia a tener valor — correcto de fondo, ilegible en pantalla.
double scale = MathMax(MathMax(MathAbs(g_value[s]), MathAbs(now)), 1e-10);
double change = MathAbs(now - g_value[s]) / scale * 100.0;
if(change > InpTolerancePct)
{
g_flagged[s] = true;
g_changed++;
if(change > g_worstChange) g_worstChange = change;
}
}
//--- 2) Guardar lo que el indicador dice de la vela que acaba de cerrar.
StoreClosed(time[closedIdx], closedValue);
g_observed++;
if(g_devMax > g_devMin)
{
double swing = (g_devMax - g_devMin) / MathMax(MathAbs(closedValue), 1e-10) * 100.0;
if(swing > g_worstSwing) g_worstSwing = swing;
}
ChangedBuffer[rates_total-1] = (double)g_changed;
g_dirty = true;
}
}
//--- ⚠️ El rotulo se rehace SOLO cuando hay algo nuevo que decir, no en cada tick.
//---
//--- Las cifras del veredicto unicamente cambian al cerrar una vela, asi que reconstruirlas por tick
//--- era trabajo tirado: dos StringFormat, un IndicatorSetString y un PlotIndexSetString por cada
//--- cotizacion. En un grafico en vivo se nota poco; en una pasada del probador sobre M1 —uno de los
//--- marcos con los que valida MQL5— son millones de formateos de cadena para pintar exactamente lo
//--- mismo, y el tiempo de validacion no es infinito.
if(!g_dirty) return(rates_total);
g_dirty = false;
//--- El veredicto vive en el nombre corto de la ventana: sin objetos graficos que
//--- limpiar y sin tapar el grafico del usuario.
//---
//--- ⚠️ El nombre corto SE TRUNCA en la cabecera de la subventana (se midio: cortaba a
//--- los 63 caracteres). Con la version larga el texto quedaba en "110 of 5" y parecia
//--- decir que habia 110 velas cambiadas de 5 observadas — imposible por la propia
//--- logica, y justo la clase de cifra que hace desconfiar de la herramienta entera.
//--- De ahi que aqui todo sea corto y que las CIFRAS vayan primero: si algo se corta,
//--- que sea la cola y no el dato.
string leaf = AuditedLabel();
string verdict;
if(g_observed == 0)
verdict = "waiting for live bars";
else
if(g_changed > 0)
verdict = StringFormat("%d/%d changed | worst %.2f%%", g_changed, g_observed, g_worstChange);
else
verdict = StringFormat("clean over %d bars", g_observed);
IndicatorSetString(INDICATOR_SHORTNAME, StringFormat("Repaint %s: %s", leaf, verdict));
//--- El detalle completo va al DATA WINDOW, que no tiene el limite de la cabecera. Es
//--- donde el usuario mira cuando quiere la cifra exacta, y ademas queda en la captura.
PlotIndexSetString(0, PLOT_LABEL,
StringFormat("changed %d of %d | worst change %.4f%% | worst intrabar swing %.4f%%",
g_changed, g_observed, g_worstChange, g_worstSwing));
return(rates_total);
}
//+------------------------------------------------------------------+