2. Kill switches and hard limits
Level 11: When it fails
A hard limit is a condition the system checks before sending each order and which, if not met, prevents the send. It is not a warning, it is not a line in the log, it is not one more strategy parameter. It is a closed door. The strategy proposes and the checker disposes, and its default answer is no.
These limits protect you from yourself, not from the market
It is worth dispelling a very widespread misunderstanding early. A daily loss limit does not protect you from a bad run: a bad run is within what your strategy does, you saw it in the backtest and you will see it again. What a hard limit really stops is the other thing: the loop that sends the same order three hundred times, the size computed with a divisor that came out zero, the reference price that was yesterday's, the five o'clock deployment with a sign flipped.
Put another way: hard limits are damage control against software and operational errors, not a market risk management technique. That they also cut off some unpleasant market tail is a pleasant side effect. If you design them with the market in mind you will set them far too loose for exactly what they were supposed to catch. From that follow three properties that are not negotiable:
- They are checked in a single place. The mandatory gate every order passes through, with no shortcuts or alternative routes for the urgent case.
- They are absolute. They do not depend on how convinced the signal is or on how good the month has been.
- They reject, they do not correct. Trimming the size to the permitted maximum and sending anyway is reasonable in normal sizing, but it is terrible here: if the order asked for a hundred times the reasonable amount, trimming it turns a serious error into a small trade and you never find out.
The five minimum limits
- Maximum number of orders per session. The dullest one and the only one that would have stopped Knight. If your strategy sends twenty orders a day, forty is a generous ceiling and still impossible to reach by accident.
- Maximum exposure per position and in total. A percentage of equity per line and another for the sum, counting leverage.
- Maximum daily loss. It does not reject an order: it stops the whole session.
- Order size outside the reasonable. Against the name's average volume and against your typical size.
- Price sanity before sending. No limit order goes out at a price far from the last valid price.
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 TrueEach check returns a readable reason, and that is not cosmetic: the day it fires, the difference between fixing it in two minutes and spending the morning guessing is that piece of text. And the first line consults the brake, because the question that precedes all others is whether the system has permission to trade.
Where the counter lives
The error that recurs in every home-made system: the order counter and the brake live in an attribute of the object, so you restart the process and they go back to zero. And the scenario in which you most need the limit is precisely a loop of failure and restart: it starts, sends orders, falls over, the supervisor brings it back, it starts, it sends orders. With the counter in memory, a cap of forty orders per session allows forty per restart, and a cap is not a cap if it can be restarted away.
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 TrueThe counter and the brake are stored with the session's date, in the same database where you keep the state, and the brake is only released by hand. None of this clearing itself the next day: if it fired, somebody has to look at why before trading again. The opening equity is recorded when the session opens and is not recomputed, because if you recompute it after a fall the limit moves with it and stops being a limit.
The specific numbers in this module are teaching examples to illustrate the mechanism; this is education, not financial advice.
Real case: Mizuho Securities, December 2005
A Mizuho Securities trader meant to sell one share of the company J-Com at 610,000 yen and typed the order exactly the other way round: 610,000 shares at one yen. That quantity far exceeded the total number of the company's existing shares. The order entered the market and the loss to Mizuho was of the order of tens of billions of yen, that is, hundreds of millions of dollars.
What matters is not the typing error, which is unavoidable and will happen again tomorrow on another desk. What matters is everything that failed to stop it: nothing compared the quantity against the name's usual volume, nothing compared the price against the prevailing quote, and the warning that did appear could be dismissed with a keystroke. Either of this module's two sanity checks, even with very generous thresholds, would have blocked that order before it went out.