Quant500 Academy

2. Interruptores de emergencia y límites duros

Nivel 11: Cuando falla

Un límite duro es una condición que el sistema comprueba antes de enviar cada orden y que, si no se cumple, impide el envío. No es una advertencia, no es una línea en el registro, no es un parámetro más de la estrategia. Es una puerta cerrada. La estrategia propone y el comprobador dispone, y su respuesta por defecto es que no.

Estos límites te protegen de ti, no del mercado

Conviene romper pronto un malentendido muy extendido. Un límite de pérdida diaria no te protege de una racha mala: una racha mala está dentro de lo que tu estrategia hace, la viste en el backtest y la vas a volver a ver. Lo que un límite duro para de verdad es lo otro: el bucle que manda la misma orden trescientas veces, el tamaño calculado con un divisor que salió cero, el precio de referencia que era el de ayer, el despliegue de las cinco de la tarde con un signo cambiado.

Dicho de otro modo: los límites duros son control de daños contra errores de software y de operación, no una técnica de gestión de riesgo de mercado. Que además recorten alguna cola desagradable del mercado es un efecto secundario agradable. Si los diseñas pensando en el mercado los pondrás demasiado holgados justo para lo que tenían que atrapar. De ahí salen tres propiedades que no se negocian:

Los cinco límites mínimos

class LimiteDuro(Exception):
    pass

class Limites:
    def __init__(self, estado, max_ordenes=40, max_peso=0.10,
                 max_bruto=0.95, max_desvio_precio=0.05, max_pct_volumen=0.02):
        self.estado = estado
        self.max_ordenes = max_ordenes
        self.max_peso = max_peso
        self.max_bruto = max_bruto
        self.max_desvio_precio = max_desvio_precio
        self.max_pct_volumen = max_pct_volumen

    def comprobar(self, orden, cartera, referencia):
        if self.estado.freno_activo():
            raise LimiteDuro('freno activado: %s' % self.estado.motivo())
        if self.estado.ordenes_hoy() >= self.max_ordenes:
            raise LimiteDuro('tope de ordenes de la sesion')
        peso = abs(orden.cantidad * referencia.precio) / cartera.patrimonio
        if peso > self.max_peso:
            raise LimiteDuro('peso %.3f sobre el maximo' % peso)
        if cartera.bruto_tras(orden) > self.max_bruto:
            raise LimiteDuro('exposicion bruta fuera de rango')
        if orden.cantidad > self.max_pct_volumen * referencia.volumen_medio:
            raise LimiteDuro('tamano fuera de lo razonable')
        if orden.limite is not None:
            desvio = abs(orden.limite / referencia.precio - 1)
            if desvio > self.max_desvio_precio:
                raise LimiteDuro('precio limite absurdo: %.3f' % desvio)
        return True

Cada comprobación devuelve un motivo legible, y eso no es cosmética: el día que salte, la diferencia entre arreglarlo en dos minutos y pasarte la mañana adivinando es ese texto. Y la primera línea consulta el freno, porque la pregunta anterior a todas es si el sistema tiene permiso para operar.

Dónde vive el contador

El error que se repite en todos los sistemas caseros: el contador de órdenes y el freno viven en un atributo del objeto, así que reinicias el proceso y vuelven a cero. Y el escenario en el que más necesitas el límite es precisamente un bucle de fallo y reinicio: arranca, manda órdenes, se cae, el supervisor lo levanta, arranca, manda órdenes. Con el contador en memoria, un tope de cuarenta órdenes por sesión permite cuarenta por reinicio, y un tope no es un tope si se puede reiniciar.

def registrar_envio(conn, hoy):
    with conn:
        conn.execute(
            "INSERT INTO contador(sesion, n) VALUES(?, 1) "
            "ON CONFLICT(sesion) DO UPDATE SET n = n + 1", (hoy,))

def revisar_perdida_diaria(conn, patrimonio, apertura, max_perdida=0.03):
    caida = patrimonio / apertura - 1
    if caida < -max_perdida:
        activar_freno(conn, 'perdida diaria %.2f por ciento' % (caida * 100))
        return False
    return True

El contador y el freno se guardan con la fecha de la sesión, en la misma base de datos donde llevas el estado, y el freno solo se desactiva a mano. Nada de que se borre solo al día siguiente: si saltó, alguien tiene que mirar por qué antes de volver a operar. El patrimonio de apertura se registra al abrir la sesión y no se recalcula, porque si lo recalculas después de una caída el límite se mueve con ella y deja de ser un límite.

Los números concretos de este módulo son ejemplos didácticos para ilustrar el mecanismo; esto es formación, no asesoramiento financiero.

Caso real: Mizuho Securities, diciembre de 2005

Un operador de Mizuho Securities quiso vender una acción de la empresa J-Com a 610.000 yenes y tecleó la orden justo al revés: 610.000 acciones a un yen. Esa cantidad superaba con creces el número total de acciones existentes de la compañía. La orden entró en el mercado y la pérdida para Mizuho fue del orden de decenas de miles de millones de yenes, es decir, cientos de millones de dólares.

Lo importante no es el error de tecleo, que es inevitable y volverá a ocurrir mañana en otra mesa. Lo importante es todo lo que no lo paró: nada comparó la cantidad con el volumen habitual del valor, nada comparó el precio con la cotización vigente y el aviso que sí llegó a aparecer se podía descartar con una tecla. Cualquiera de las dos comprobaciones de cordura de este módulo, incluso con umbrales muy generosos, habría bloqueado esa orden antes de salir.

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.