Quant500 Academy

2. Qué métricas hay que medir en un sistema de trading

Nivel 10: Saber qué está pasando

Medir no es guardar números por si acaso. Una métrica solo se gana su sitio si existe una decisión que tomarías distinta según su valor. Con ese filtro, la lista de un sistema de trading se queda corta y clara, y se parte en dos familias que conviene no mezclar nunca en el mismo panel.

Dos familias que responden a preguntas distintas

Las métricas del sistema contestan a "¿funciona la maquinaria?". Son cinco, y con esas cinco cubres casi todo:

Las métricas de la estrategia contestan a "¿cuánto estoy ganando o perdiendo?": posición por símbolo, exposición bruta y neta, resultado del día y caída desde el máximo del patrimonio.

Mantén las dos familias en paneles separados, y no por estética: se leen en momentos distintos. Las del sistema, cada mañana antes de operar; las de la estrategia, al cerrar el día. Mezclarlas produce un panel de cuarenta números donde el ojo no encuentra el que importa, que es el problema del registro a nivel de depuración trasladado a los gráficos.

Las del sistema avisan antes de que la estrategia pierda dinero

Esta es la idea central del módulo. Las métricas de la estrategia son retrasadas: cuando la caída desde máximo empeora, el dinero ya se ha ido. Las del sistema son adelantadas: describen las condiciones en las que se está tomando cada decisión, y se degradan antes de que el efecto llegue al resultado.

Un ejemplo típico: el retraso de los datos pasa de tres segundos a noventa a lo largo de una mañana. La estrategia sigue en verde, no hay rechazos y nadie ha notado nada. Pero las señales se están calculando sobre velas viejas, y esa desalineación entre lo que el modelo cree ver y lo que el mercado hace acabará apareciendo en el resultado con días de retraso. Si solo miras el resultado, te enteras cuando ya no hay nada que hacer.

import time
from collections import defaultdict

class Metricas:
    def __init__(self):
        self.contadores = defaultdict(float)
        self.muestras = defaultdict(list)

    def _clave(self, nombre, etiquetas):
        return (nombre, tuple(sorted(etiquetas.items())))

    def contar(self, nombre, valor=1.0, **etiquetas):
        self.contadores[self._clave(nombre, etiquetas)] += valor

    def observar(self, nombre, valor, **etiquetas):
        self.muestras[self._clave(nombre, etiquetas)].append(valor)

    def percentil(self, nombre, p, **etiquetas):
        datos = sorted(self.muestras[self._clave(nombre, etiquetas)])
        if not datos:
            return None
        return datos[min(len(datos) - 1, int(p / 100.0 * len(datos)))]

M = Metricas()

class Cronometro:
    def __init__(self, tarea):
        self.tarea = tarea

    def __enter__(self):
        self.t0 = time.perf_counter()
        return self

    def __exit__(self, tipo, valor, traza):
        M.observar("tarea_segundos", time.perf_counter() - self.t0, tarea=self.tarea)
        if tipo is not None:
            M.contar("errores", tarea=self.tarea)
        return False

Mide siempre percentiles, no medias. La media de la duración de una tarea la aplastan los días buenos; lo que rompe un sistema es la cola, el percentil 95 que un martes se come la ventana de ejecución entera.

El deslizamiento real contra el esperado es el termómetro

De todas las métricas, la que más pronto detecta una degradación silenciosa es la comparación entre el deslizamiento que pagas y el que asumía tu simulación. Tu backtest usó un número, típicamente unos pocos puntos básicos. Ese número es una hipótesis, y en producción tienes la posibilidad de contrastarla todos los días.

def registrar_deslizamiento(orden, ejecucion):
    signo = 1.0 if orden["lado"] == "compra" else -1.0
    base = orden["precio_senal"]
    real_pb = signo * (ejecucion["precio"] - base) / base * 10000.0
    esperado_pb = orden["deslizamiento_esperado_pb"]
    M.observar("deslizamiento_pb", real_pb, simbolo=orden["simbolo"])
    M.observar("exceso_pb", real_pb - esperado_pb, simbolo=orden["simbolo"])
    registrar(logging.INFO, "deslizamiento", simbolo=orden["simbolo"],
              precio_senal=base, precio_ejecucion=ejecucion["precio"],
              real_pb=round(real_pb, 2), esperado_pb=esperado_pb)

Si el exceso medio se aleja de cero de forma sostenida, algo se ha movido: liquidez del valor, hora a la que envías, tamaño relativo de tu orden o ruta de ejecución. Ninguna de esas cosas aparece en el resultado del día de forma legible; en esta métrica aparecen enseguida.

Las de la estrategia dicen cuánto duele, y hay que leerlas juntas

La exposición bruta suma el valor absoluto de todas las posiciones; la neta suma con signo. Leer solo una lleva a conclusiones falsas: una cartera con bruta de 180.000 y neta de 4.000 sobre un patrimonio de 100.000 no está "casi en efectivo", está muy apalancada y casi neutral al mercado, con un riesgo que no viene de la dirección sino de que sus pares dejen de comportarse como esperabas.

def instantanea_estrategia(posiciones, precios, efectivo, maximo_previo):
    bruta = sum(abs(c) * precios[s] for s, c in posiciones.items())
    neta = sum(c * precios[s] for s, c in posiciones.items())
    patrimonio = efectivo + neta
    maximo = max(maximo_previo, patrimonio)
    caida = 0.0 if maximo <= 0 else (patrimonio / maximo) - 1.0
    M.observar("exposicion_bruta", bruta)
    M.observar("exposicion_neta", neta)
    M.observar("caida_desde_maximo", caida)
    return patrimonio, maximo, caida

Escenario trabajado: tres puntos que no estaban en el modelo

Escenario inventado con fines didácticos. Una estrategia diaria rinde durante un mes tres puntos porcentuales por debajo de lo simulado. Las señales coinciden una a una con las del backtest y no hubo ni un rechazo. La métrica de exceso de deslizamiento cuenta la historia: media de 17 puntos básicos frente a los 4 asumidos, concentrada en los dos valores menos líquidos de la cesta y en las órdenes enviadas en el primer minuto de sesión. La estrategia no estaba rota; la ejecución sí. Sin esa métrica, el equipo habría reoptimizado los parámetros del modelo y habría empeorado las dos cosas: seguiría pagando el mismo coste de ejecución y encima sobre una señal ajustada a un mes concreto. La corrección fue partir las órdenes de los dos valores ilíquidos y retrasar el envío quince minutos, hasta que el libro se asienta. Esto es formación, no asesoramiento financiero: el número que importa no es 17 ni 4, sino la costumbre de comparar lo que pagas con lo que asumiste.

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.