Kill switches & when to turn it off
Hard drawdown limits, data-feed failures, regime change and circuit breakers — plus real Python for a kill switch that flattens every position and halts the bot automatically.
Every professional trading operation has a big red button, and it is wired to trip *automatically* long before a human would think to press it. The purpose of a kill switch is to convert an open-ended, emotional, catastrophic loss into a bounded, mechanical, survivable one. You decide the limits when you are calm; the code enforces them when you are not.
The four triggers
- Hard drawdown limit — equity falls a set % from its peak (say 10%). Non-negotiable. This is the master switch.
- Daily loss limit — you lose more than X% *today*. Stops a single bad day from becoming a bad week. Resets tomorrow.
- Data-feed failure — the feed goes stale (from Lesson 2's health check). Trading on dead data is how bots buy into a crash they cannot see.
- Anomaly / regime break — realised volatility or spread explodes past anything in your backtest. Your edge was never tested in this world; step aside until it normalises.
A real drawdown kill switch
Here is a self-contained kill switch you can drop into the main loop. It tracks the equity peak, checks every trigger, and when any trips it does two things in order: flatten all positions, then halt so no new orders can be placed until a human clears it.
import time, logging
class KillSwitch:
def __init__(self, broker, alerter,
max_drawdown=class="n">0.10, class="c"># class="n">10% from peak equity
max_daily_loss=class="n">0.05, class="c"># class="n">5% loss in one day
max_data_age_s=class="n">120):
self.broker = broker
self.alert = alerter
self.max_dd = max_drawdown
self.max_daily = max_daily_loss
self.max_data_age = max_data_age_s
self.peak_equity = broker.equity()
self.day_start_equity = broker.equity()
self.halted = False
def _trip(self, reason):
class="s">""class="s">"Flatten everything, halt, and shout about it. Idempotent."class="s">""
if self.halted:
return
self.halted = True
logging.critical(fclass="s">"KILL SWITCH TRIPPED: {reason}")
self.alert.send(class="s">"critical", fclass="s">"KILL SWITCH: {reason}. Flattening all positions.")
try:
self.broker.close_all_positions() class="c"># flatten first
self.broker.cancel_all_orders() class="c"># then kill working orders
except Exception as e:
self.alert.send(class="s">"critical", fclass="s">"FLATTEN FAILED: {e} -- MANUAL ACTION NEEDED")
def check(self, last_data_ts):
if self.halted:
return False class="c"># already down; stay down
equity = self.broker.equity()
self.peak_equity = max(self.peak_equity, equity)
drawdown = (self.peak_equity - equity) / self.peak_equity
daily_loss = (self.day_start_equity - equity) / self.day_start_equity
data_age = time.time() - last_data_ts
if drawdown >= self.max_dd:
self._trip(f"drawdown {drawdown:.class="n">1%} >= limit {self.max_dd:.class="n">0%}")
elif daily_loss >= self.max_daily:
self._trip(f"daily loss {daily_loss:.class="n">1%} >= limit {self.max_daily:.class="n">0%}")
elif data_age > self.max_data_age:
self._trip(fclass="s">"data stale for {data_age:.0f}s")
return not self.halted class="c"># True == safe to keep trading
def new_day(self):
class="s">""class="s">"Call at the session rollover to reset the daily counter."class="s">""
self.day_start_equity = self.broker.equity()Notice the order of operations inside _trip: flatten, then halt, then alert — and if flattening itself fails, it escalates to a "manual action needed" alert rather than pretending everything is fine. A kill switch that cannot close positions must scream for a human. Notice too that it is *idempotent*: once tripped it stays tripped, so a flapping trigger cannot spam orders.
Wiring it into the loop
kill = KillSwitch(broker, alerter, max_drawdown=class="n">0.10)
while market_is_open():
data = broker.get_latest_data()
if not kill.check(last_data_ts=data.timestamp):
logging.warning(class="s">"Halted by kill switch — no trading. Awaiting human.")
time.sleep(class="n">30)
continue class="c"># bot is alive but flat and quiet
signal = strategy.generate(data)
if signal:
broker.submit_order(signal, risk.position_size(broker.equity(), data))
time.sleep(strategy.interval)The human triggers, too
Automated limits handle the measurable dangers. Some dangers are not in your metrics — a central bank surprise, a broker outage, a war headline, a strategy behaving in a way you simply do not understand. For those, *you* are the kill switch. The rule of thumb:
When you do not understand what your system is doing, you flatten and halt first, and understand second. Confusion is a position size of zero.The oldest rule in systematic trading
You can now stop cleanly, automatically, and on your own terms. The next lesson is about the slower, quieter discipline that decides whether your edge is still an edge at all: reviewing the live system.