Quant500 Academy

2. Which metrics to measure in a trading system

Level 10: Knowing what is going on

Measuring is not storing numbers just in case. A metric only earns its place if there is a decision you would take differently depending on its value. With that filter, a trading system's list comes out short and clear, and it splits into two families that should never be mixed on the same dashboard.

Two families answering different questions

The system metrics answer "is the machinery working?". There are five, and with those five you cover nearly everything:

The strategy metrics answer "how much am I making or losing?": position by symbol, gross and net exposure, the day's result and drawdown from peak equity.

Keep the two families on separate dashboards, and not for aesthetics: they are read at different moments. The system ones, every morning before trading; the strategy ones, at the close of the day. Mixing them produces a dashboard of forty numbers in which the eye cannot find the one that matters, which is the debug-level logging problem transferred to charts.

The system ones warn before the strategy loses money

This is the module's central idea. Strategy metrics are lagging: by the time the drawdown from peak worsens, the money has already gone. System metrics are leading: they describe the conditions under which each decision is being taken, and they degrade before the effect reaches the result.

A typical example: the data delay goes from three seconds to ninety over the course of a morning. The strategy is still in the green, there are no rejections and nobody has noticed anything. But the signals are being computed on stale candles, and that misalignment between what the model believes it sees and what the market is doing will eventually show up in the result days later. If you only look at the result, you find out when there is nothing left to do.

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

Always measure percentiles, not means. The mean duration of a task is flattened by the good days; what breaks a system is the tail, the 95th percentile that one Tuesday eats the entire execution window.

Real slippage against expected slippage is the thermometer

Of all the metrics, the one that detects a silent degradation earliest is the comparison between the slippage you pay and the one your simulation assumed. Your backtest used a number, typically a few basis points. That number is a hypothesis, and in production you have the chance to test it every day.

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)

If the average excess moves away from zero in a sustained way, something has shifted: the name's liquidity, the time you send at, the relative size of your order or the execution route. None of those things shows up legibly in the day's result; in this metric they show up immediately.

The strategy ones say how much it hurts, and must be read together

Gross exposure sums the absolute value of every position; net sums them with sign. Reading only one leads to false conclusions: a portfolio with gross of 180,000 and net of 4,000 on equity of 100,000 is not "almost in cash", it is heavily leveraged and almost market-neutral, with a risk that comes not from direction but from its pairs ceasing to behave as you expected.

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

Worked scenario: three points that were not in the model

A scenario invented for teaching purposes. A daily strategy runs for a month three percentage points below simulation. The signals match the backtest's one for one and there was not a single rejection. The excess slippage metric tells the story: an average of 17 basis points against the 4 assumed, concentrated in the two least liquid names in the basket and in the orders sent in the first minute of the session. The strategy was not broken; the execution was. Without that metric, the team would have reoptimized the model's parameters and made both things worse: it would still be paying the same execution cost and now on top of a signal fitted to one particular month. The fix was to split the orders in the two illiquid names and to delay the send by fifteen minutes, until the book settles. This is education, not financial advice: the number that matters is not 17 or 4, but the habit of comparing what you pay with what you assumed.

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.