Guardrails for Live Capital: Leverage Caps, Sanity Checks, and a Kill Switch
Jonny Bravo
-Guardrails for Live Capital: Leverage Caps, Sanity Checks, and a Kill Switch
A backtest can't hurt you. A live signal can. The moment a strategy stops writing numbers to a results table and starts emitting positions that an executor will turn into real orders, the cost of a bad number changes from "a wrong chart" to "a margin call." Everything that was a harmless quirk in research becomes a live risk: a NaN that slips through, a leverage figure that's an order of magnitude too high, a strategy that's silently throwing errors but still being asked to trade.
The defense is a set of guardrails that sit between the signal and the order — code whose only job is to look at what a strategy wants to do and decide whether it's sane enough to allow. This is the least exciting and most important layer in the whole system, and we built it to fail closed.
Validate every signal before it can become an order
Before any signal row reaches fund.positions and the live executor, it runs through a validator whose contract is simple: return the list of problems, and an empty list means safe.
def validate_signal_rows(rows, max_abs=MAX_ABS_TRADE_SIDE_SIZE, max_gross=MAX_GROSS_TRADE_SIDE_SIZE): """Return a list of problems (empty == safe). Guards against NaN, bad prices, and over-leverage before signals reach fund.positions / the live executor.""" problems = [] gross = 0.0 for row in rows: label = row.get("broker_instrument_id") or row.get("instrument_id") or "?" close = row.get("close") tss = row.get("trade_side_size") if close is None or (isinstance(close, float) and math.isnan(close)) or float(close or 0) <= 0: problems.append(f"{label}: invalid close ({close})") ...
The first thing it checks is the most boring and most lethal: is the price real? A NaN close, a None, a zero or negative price — any of these poisons every downstream calculation. Position sizing divides by price; a bad price produces a nonsensical quantity; a nonsensical quantity becomes a real order. So the very first gate is "is this number a number," and a signal that fails it never moves forward. Boring checks stop catastrophic bugs.
Leverage caps, per-leg and gross
The guardrails encode two distinct leverage limits, because there are two distinct ways to blow up. One leg can be individually enormous, or many modest legs can sum to an enormous book:
# trade_side_size = per-instrument notional / AUM. MAX_ABS_TRADE_SIDE_SIZE = float(os.environ.get("FUND_MAX_TRADE_SIDE_SIZE", "5.0")) # per-leg leverage cap MAX_GROSS_TRADE_SIDE_SIZE = float(os.environ.get("FUND_MAX_GROSS_TRADE_SIDE_SIZE", "10.0")) # gross book cap
The per-leg cap (max_abs) stops a single instrument from claiming 8x your AUM because a parameter was fat-fingered or a strategy produced a runaway size. The gross cap (max_gross) stops the sum of all legs from quietly exceeding the book's total risk budget even when no single leg looks alarming — the classic way a multi-instrument strategy over-extends without any one position raising a flag. Both are environment-tunable, so a conservative mandate can dial them down and an aggressive one can lift them, deliberately, with intent.
Crucially these limits are checked in the signal runner — before the executor ever sees the rows. The over-leverage is caught at the point of generation, not discovered at the point of execution when orders are already going out.
A kill switch for the strategy that's gone wrong
The last guardrail is the one that handles a strategy turning toxic over time. A strategy that throws errors, returns no data, or produces stale signals run after run isn't a transient blip — it's a strategy that has broken, and continuing to ask it to trade is how a broken strategy becomes a financial incident. So there's an automatic kill switch:
KILL_SWITCH_CONSECUTIVE = int(os.environ.get("FUND_KILL_SWITCH_CONSECUTIVE", "3")) # auto-disable threshold _BAD_STATUSES = {"error", "stale_data", "no_data", "rejected"}
After a configurable number of consecutive bad runs — three by default — the strategy is automatically disabled. No human has to be watching at 3am. The system notices the pattern, decides the strategy is no longer healthy, and stops it from trading until someone investigates. The threshold is tunable, and the set of "bad" statuses is explicit: an error, stale data, no data, a rejection. Any of those, three times running, and the strategy is benched.
This is the difference between a system that degrades safely and one that degrades dangerously. Without a kill switch, a strategy that starts emitting garbage emits garbage forever, every cron tick, until the damage is discovered manually. With it, the blast radius is capped at a handful of runs.
Why fail-closed is the only acceptable default
The philosophy threaded through all of this is fail closed. When something is wrong — a bad price, too much leverage, a string of failed runs — the default action is to stop, not to push forward and hope. A signal with problems doesn't trade. An over-leveraged book is rejected. A repeatedly-failing strategy disables itself. The safe state is the default state, and it takes a clean, valid, sane signal to earn the right to reach the exchange.
For anyone putting real capital behind an automated system, this is the layer that lets them sleep. Not the alpha, not the Sharpe — the knowledge that between the strategy's imagination and the exchange's order book sits a wall of checks designed by people who assumed things would go wrong, and engineered for what happens when they do.
The strategy's job is to find edge. The guardrails' job is to make sure a bad day stays a bad day instead of becoming the last day. Both ship in the box.
Every live signal passes NaN, leverage, and health checks before it can trade — and a misbehaving strategy benches itself. Explore live safety →