AlgoPro UniversityCourse home
Part 10 · Lesson

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

  1. Hard drawdown limit — equity falls a set % from its peak (say 10%). Non-negotiable. This is the master switch.
  2. Daily loss limit — you lose more than X% *today*. Stops a single bad day from becoming a bad week. Resets tomorrow.
  3. 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.
  4. 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.

pythonA production drawdown / daily-loss / data kill switch
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

pythonThe kill switch as a gate on every iteration
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.