# Trading bot kill switch: triggers, cancel-all and tests

URL: https://fincodelabs.com/en/blog/trading-bot-kill-switch · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Backtesting and risk · Published: 2026-11-06 · Updated: 2026-11-06

Trading bot kill switch design: what RTS 6, FINRA and FIA describe, triggers, who resets it, cancel-all limits per venue, and tested Python with a mock.

**Short answer:** A **trading bot kill switch** blocks new orders, cancels every working order and stays on until a named person resets it. Because it must work when the strategy has failed, it needs a latch that survives restarts and a cancel loop that reads the venue back until no orders remain. EU RTS 6 Article 12 and FIA's July 2024 best practices describe the function.

A loss limit that stops the bot is one trigger, not a kill switch. The hard parts are fills during cancels, cancel-alls that cover one symbol, restarts that forget the switch and config values that disable a limit.

This is engineering guidance, not a strategy, setting or venue recommendation; automated trading can lose money with every control in place, and rules depend on jurisdiction and venue terms. For readers in Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## What a trading bot kill switch does, and what it is not

A kill switch blocks risk-increasing orders, cancels working ones and stays on until a designated person re-enables trading.

**Kill switch and neighboring controls**

| Control | What it does | Turned back on by |
| --- | --- | --- |
| Kill switch | Blocks and cancels; may still allow risk-reducing orders (FIA, July 2024) | A designated person |
| Dead man's switch or cancel-on-disconnect | The venue cancels when a timer lapses or a session drops | Your code re-arms it |
| Framework kill switch | Hummingbot's stops the bot at a profit or loss threshold; its page does not say if open orders are canceled | The user |

Canceling orders does not close a position, so decide in writing whether the switch also reduces (see the [reduce-only order guide](https://fincodelabs.com/en/blog/reduce-only-order)); the [risk management guide](https://fincodelabs.com/en/blog/trading-bot-risk-management) covers other limits.

## Kill switch requirements: what regulators and exchanges describe

EU law requires investment firms that trade algorithmically to have kill functionality; US guidance and an industry paper describe it less formally.

**Read 6 November 2026; private bots are usually out of scope, but the list still helps**

| Source | What it says | For a bot |
| --- | --- | --- |
| RTS 6 (EU 2017/589) Article 12 | Cancel immediately any or all unexecuted orders on any or all venues; know which algorithm sent each order | Every symbol and venue |
| RTS 6 Articles 15(3) and 16 | A tripped throttle disables the system until a designated staff member re-enables it; alerts within five seconds | Named human reset; fast alerts |
| FINRA Regulatory Notice 15-09 (March 2015) | Effective practices include disabling an algorithm or its platform quickly, in few steps | One command |
| FIA best practices (July 2024) | May sit outside the trading application, operable by the trader and the risk person | Separate process |
| CME Client Systems Wiki (updated 12 December 2025) | Blocks order entry by SenderComp ID and cancels working orders, on a best-efforts basis | Read back anyway |

On 1 August 2012 Knight Capital's router sent more than 4 million orders in 45 minutes; Knight lost more than $460 million (SEC press release 2013-222).

## Kill switch triggers and who can press it

Triggers are conditions the bot can measure, plus a manual switch that works without the strategy process.

- **Loss limit or unknown P&L.** `float("nan") <= -500` is `False`, so `pnl <= -limit` never fires on NaN; write `not pnl > -limit`.
- **Missing heartbeat** from the strategy loop; see [heartbeats and reconnects](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect).
- **Reject bursts**: the bot misreads its own state.
- **Unknown orders or a position mismatch**; see [position reconciliation](https://fincodelabs.com/en/blog/position-reconciliation).
- **Stale market data.** FIA says to block orders while a deviation is investigated.

No threshold is suggested; limits depend on the strategy and account. The trader and the risk person need a file or command on the host, with the venue's interface as a last resort; FIA notes a client-side network failure can cut those routes too, leaving cancel-on-disconnect. The bot may trip the switch, never reset it.

## Cancel all orders API: how venues differ

Venues cancel "all" in different ways, so read open orders back after canceling and repeat until none remain.

**Cancel-all and timer endpoints, read 6 November 2026**

| Venue | Cancel-all | Timer or disconnect cancel |
| --- | --- | --- |
| Binance Spot | `DELETE /api/v3/openOrders`: one symbol per call, order lists included | None found in its REST, WebSocket or FIX API documents |
| Binance USD-M futures | `DELETE /fapi/v1/allOpenOrders`: one symbol per call | `countdownCancelAll`: per symbol, milliseconds, 0 disables; example: refresh every 30 seconds with 120,000 ms |
| Bybit V5 | `POST /v5/order/cancel-all`: futures need a symbol or coin and stop at 500 orders, picked at random; spot cancels normal orders unless `orderFilter` says otherwise | Disconnection Protect: 3 to 300 seconds (default 10); Bybit "Ins" clients only, not VIP; `dcp` topic required |
| Kraken Spot | `CancelAll`: all open orders; the reply may say `pending` | `CancelAllOrdersAfter`: under 86,400 seconds; refresh every 15 to 30 seconds with a 60-second timeout recommended; off after firing; disable before maintenance |

- **An acknowledgment is not a result.** Bybit calls its cancel-all asynchronous; Binance's USD-M general information (last modified 30 October 2026) says an HTTP 503 unknown error leaves execution status unknown.
- **Timers.** A timer refreshed by a separate thread survives a hung strategy; refresh it from the strategy loop.

## A tested kill switch in Python with a mock exchange

The module keeps the switch in a file, blocks before canceling, cancels by exchange order ID across all symbols and re-reads until none remain. Python 3.11, standard library, no venue or key, run on 6 November 2026; the mock injects a timeout, a fill during a cancel, a late order, a lost latch folder and a refused key.

```python
import json, math, os, tempfile, time

class NetworkError(Exception): pass
class AuthError(Exception): pass
class UnknownOrder(Exception): pass

def parse_limit(name, raw):
    try:
        value = float(raw)
    except (TypeError, ValueError):
        raise ValueError(f"{name}={repr(raw)} is not a number") from None
    if not math.isfinite(value) or value <= 0:      # '1e400' is inf, '1e-400' is 0.0
        raise ValueError(f"{name}={repr(raw)} must be finite and above zero")
    return value

class Latch:
    """Kill state in a file; unreadable counts as tripped."""
    def __init__(self, path):
        if not os.path.isdir(os.path.dirname(os.path.abspath(path))):
            raise ValueError("no folder for the kill latch")   # else it reads as off
        self.path = path

    def state(self):
        try:
            with open(self.path, encoding="utf-8") as f:
                s = json.load(f)
        except FileNotFoundError:
            return None
        except (OSError, ValueError):
            s = None
        return s if isinstance(s, dict) and s.get("reason") else {"reason": "latch file unreadable"}

    def trip(self, reason):
        try:                                            # O_EXCL: the first reason wins
            fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        except FileExistsError:
            return
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump({"reason": reason, "at": time.time()}, f)

    def reset(self, person, venue):
        if not person.strip():
            return "refused: name the person"
        if venue.open_orders() == []:
            os.remove(self.path)
            return f"reset by {person}; confirm positions {venue.positions}"
        return "refused: orders still open"

def place(venue, latch, symbol, side, qty):
    state = latch.state()                               # read before every order, never cached
    if state:
        return f"blocked ({state['reason']})"
    return f"placed {venue.place(symbol, side, parse_limit('qty', qty))}"

def kill(venue, latch, reason, rounds=5):
    try:
        latch.trip(reason)                              # block first, then cancel
        warn = ""
    except OSError as e:                                # cancel anyway, then escalate
        warn = f"; ESCALATE: latch not written ({type(e).__name__})"
    for n in range(1, rounds + 1):
        try:
            orders = venue.open_orders()                # all symbols
            if orders == []:                            # None or {} is not "clean"
                return f"clean after {n} reads; positions {venue.positions}{warn}"
            for o in orders:
                try:
                    venue.cancel(o["orderId"])          # exchange ID, never client ID
                except UnknownOrder:                    # filled or gone: next read decides
                    pass
        except NetworkError:
            continue                                    # real code pauses before retrying
        except Exception as e:                          # anything else: a person acts
            return f"ESCALATE: {repr(e)}; cancel by hand at the venue{warn}"
    return f"ESCALATE: orders may still be open after {rounds} rounds{warn}"

class MockVenue:
    def __init__(self, read_errors=(), late=False, fills=()):
        self.orders, self.positions, self.next_id = {}, {}, 1
        self.read_errors, self.late, self.fills = list(read_errors), late, set(fills)

    def place(self, symbol, side, qty):
        oid, self.next_id = self.next_id, self.next_id + 1
        self.orders[oid] = {"orderId": oid, "symbol": symbol, "side": side, "qty": qty}
        return oid

    def open_orders(self):
        if self.read_errors:
            raise self.read_errors.pop(0)
        out = list(self.orders.values())
        if self.late:                                   # lands after this read
            self.late = False
            self.place("ETHUSDT", "BUY", 2.0)
        return out

    def cancel(self, oid):
        if oid in self.fills:                           # fills while the cancel travels
            self.fills.remove(oid)
            o = self.orders.pop(oid)
            signed = o["qty"] if o["side"].upper() == "BUY" else -o["qty"]
            self.positions[o["symbol"]] = self.positions.get(o["symbol"], 0.0) + signed
        if oid not in self.orders:
            raise UnknownOrder(oid)
        del self.orders[oid]

d = tempfile.mkdtemp()
v = MockVenue(read_errors=[NetworkError("timeout")], late=True, fills={1})
for sym, side, qty in [("BTCUSDT", "BUY", 0.5), ("BTCUSDT", "SELL", 0.5), ("ETHUSDT", "BUY", 3.0)]:
    v.place(sym, side, qty)
latch = Latch(os.path.join(d, "kill.json"))
print("kill:", kill(v, latch, "manual: operator"))
v.place("ETHUSDT", "SELL", 1.0)                         # placed by hand at the venue
print("reset:", latch.reset(" ", v))
print("reset:", latch.reset("duty-operator", v))
v.cancel(5)
print("after restart:", place(v, Latch(latch.path), "BTCUSDT", "SELL", 0.5))
print("reset:", latch.reset("duty-operator", v))
print("after reset:", place(v, latch, "BTCUSDT", "SELL", 0.5))
open(latch.path, "w").close()                           # a half-written latch file
print("corrupt latch:", place(v, latch, "BTCUSDT", "BUY", 0.1))
lost = Latch(os.path.join(tempfile.mkdtemp(), "k4"))
os.rmdir(os.path.dirname(lost.path))                    # the latch folder vanishes
print("kill:", kill(v, lost, "manual"))
print("kill:", kill(MockVenue(read_errors=[AuthError("invalid key")]), Latch(os.path.join(d, "k2")), "manual"))
for raw in ["1e400", "1e-400"]:
    try:
        parse_limit("max_loss", raw)
    except ValueError as e:
        print("refused:", e)
```

Output:

```
kill: clean after 4 reads; positions {'BTCUSDT': 0.5}
reset: refused: name the person
reset: refused: orders still open
after restart: blocked (manual: operator)
reset: reset by duty-operator; confirm positions {'BTCUSDT': 0.5}
after reset: placed 6
corrupt latch: blocked (latch file unreadable)
kill: clean after 2 reads; positions {'BTCUSDT': 0.5}; ESCALATE: latch not written (FileNotFoundError)
kill: ESCALATE: AuthError('invalid key'); cancel by hand at the venue
refused: max_loss='1e400' must be finite and above zero
refused: max_loss='1e-400' must be finite and above zero
```

> **Synthetic run.** Four reads: a timeout, three orders found while a fourth landed, the late one, then none. Order 1 filled during its cancel, so 0.5 BTCUSDT remains. A hand-placed order at the venue blocked the reset until canceled.

A fuller design with a journal and crash recovery is [Fincode's order gateway reference](https://fincodelabs.com/en/reference/order-gateway).

## Failure modes the tests caught

Each case broke a draft or a naive check.

- **Client IDs as keys.** Keyed by [client order ID](https://fincodelabs.com/en/blog/client-order-id), six orders with empty IDs became one entry per read, and one was still open after five rounds; keyed by exchange ID, none were left after two reads.
- **A false "clean".** A draft read `None` (from `.get` on an error body) as no open orders.
- **Reducing orders while tripped.** If you allow them, count working orders: a position-only guard let a second 0.5 sell through against a 0.5 long, and `== "BUY"` read `'buy'` as a sell.

Known limits: an order sent just before the trip can land after the last clean read, so read again after the longest timeout; if the latch cannot be written, `kill()` still cancels but new orders are not blocked; and whoever can delete the file can reset the switch, so log trips and resets elsewhere.

### What a backtest cannot show

A backtest replays prices: no cancel in flight, no fill during a cancel, no rate limit, disconnect, restart or person. At most it shows how often a loss trigger would have fired, which describes the strategy, not the kill path. Test that with fault injection, test-environment drills for the parts that need a key, and by killing the bot process while orders work; see the [testing guide](https://fincodelabs.com/en/blog/how-to-test-trading-bots).

## Restart rules after a kill switch fires

Restart only after a named person resets the switch, the venue shows no working orders and positions are reconciled.

1. Keep the latch on; a restart is not a reset.
2. Read orders and positions from the venue, not the bot's memory; handle any leftover position separately.
3. Fix the cause; the same trigger again means a loop.
4. Re-arm venue timers; Kraken's stays off after firing.
5. Drop orders queued before the trip.
6. Log who reset it and why.

For review work in general, see Fincode's [trading bot code review](https://fincodelabs.com/en/services/trading-bot-code-review); naming Binance, Bybit, Kraken or CME here does not mean Fincode integrates them or has built on them.

## Frequently asked questions

### What is a kill switch in algorithmic trading?

A control that blocks new orders and cancels working ones at once. RTS 6 Article 12 requires EU investment firms that trade algorithmically to be able to cancel all unexecuted orders immediately.

### How do you test a kill switch?

With injected faults against a mock venue (timeouts, fills during cancels, late orders, a refused key), then drills in a venue's test environment for the parts that need a key.

### Is an exchange dead man's switch enough?

No. It covers one venue, on Binance USD-M one symbol, and does not stop your bot sending new orders. Use it beneath your own switch.

### Who should be allowed to reset a kill switch?

A designated person, never the bot. RTS 6 Article 15(3) sets the same rule for execution throttles.

## Sources

- [Commission Delegated Regulation (EU) 2017/589 (RTS 6), Article 12: kill functionality, as adopted (legislation.gov.uk)](https://www.legislation.gov.uk/eur/2017/589/article/12/adopted)
- [RTS 6, Chapter II Section 3 (Articles 12 to 18), including Article 15(3) on re-enabling a throttled system and Article 16(5) on five-second alerts, as adopted (legislation.gov.uk)](https://www.legislation.gov.uk/eur/2017/589/chapter/II/section/3/adopted)
- [FINRA Regulatory Notice 15-09: Guidance on Effective Supervision and Control Practices for Firms Engaging in Algorithmic Trading Strategies (March 2015)](https://www.finra.org/rules-guidance/notices/15-09)
- [FIA: Best Practices for Automated Trading Risk Controls and System Safeguards (July 2024), sections 1.4 to 1.6 (pp. 6 to 9), 3.1 and 3.2 (pp. 14 and 15)](https://fia.org/sites/default/files/2024-07/FIA_WP_AUTOMATED%20TRADING%20RISK%20CONTROLS_FINAL_0.pdf)
- [SEC press release 2013-222: SEC Charges Knight Capital With Violations of Market Access Rule](https://www.sec.gov/newsroom/press-releases/2013-222)
- [CME Group Client Systems Wiki: Kill Switch (last updated 12 December 2025)](https://cmegroupclientsite.atlassian.net/wiki/spaces/EPICSANDBOX/pages/457088324/Kill+Switch)
- [Binance Spot API documentation: REST API, Cancel All Open Orders on a Symbol (file last changed 28 October 2026)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance USD-M Futures: Trade endpoints (Cancel All Open Orders, Auto-Cancel All Open Orders)](https://developers.binance.com/en/docs/catalog/core-trading-derivatives-trading-usd-s-m-futures/api/rest-api/trade)
- [Binance USD-M Futures: General info (HTTP 503 unknown execution status; last modified 30 October 2026)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/general-info)
- [Bybit V5 API: Cancel All Orders](https://bybit-exchange.github.io/docs/v5/order/cancel-all)
- [Bybit V5 API: Disconnection Protect (DCP), POST /v5/order/disconnected-cancel-all](https://bybit-exchange.github.io/docs/v5/order/dcp)
- [Kraken API: Cancel All Orders (POST /0/private/CancelAll)](https://docs.kraken.com/api-reference/trading/cancel-all-orders)
- [Kraken API: Cancel All Orders After X (dead man's switch)](https://docs.kraken.com/api-reference/trading/cancel-all-orders-after-x)
- [Hummingbot documentation: Kill Switch (global config)](https://hummingbot.org/client/global-configs/kill-switch/)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish), mevzuat.gov.tr](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/trading-bot-kill-switch

Cite as: Fincode, "Trading bot kill switch: triggers, cancel-all and tests", updated November 6, 2026, https://fincodelabs.com/en/blog/trading-bot-kill-switch
