Four gates before the broker
The pre-trade controls for āagman: a kill switch, a daily loss cap, circuit breakers and a margin check, decided by a policy engine from inputs the model can’t touch, plus the readiness gate, the plan-bound approval and the failure matrix.
Contents
The public āagman site shows an order being blocked: a Bank Nifty futures order stopped because the user’s daily loss cap had been reached, with the note “Nothing reached the broker.” Below it, a RELIANCE order passes “all 4 checks” and is sent.
Those four checks are the subject of this part: kill switch, daily loss cap, circuit breakers and margin. My earlier post covered why a policy engine sits between the model and the broker. This part covers:
- The readiness gate: what the backend enforces before a strategy can go live
- Why a strategy-level check is not an order-level check
- The four gates, and how a policy engine decides from trusted inputs
- The approval step, bound to the exact plan
- A failure matrix, with fail-closed as the rule
The readiness gate and the risk tools#
The backend (the Mastra orchestrator) keeps a strategy in DRAFT until it can be marked READY. A strategy becomes ready only if all five of these hold:
- JSON Schema validation passes.
- Zod and cross-field validation pass.
- Every assumption has
needs_confirmationset to false, meaning the user confirmed it. - The list of clarifications still needed is empty.
- A policy client allows the action.
If any check fails, the strategy stays in DRAFT with structured blockers. The policy client is an interface in front of the same policy engine the execution service uses, so the readiness check and the order-time check read one set of rules. The backend also persists an audit trail in Postgres: chat transcripts, strategy versions (append-only), confirmations and policy decisions, with a seven-year retention goal and correlation ids.
The risk worker agent has real calculation tools: position risk, capital exposure validation, maximum drawdown, options and futures constraints, and a comprehensive analysis. They read platform defaults from one file, for example: no single position over 20% of capital, total exposure capped at 80%, risk per trade capped at 2% of capital, default stop-losses of 1% for equity and 2% for futures and options, a daily loss limit of 5% of capital and a weekly one of 10%, and naked short options blocked by default. The strategy document itself has account_limits for a maximum number of trades per day and a maximum daily loss.
These are pre-trade checks on a strategy. They aren’t checks on an individual order, and that difference is why the execution service exists. A strategy can be perfectly sound while an order is wrong: a stale price, an exhausted margin balance, a day that has already lost its cap, a broker outage. Those are facts about now, so they are checked at the moment of each order.
An order is an intent until it passes#
Nothing produces a broker order directly. A live strategy evaluator, a user’s one-off “buy 50 RELIANCE,” and an exit all produce the same thing: an order intent, a typed record of what should be sent and why. An intent becomes an order only after it passes the gates, which run in an execution service no agent can call, and the final decision comes from a policy engine (Open Policy Agent) running as a sidecar.
One field matters most for the gates: whether the intent reduces a position. An order that closes or shrinks a position is treated differently from one that opens or grows one. Blocking a stop-loss exit because the loss cap was hit would make the system more dangerous, not less.
The gates run in a fixed order, cheapest and most absolute first:
graph LR
I["Order intent"] --> K{"1. Kill switch"}
K -->|tripped| X["Blocked\n+ reason"]
K --> L{"2. Daily\nloss cap"}
L -->|breached, opens risk| X
L --> B{"3. Circuit\nbreakers"}
B -->|tripped| X
B --> M{"4. Margin"}
M -->|insufficient| X
M --> O{"Policy engine\nfinal decision"}
O -->|deny or error| X
O -->|allow| S["Send to the\nexecutor"]
class K,L,B,M amber
class O violet
class X red
class S greenGate 1: the kill switch#
A flag that blocks every risk-increasing order, at three scopes: one strategy, one user (the “Stop all trading” button, and the loss cap), and the whole platform (for example if a broker reports an incident). A runaway algo (for instance more than ten orders a second) or a breach of the user’s maximum daily loss also trips it. The exchange framework gives the reason: below ten orders per second a retail algo can use a generic identifier, and above it registration is required, so rate is a number you must watch.
Gate 2: the daily loss cap#
A rupee amount the user sets. When the day’s loss reaches it, new risk-increasing orders are blocked and the user’s kill switch trips. The hard part is computing “today’s loss”: realized profit and loss from closed trades, plus unrealized profit and loss on open positions, minus estimated charges. Three decisions follow from that: positions come from the broker, not from our own records (a user can trade by hand in the broker’s app); prices come with a staleness bound, and a stale price is unknown, which blocks; and the check also runs on a timer, because an open position can breach the cap with no new order.
Gate 3: circuit breakers#
Checks that catch orders that are structurally valid but likely wrong: a price outside the exchange’s daily band, a notional above a per-order cap (the backstop for a model that writes a quantity of 100 when the user said 10), an order rate above the limit, a streak of broker rejections, an unprotected market order, and an order outside market hours. Counting is stateful, so it isn’t the policy engine’s job; a counter feeds the engine its result.
Gate 4: margin#
The only gate that calls an external API, so it runs last. Single orders use the broker’s order-margin call; multi-leg options use the basket-margin call, because a hedged structure needs far less margin than its legs separately. A failed or timed-out margin call blocks the order. It also prevents a half-placed multi-leg order that a late insufficient-funds rejection would create.
The policy engine: one decision, trusted inputs#
All four gates feed one policy query. The execution service assembles the input from trusted sources only: the intent from the compiled plan, the approval from the database, user flags and counters from the risk service, market fields from the market-data service, margin from the broker. None of those fields comes from the model.
The policy bundle holds an exposure check against the user’s net worth, a check that the user’s segment is activated, a ban-list check and the four gates. The following sketch (illustrative, not the production policy) shows how deny rules and a default of deny fit together:
package aagman.pretrade
default allow := false
allow if { count(deny) == 0 }
deny contains "Kill switch is on" if {
input.user.kill_switch
not input.intent.reduces_position
}
deny contains "Daily loss cap reached" if {
input.risk.day_pnl <= -input.risk.loss_cap
not input.intent.reduces_position
}
deny contains "Approval does not match this plan" if {
input.approval.sir_hash != input.intent.sir_hash
}Treat every failure mode as a deny: the policy engine unreachable, a timeout, an undefined result, a malformed response. Deny reasons go straight to the user, so “Daily loss cap reached” replaces “order rejected,” and because the reasons come from the policy, they’re always the real reason.
The approval step#
A live strategy runs only under an approval: a row that binds the sir_hash of the compiled plan the user backtested and paper traded, the risk limits for the strategy, the broker account and a validity window. The hash comes from the same canonical SHA-256 of the strategy document that Part 2 describes, so if the user changes a stop from 2% to 2.5%, the hash changes and the approval no longer applies. The readiness gate above is the first half of this, and the binding to each live order is the second: the policy engine compares the approval’s hash with the intent’s hash on every order.
Algo identifiers and the audit trail#
The framework requires each algo order to carry an identifier, and the execution service stamps one on placement, modification and cancellation, with a secondary rule that denies an order missing it. Every decision is logged with its full input and the policy bundle version, append-only, so for any order the chain from request to broker response is one query away. The backend’s append-only audit tables are the foundation, and the decision logs land in ClickHouse.
The failure matrix#
| Failure | Risk-increasing orders | Risk-reducing orders |
|---|---|---|
| Policy engine unavailable | Blocked | Blocked, with an immediate alert |
| Market data stale | Blocked | Allowed as protected or limit orders |
| Broker margin call down | Blocked | Allowed (closing needs no new margin) |
| Executor offline | Blocked, since it can’t send | Can’t send; the user is told to act in the broker’s app |
| Counter store down | Blocked | Blocked, with an alert |
The rows that block risk-reducing orders are the uncomfortable ones. When the system can’t evaluate policy at all, it stops and tells the user at once instead of guessing, and protective stops stay at the broker wherever the broker supports them.
What I’d do differently#
Build the order-time layer in the same sprint as the strategy-time one. The readiness gate and the risk tools came first, and they were the easy half: everything that evaluates a strategy is a pure function, while everything that evaluates an order depends on live state. Next time the failure matrix comes first.
Policy tests as golden tests. We hold policy to the same standard as the backtester (Part 4), and OPA has a good unit-test framework. I’d hold policy to the same standard as the backtester (Part 4): a fixed set of inputs with expected decisions and reasons, and any change that alters a decision needs an explicit update. I’d write them before the policies.
Conclusion#
The gates are where āagman’s promise to users and to brokers is kept: the model can propose, but only an approved plan, inside the user’s limits, with fresh prices and confirmed margin, produces an order. Part 8 covers the last mile: how the order leaves from the user’s own machine.
I built āagman as Founding Software Engineer at KotiLabs, July 2025 to April 2026.
Responses
Select any passage to highlight it or comment on that line. Highlights stay private, in this browser.