Guías · Equipo de FARO · 19 de septiembre de 2026 · Lectura: 9 minutos

Sellado previo de parámetros: cómo hacer que tu forward test sea comprobable

Un historial de operaciones no dice con qué configuración se generó. Si los parámetros se tocaron por el camino, el historial sigue siendo real y deja de significar lo que parece. Esta guía explica cómo comprometerse con una configuración antes de empezar, de forma que cualquiera pueda comprobarlo sin fiarse de ti.

Un historial al que se le mueven los parámetros no es un historial

Imagina un Expert Advisor que lleva seis meses operando. En el mes tres iba mal y se le bajó el lotaje. En el cuatro se le movió el stop 50 puntos. En el cinco se le activó el trailing, que estaba apagado.

Al final del semestre hay un historial de operaciones. Todas son reales, ninguna se ha borrado, ninguna cifra está inventada. Y el historial es falso, porque no es el historial de un sistema: es el de tres o cuatro sistemas distintos presentados como uno. Cada ajuste se hizo después de ver los resultados, así que las decisiones de los meses buenos incorporan información que no existía cuando empezó el test.

Nadie ha mentido en ningún dato concreto. Es la misma trampa que publicar cinco sistemas, retirar los cuatro que van mal y quedarse con el que sobrevivió —sesgo de supervivencia—, pero un nivel más adentro: dentro de un solo sistema, en sus parámetros.

Y es indetectable desde fuera. Quien mira el historial ve operaciones; los parámetros con los que se generaron no están en ninguna parte.

Lo que sigue es una forma de que sí estén, y de que no haga falta creerte.

Por qué no vale con apuntarlos

La respuesta intuitiva es guardar la configuración: un PDF con los parámetros y la fecha, o una fila en una base de datos, o un hash guardado en un servidor.

Ninguna de las tres resuelve el problema, y todas fallan por el mismo motivo. Un dato guardado es una afirmación de quien lo guardó. Para creerla hay que fiarse de él: de que el PDF es de la fecha que dice, de que la fila no se ha actualizado, de que el hash de su servidor es el que calculó entonces. Si la pregunta era «¿me puedo fiar?», la respuesta no puede ser «fíate de esto otro».

Un hash guardado es una afirmación. Un hash recalculable es una comprobación.

La diferencia no es semántica. Si el hash se recalcula a partir de datos que tú puedes leer, para creerlo no hace falta fiarse de nadie: ejecutas la cuenta y sale, o no sale. Un sistema de integridad que exige confianza en quien lo opera no es un sistema de integridad. Es el mismo criterio con el que FARO sella sus señales, y por el que la metodología publica el formato exacto del texto que se hashea en vez de pedir que se dé por bueno el resultado.

Las tres reglas

De ahí salen las tres condiciones. Las tres son restricciones sobre lo que el código no puede hacer, no características:

  1. La cadena que se hashea se construye solo con valores que el usuario ve en el diálogo de parámetros. Nada de número de cuenta, bróker ni hora: eso cambiaría el hash entre dos terminales con la misma configuración, y el sello dejaría de significar «estos parámetros».
  2. SHA-256 a secas, sin clave y sin sal. Con clave se convierte en un HMAC y deja de ser reproducible con sha256sum. Una huella que solo el autor puede recalcular es otra vez una afirmación.
  3. El texto exacto se escribe a un fichero, byte a byte. Sin esto la regla 2 es una promesa: el lector no tendría de dónde sacar el texto para hashearlo.

La tercera es la que se olvida, y es la que sostiene las otras dos.

La cadena canónica

Un campo por línea, en orden fijo, con el número delante. Para la configuración de ejemplo, el texto exacto que se hashea son estas ocho líneas:

sello-config-v1
01_lotaje=3FB999999999999A
02_magic=246810
03_riesgo_pct=3FF8000000000000
04_stop_puntos=350
05_periodo=15
06_trailing=1
07_etiqueta=produccion

160 bytes en UTF-8, con salto de línea también al final de la última. SHA-256:

6f55a362e479dc9c514228ac72e0be32f5be273f490a32af8b96f9d6f79cfad2

Tres decisiones de formato que no son estéticas:

Y el lotaje no dice 0.10. Eso es lo siguiente.

La trampa que deja pasar el fallo en vez de avisar

Lo natural para meter un double en una cadena es DoubleToString(v). No lo hagas en un hash, y el motivo es que falla en la dirección peligrosa.

DoubleToString usa 8 decimales por defecto y redondea. Así que:

ValorTexto impresoBits reales
1.51.500000003FF8000000000000
1.5000000011.500000003FF800000044B830

Dos configuraciones distintas con el mismo hash. Para un sello ése es el peor fallo posible, porque no da una falsa alarma: deja pasar un parámetro cambiado sin decir nada. Un guardián que se equivoca avisando es molesto; uno que se equivoca callando es peor que no tenerlo, porque además da confianza.

Subir los decimales no lo arregla, lo mueve de sitio. A 17 dígitos empiezas a imprimir el ruido de la representación binaria —0.3 sale como 0.29999999999999999 y 0.1+0.2 como 0.30000000000000004, que son dos double distintos y el mismo número para cualquier humano—. Y aparece un problema nuevo: el separador decimal. En MQL5 es siempre un punto, pero quien reimplemente la verificación en C con locale español obtendrá 1,500000 y otro hash. El riesgo está fuera, que es donde no puedes controlarlo.

La solución es no convertir a decimal en ningún momento. Se hashean los 8 bytes del double, en hexadecimal:

union DobleEnBits
  {
   double            valor;
   ulong             bits;
  };

string DobleCanonico(const double valor)
  {
   DobleEnBits u;
   u.valor = valor;
   return StringFormat("%016I64X", u.bits);
  }

Dieciséis caracteres, sin redondeo, sin separador y sin locale. 0.10 es 3FB999999999999A y 0.11 es 3FBC28F5C28F5C29: exacto por construcción, no por precisión suficiente.

Y una nota para quien reimplemente esto, porque es el error más probable y es silencioso. La union lee la memoria en little-endian, pero %016I64X no imprime bytes: imprime el número, con el dígito más significativo primero. Las dos operaciones se cancelan, así que la cadena sale con el patrón de bits en orden MSB→LSB en cualquier arquitectura. El equivalente en Python es por tanto big-endian: struct.pack('>d', valor).hex().upper(). Con '<d' salen los mismos ocho bytes al revés, y otro hash.

El hash y el fichero salen del mismo array

Dos detalles del cálculo que valen más que su tamaño.

El terminador. StringToCharArray con count = -1 añade el 0x00 final, y ese byte entraría en el hash: el resultado no coincidiría con el de ninguna herramienta externa sobre el mismo texto. Se recorta de forma explícita.

Una sola conversión. La misma función devuelve los bytes que se hashean y los que se escriben al fichero. Si el texto se convirtiera dos veces —una para cada cosa— habría dos conversiones que pueden diferir, y sha256sum sobre el fichero podría no dar el hash sellado. Una sola conversión es la única forma de que fichero y huella no puedan divergir: deja de ser algo que hay que comprobar y pasa a ser algo que no puede pasar.

bool BytesCanonicos(const string texto, uchar &fuera[])
  {
   int n = StringToCharArray(texto, fuera, 0, -1, CP_UTF8);
   if(n <= 0)
      return false;
   if(fuera[n - 1] == 0)   // el terminador NO entra en el hash
      n--;
   return (ArrayResize(fuera, n) == n);
  }

string Sha256Hex(const uchar &datos[])
  {
   uchar clave[];    // SHA-256 no lleva clave; el array va vacio a proposito.
   uchar salida[];   // Con clave se convertiria en un HMAC y dejaria de ser
                     // reproducible con `sha256sum`, que es justo lo que se busca.

   if(CryptEncode(CRYPT_HASH_SHA256, datos, clave, salida) != 32)
      return "";     // SHA-256 son 32 bytes SIEMPRE; otra cosa es un error

   string hex = "";
   for(int i = 0; i < 32; i++)
      hex += StringFormat("%02x", salida[i]);
   return hex;
  }

Sha256Hex devuelve "" si algo falla, y quien llama tiene que comprobarlo: un hash vacío comparado con un hash vacío da igual, y el sello pasaría sin haber calculado nada.

Esto es lo que imprime el terminal al sellar:

Sello: sha256 = 6f55a362e479dc9c514228ac72e0be32f5be273f490a32af8b96f9d6f79cfad2
Sello: cadena canonica en canonica_SelloDeConfiguracion.txt (160 bytes).
Sello: comprueba el hash con -> sha256sum canonica_SelloDeConfiguracion.txt
Sello: configuracion sellada en sello_SelloDeConfiguracion.txt

La segunda implementación

Aquí está la demostración, y no es el código MQL5.

Esta reimplementación en Python no lee el fichero que escribe el Expert Advisor ni su código fuente: reconstruye la cadena canónica desde los parámetros y la hashea. Solo usa la biblioteca estándar, así que se ejecuta con cualquier Python 3 sin instalar nada.

def doble(valor):
    """Los 8 bytes del double en hexadecimal, digito mas significativo primero."""
    return struct.pack(">d", valor).hex().upper()


def canonica(lotaje, magic, riesgo_pct, stop_puntos, periodo, trailing, etiqueta):
    """El texto exacto que se hashea: un campo por linea, en orden fijo."""
    # Un salto de linea dentro de un valor partiria el campo en dos y haria que
    # dos configuraciones distintas se hashearan igual. Se sustituye, no se borra.
    etiqueta = etiqueta.replace("\r", " ").replace("\n", " ")

    campos = [
        FORMATO,                                    # subirlo invalida los sellos previos
        f"01_lotaje={doble(lotaje)}",
        f"02_magic={magic}",
        f"03_riesgo_pct={doble(riesgo_pct)}",
        f"04_stop_puntos={stop_puntos}",
        f"05_periodo={periodo}",                    # el entero del timeframe: M15 -> 15
        f"06_trailing={1 if trailing else 0}",
        f"07_etiqueta={etiqueta}",
    ]

    # Separador LF, y LF tambien al final del ultimo campo. Nunca CRLF: cada
    # 0x0D de mas es un byte que entra en el hash.
    return "".join(campo + "\n" for campo in campos)

Y al ejecutarlo:

$ python3 sello_config.py
sha256 = 6f55a362e479dc9c514228ac72e0be32f5be273f490a32af8b96f9d6f79cfad2
bytes = 160

Ese hash es el mismo que imprime el terminal. Y eso demuestra algo más fuerte que «el código funciona»: demuestra que el formato está especificado por completo. Si al reimplementarlo desde la descripción hubiera hecho falta mirar el fuente para acertar, el formato tendría un hueco y la huella no sería reproducible por un tercero de verdad — sería reproducible por quien tuviera el código.

Dos implementaciones independientes que coinciden es la propiedad que se estaba buscando. El hash es solo la forma de comprobarla.

Comprobado en MetaTrader 5 sobre Wine en macOS: CryptEncode(CRYPT_HASH_SHA256, …) devuelve el SHA-256 estándar, no una variante. Es un dato verificado en un terminal real, no una suposición de la documentación.

La verificación, en un comando

Es lo que hará el lector el primer día, y no necesita código:

$ shasum -a 256 canonica_SelloDeConfiguracion.txt
6f55a362e479dc9c514228ac72e0be32f5be273f490a32af8b96f9d6f79cfad2  canonica_SelloDeConfiguracion.txt

En Linux es sha256sum; en Windows, certutil -hashfile <fichero> SHA256 o Get-FileHash. Los dos de Windows devuelven mayúsculas y el programa imprime minúsculas: compara sin distinguir caso.

Ese hash tiene que ser el de la línea SELLO del fichero de sello. Si coincide, la configuración que generó ese historial es la que está sellada. Si no coincide, algo cambió — y el fichero de sello dice cuándo se intentó arrancar con otra.

Las dos trampas de codificación

Este único comando valida, de paso, los dos fallos que romperían la verificación externa sin dar ningún síntoma. Los dos están en el mismo sitio: cómo se escribe el fichero.

El primero es el salto de línea. FILE_TXT traduce los saltos al convenio del sistema, o sea CRLF en Windows. Los separadores canónicos son LF, así que el fichero saldría con un 0x0D de más por cada campo: 168 bytes en vez de 160, y otro hash. El programa hace su trabajo, el fichero parece correcto abierto en un editor, y sha256sum no coincide sin que nada explique por qué.

El segundo es el juego de caracteres. En MQL5 no existe ningún flag de texto UTF-8: FILE_UNICODE escribe UTF-16 y FILE_ANSI escribe ANSI. Ninguno sirve — con una tilde en un parámetro de texto, el fichero tendría bytes distintos de los que se hashearon.

La solución cubre los dos: se escribe con FILE_WRITE | FILE_BIN y FileWriteArray, y se escribe el mismo array de bytes que se hasheó. FILE_BIN no traduce nada y no inventa ningún BOM.

int h = FileOpen(ruta, FILE_WRITE | FILE_BIN);
...
uint escritos = FileWriteArray(h, datos, 0, WHOLE_ARRAY);

Y aquí está el detalle que hace falta para probarlo de verdad: con una etiqueta en ASCII el fallo de codificación es invisible, porque UTF-8 y ANSI dan los mismos bytes. Hay que sellar con una tilde. Con etiqueta = producción el fichero pasa a 161 bytes —la ó ocupa dos, C3 B3— y el hash a:

f4f5c0b32ceabc1c902e084d3745ee8b23d76d16bc11bce43dc06cfe44ac59d5

Si estuviera en ANSI serían 160 bytes y otro hash. Ese byte de más es toda la prueba.

Qué pasa cuando no coincide

El programa registra el intento y niega la ejecución. El fichero de sello queda así:

# Sello de configuracion. Formato: sello-config-v1
# Una linea por evento, en orden de llegada. Nunca se reescribe.
# SELLO   = configuracion vigente
# RECHAZO = intento de arrancar con otra configuracion
#
# tipo | fecha y hora del servidor | programa | sha256
SELLO   | 2026.08.31 20:20:17 | SelloDeConfiguracion | f4f5c0b32ceabc1c902e084d3745ee8b23d76d16bc11bce43dc06cfe44ac59d5
RECHAZO | 2026.08.31 20:37:29 | SelloDeConfiguracion | ebc11930a2b41efda99f8908cf1d62a3b8570c156bd66d55bf33c7a1c6671466

Tres decisiones, y las tres son el fondo del asunto:

Se registra el rechazo antes de fallar. Un sello que bloquea pero no deja historia sirve de poco: quien reinicia tres veces con tres configuraciones distintas no deja rastro. Con la línea RECHAZO, el fichero cuenta lo que se intentó y cuándo. La historia es el punto; el bloqueo es solo el mecanismo.

La canónica sellada no se sobrescribe. El intento rechazado se vuelca a un fichero aparte. Pisar la sellada destruiría justo lo que permite verificar el sello vigente, y lo haría precisamente en el momento en que más falta hace.

Se devuelve INIT_FAILED, no INIT_PARAMETERS_INCORRECT. Ésta es la que importa. INIT_PARAMETERS_INCORRECT reabre el diálogo de parámetros, y eso invita a ajustarlos hasta que el sistema arranque: convierte el sello en un obstáculo que se sortea a base de intentos. Pero la configuración no está mal: está cambiada. Y si ha cambiado, ya no es el mismo test. La respuesta correcta no es corregir los parámetros hasta que encajen — es reconocer que es otra prueba, archivar el sello anterior y sellar de nuevo con fecha nueva, dejando el intento previo a la vista. INIT_FAILED descarga el programa del gráfico y obliga a esa decisión consciente en vez de ofrecer un camino de vuelta.

Lo que un sello no demuestra

Conviene decirlo con la misma claridad que el resto, porque un mecanismo de verificación invita a concluir más de lo que soporta.

«¿Y mis parámetros, quedan al descubierto?»

Es la primera objeción razonable, y tiene respuesta porque el hash y el texto se publican en momentos distintos.

Para sellar solo hace falta publicar el hash: 64 caracteres que no revelan nada. La cadena canónica —que sí contiene los parámetros en claro— se queda en tu máquina y se entrega cuando toque: al cerrar el periodo de prueba, o a quien tenga que auditar y no antes.

Y funciona en ese orden, precisamente, porque el hash es un compromiso previo. Publicarlo primero es lo que hace que la revelación posterior signifique algo: no puedes elegir después qué parámetros decir que tenías. Al revés —publicar los parámetros y luego el hash— no demuestra nada en absoluto.

Es la única secuencia que funciona, y conviene entender por qué antes de invertirla por comodidad.

Dónde se publica el hash

Queda una pregunta práctica: publicar el hash, ¿dónde?

La propiedad que hace falta es una sola: que la fecha no la pongas tú. Un fichero de texto en tu disco con el hash y la fecha de hoy no vale nada, porque mañana puedes escribir otro con la fecha de hoy. El hash tiene que quedar registrado por algo que no controles.

Y eso no necesita ninguna infraestructura. Sirve cualquier sitio que ponga fecha por su cuenta: un mensaje en un foro, un correo al cliente para el que trabajas —el servidor de correo lo fecha—, un commit empujado a un repositorio público, un mensaje en la descripción de un producto o de una señal. Ninguno de esos sitios tiene que entender qué es un sello ni colaborar de ninguna manera: solo tiene que haber puesto la fecha antes de que empezara el test.

Cuál elijas cambia lo fácil que es discutir la fecha, no cómo funciona el mecanismo. Un correo lo puede poner en duda quien lo recibió; un mensaje público en un foro, no. Pero incluso el correo es cualitativamente distinto de un fichero en tu disco, porque hay una copia que no está en tu poder.

Ésa es la única pieza que el sello no se da a sí mismo, y por eso conviene decirla en voz alta en vez de dejarla implícita.

Si el programa que sellas publica señales en FARO, esa parte ya está resuelta por el sitio: los agentes publican con ficha propia y bajo la responsabilidad de un operador verificado, y cada señal queda sellada con su fecha en un registro público que no controla quien la emite.

El código

Se publica para que se copie y se adapte. Si adaptas los campos, sube la versión del formato en la primera línea: es lo que impide que dos hashes iguales signifiquen configuraciones distintas.

Los dos hashes de esta guía se reproducen ejecutándolos.

Por qué escribimos esto

FARO publica el historial completo de cada analista y de cada sistema automático, incluidas las pérdidas, porque un historial del que se elige qué enseñar no significa nada. El sellado de parámetros es el mismo problema un nivel más adentro, y la solución tiene la misma forma: comprometerse antes, y que la comprobación no dependa de que te crean.

Si quieres aplicar esa vara a cualquier analista o canal —de aquí o de fuera—, la lista práctica está en cómo saber si un trader es fiable: 7 comprobaciones.