A manual trader has an implicit risk system: they get uncomfortable and stop. An automated one has no such reflex, and will cheerfully execute the four hundredth iteration of a losing rule at 14:58 on a day when the assumption underneath it stopped holding at 09:30. Every control that a discretionary trader supplies with judgement has to be written down.
Layer 1 — Position limits
Layered model
Five independent layers of defence
Controls overlap deliberately: each catches a different failure and remains useful when another component is wrong.
Pre-trade position limits prevent oversized exposure. Mark-to-market loss limits flatten a losing book. Order-rate limits contain runaway loops. An external kill switch removes order access and closes positions without strategy cooperation. Reconciliation compares system, fills and broker truth and stops trading on any mismatch.
The layers should not share one failure domain. In particular, the kill switch and broker reconciliation must survive a strategy process crash.
The maximum quantity the system may hold, per instrument and in aggregate, expressed in lots and checked before every order. This is the least interesting control and it prevents the most spectacular failures, because the classic runaway bug is not a bad signal — it is a loop that fires the same order repeatedly.
limits.pypython
class PositionLimitBreach(Exception): passclass PreTradeGate: """Sits between strategy intent and the order manager. Every order passes through here, including the ones the strategy is certain about.""" def __init__(self, per_instrument: dict[str, int], gross_lots: int): self.per_instrument = per_instrument self.gross_lots = gross_lots def check(self, symbol: str, delta_lots: int, book: dict[str, int]) -> None: projected = book.get(symbol, 0) + delta_lots # Absolute value, because a runaway short is as dangerous as a # runaway long and a naive >= check only catches one of them. cap = self.per_instrument.get(symbol, 0) if abs(projected) > cap: raise PositionLimitBreach(f"{symbol}: {projected} lots exceeds cap {cap}") gross = sum(abs(v) for k, v in book.items() if k != symbol) + abs(projected) if gross > self.gross_lots: raise PositionLimitBreach(f"gross {gross} lots exceeds cap {self.gross_lots}")
Pre-trade check. The point is that it lives between the strategy and the broker, not inside the strategy.
Layer 2 — Loss limits
A maximum loss per day, and usually a maximum drawdown from the account’s recent high. The design question is what "breach" triggers, and there are only two honest answers.
Response to a loss-limit breach
What it actually achieves
Stop opening new positions
Nothing, if the open position is what is losing
Flatten the book and stop
Bounds the day’s loss — the only version that does
Almost every system starts with the first option because it is easier, and almost every system that has had a bad day moves to the second. If the limit does not close positions, it is a preference, not a limit.
Layer 3 — Order rate limits
A cap on orders per second and per minute, enforced by your own code rather than by the broker rejecting you. Two reasons: the broker’s throttle response is a failure you then have to handle, and the SEBI framework makes your peak order rate a regulatory fact about your strategy rather than an implementation detail.
A rate limiter also happens to be the cheapest possible circuit breaker against a logic bug. A loop that would have sent ten thousand orders sends the first fifty and then trips something you can alert on.
Cap orders per second and per minute — a burst limit alone lets a slow leak through.
Count modifications and cancellations. They are order messages and the exchange counts them.
Treat hitting your own limit as an alert, not as normal back-pressure. If it fires in normal operation, either the limit or the strategy is wrong.
Layer 4 — A kill switch that outlives the strategy
System architecture
The kill path must bypass the strategy
A separate supervisor talks directly to the gate and broker, so a deadlocked or runaway strategy cannot block the emergency path.
The strategy sends intents through an order gate to the broker. A separate kill-switch service has its own operator trigger, process, credentials and broker session. On activation it disables the order gate, queries broker open orders and positions, cancels orders, flattens from broker-reported quantities, verifies flat and alerts. It does not wait for the strategy.
Independence means different process, credentials and preferably infrastructure—not a Boolean variable inside the process being killed.
This is the control most often built wrong. A kill switch implemented as a flag the strategy checks each iteration does not help when the strategy is stuck, deadlocked, or in a tight loop — which are exactly the conditions under which you want to kill it.
The kill switch has to be a separate process with its own credentials and its own connection to the broker, capable of cancelling all open orders and flattening all positions without cooperation from the strategy. It should also be able to stop the strategy, but flattening must not depend on that succeeding.
What a usable kill switch does, in order
1
Revoke the strategy’s ability to place new orders
Ideally at the gate layer, so it takes effect even if the strategy process is unresponsive. A shared flag in Redis that the order manager reads before every send works; a variable inside the strategy does not.
2
Cancel every open order
Query the broker for open orders rather than relying on your own record of them. Your record is what might be wrong.
3
Flatten every position
Market orders, from the broker’s reported position, not from your internal book. Confirm each fill.
4
Verify flat, then alert
Re-query positions and confirm they are zero. A kill switch that reports success without verifying is worse than none, because it removes the urgency to check manually.
5
Stay killed until a human re-enables it
No automatic recovery. Whatever caused the kill has not been diagnosed yet.
Layer 5 — Reconciliation
Once a day, and ideally continuously, compare three things: what your system thinks the position is, what the broker says the position is, and what the sum of your fills implies the position should be. All three must agree.
Divergence
Likely cause
System long, broker flat
An order you believed filled was rejected or cancelled
System flat, broker long
A fill arrived that your system did not process — often after a websocket reconnect
Quantities differ by a partial amount
Partial fill handling; the classic source of unbalanced multi-leg positions
Fills sum correctly but position differs
Corporate action, expiry settlement, or a manual intervention nobody logged
What each kind of divergence usually means
The rule is unconditional: when reconciliation fails, trading stops until it is explained. A system that trades on top of an unexplained position mismatch is compounding an error it does not understand.
Position sizing
Trend chart
Compounding drawdown from repeated 2% losses
A modest risk fraction still produces a material decline across an ordinary double-digit losing streak.
Starting from a capital index of 100 and risking 2 percent of current capital on each loss, capital is 90.4 after five consecutive losses, 81.7 after ten, and 73.9 after fifteen. The account survives, but needs a 35.3 percent gain from 73.9 to return to 100.
Illustrative compounding with no wins, costs or slippage. Position sizing should be tested against the longest plausible streak, not the average trade.
Sizing is not really a separate layer — it is what makes the loss limits reachable rather than theoretical. The relevant question is not "how much can I make on this trade" but "how many consecutive losses does this size survive".
A strategy with a 55% win rate will, over a few thousand trades, produce a losing streak in the double digits. Not as a tail scenario — as an expected occurrence. If the size does not survive fifteen consecutive losses, the strategy will end the account before its edge has a chance to express itself.
≥ 10
Consecutive losses
Expected at a 55% win rate over a few thousand trades
1 lot
Correct starting size
Regardless of account size, for the first month live
0
Acceptable unexplained mismatches
Stop and investigate every time
The order these should be built in
Position limits — before the first live order.
Kill switch, external, tested — before the first live order.
Reconciliation, run daily — from day one.
Loss limits that flatten — before you scale past one lot.
Order rate limits — before any strategy that fires bursts.
The first three are not optional and are not much work. What makes them feel optional is that nothing bad has happened yet, which is also true of every system the day before something bad happens.
Frequently asked questions
Why does a kill switch need to run in a separate process?
Because the conditions that require a kill — a deadlock, a tight loop, an unresponsive process — are the same conditions that prevent the strategy from checking a flag. A kill switch with its own process, credentials and broker connection can cancel orders and flatten positions without any cooperation from the strategy it is stopping.
Should a daily loss limit stop new trades or close open positions?
Close positions. A limit that only blocks new entries does nothing when the open position is the thing losing money, which is the main scenario the limit exists for. It also has to be computed on realised plus unrealised P&L, since a realised-only calculation will not fire while a position is deep underwater and still open.
How do I decide position size for an automated strategy?
By asking how many consecutive losses the size survives, not by expected profit. A strategy with a 55% win rate will produce losing streaks in the double digits over a few thousand trades as a matter of course. If the size cannot absorb roughly fifteen consecutive losses, the account will not last long enough for the edge to express itself.
What should happen when position reconciliation fails?
Trading stops until the divergence is explained. A mismatch between your system's position, the broker's reported position and the sum of your fills means one of them is wrong, and trading on top of an unexplained mismatch compounds an error you do not understand yet. The most common causes are unprocessed fills after a websocket reconnect and mishandled partial fills.
Do I need order rate limits if I am below SEBI's 10 orders per second threshold?
Yes, for two reasons independent of the regulation. Your own limiter is a circuit breaker against logic bugs — a runaway loop sends fifty orders instead of ten thousand. And enforcing the cap yourself is cleaner than handling the broker's throttle rejection as a failure case. Count modifications and cancellations too, since the exchange counts order messages rather than trades.
QK
QuaTick Risk Desk
Risk & compliance
Position sizing, margin mechanics, kill switches and the operational controls that decide whether an automated book survives a bad week.