Saltar al contenido
Datos

El dato frío se declara: basis_status y stale.

Un sistema que inventa un precio es peor que uno que dice no sé. El contrato que separa un dato fresco de uno frío, con razón y edad explícitas.

Cerberus

Un precio de hace tres horas tiene el mismo aspecto que un precio de hace tres segundos. Ambos son un número decimal, ambos caben en el mismo campo, ambos se grafican igual. La interfaz no distingue; el consumidor tampoco, hasta que la decisión ya está tomada.

Ese es el fallo silencioso de los sistemas de mercado: no el error visible, sino el dato viejo servido con formato de dato fresco.

El fallo silencioso

La cadena típica es conocida. Un feed deja de llegar, el último valor queda en caché, el endpoint lo devuelve. Nadie recibe una alerta porque el esquema de respuesta es idéntico al de un tick válido. El dashboard muestra el precio, el cálculo de riesgo lo usa, el modelo lo pondera y el reporte lo imprime.

Cuando el mercado se mueve y el sistema no, la pérdida no aparece como un error de software: aparece como una posición mal valuada. Y una posición mal valuada se descubre tarde, cuando alguien compara contra la fuente.

La corrección no es reintentar más fuerte. Es hacer que la frescura sea parte del dato, no una propiedad que el consumidor debe inferir.

El contrato

Prescience declara el estado del dato en la misma respuesta que lo entrega. El campo basis_status responde una sola pregunta: ¿sobre qué base se calculó este número?

  • ok: el dato está dentro de la ventana de frescura declarada para su fuente y su tipo.
  • cold: el dato existe, pero quedó fuera de la ventana. Viaja marcado con stale: true y con su edad real en segundos.
  • absent: el símbolo es conocido, no hay dato disponible. El payload va en null. No se rellena con el último valor, no se interpola, no se estima.

Cada estado viaja con reason: una frase que explica la causa en lenguaje operativo («último tick hace 3 h 12 min; fuera de la ventana de 90 s»). La razón no es decorativa. Es lo que permite distinguir un feed caído de un mercado cerrado, y son incidentes distintos con respuestas distintas.

Un bloque que existe pero fue calculado antes de la última ventana se marca además como stale con su edad. Un bloque viejo sigue siendo útil —la estructura de riesgo de ayer no es ruido— pero viaja con la advertencia puesta, no disfrazado de cómputo de ahora.

{
  "symbol": "COPPER",
  "price": "4.2180",
  "basis_status": "cold",
  "reason": "último tick hace 3 h 12 min; fuera de la ventana de 90 s",
  "stale": true,
  "age_s": 11520,
  "source": "alpaca"
}

Decimal como string

Los montos viajan como string JSON, nunca como número. No es una preferencia de estilo: un float64 de JavaScript no representa exactamente la mayoría de los decimales, y el redondeo binario se acumula en cuanto hay una suma de posiciones. Al serializar 4.2180 como número se pierden el último dígito significativo y la escala.

Como string, el valor conserva sus decimales y el consumidor lo parsea a Decimal en el borde del sistema. La aritmética ocurre en decimal de extremo a extremo, y la conciliación del libro cierra al centavo en lugar de «casi».

Qué ve el agente MCP

Un agente que consulta una tool por MCP no recibe un número suelto. Recibe el bloque con su estado. Cuando el resultado viene cold, la tool no lanza una excepción: responde con el dato marcado y su razón, porque el agente necesita poder decir «el precio está frío desde hace tres horas» en vez de construir una frase sobre un valor que ya no describe el mercado.

Ese detalle cambia el comportamiento del modelo. Sin el campo, el agente rellena el hueco: infiere, promedia o usa el valor anterior como si fuera actual. Con el campo, la instrucción es explícita y la abstención es una respuesta posible. Un agente que declara un dato frío es más útil que uno que suena seguro sobre información vencida.

Cómo se prueba

El contrato se verifica con fixtures congelados, no contra el feed vivo. Tres casos mínimos: una respuesta fresca que debe quedar en ok, una respuesta envejecida que debe quedar en cold con stale: true y age_s consistente con la marca de tiempo, y un símbolo conocido sin datos que debe devolver absent con payload null y su razón.

Encima de eso van dos invariantes que se comprueban como propiedades: ningún bloque puede traer un precio no nulo con basis_status distinto de ok sin stale: true; y todo monto debe parsear a Decimal sin pasar por punto flotante.

La prueba que más importa es la más aburrida: la de que el sistema, cuando no sabe, lo dice. Un dato frío declarado es información. Un dato frío disfrazado es una pérdida que todavía no ocurrió.

  • contratos
  • market-data
  • MCP