What happens between an alpha saying "sell 60" and an order reaching a venue? In Kestrel, every order is normalised, risk-checked in a fixed order, and only then sent.
Intro
Between Intent and Exchange
An alpha decides it wants to sell sixty units. Somewhere between that decision and a venue, a dozen things can go wrong. The quantity may not fit a lot. The price may sit off the tick grid. A typo could be ten percent away from the market. Today we follow one order through that gauntlet.
- Alpha intent
- A dozen ways to go wrong
- Normalise, check, then send
Where We Are in the Series
Last time, in episode seven, we saw the alpha framework, where alphas express their wishes as target positions behind gates. Those wishes become orders, and this episode is about the component that receives them: the order manager and its risk manager. Positions and profit and loss come next, in episode nine, so we will leave them alone.
- Ep 7: alphas and targets
- Ep 8: OMS and risk
- Ep 9: positions, later
The Three Verbs
The title is the whole design. Normalise the order into something the market can accept. Check it against a fixed list of risk rules. Then, and only then, send it to the venue. Every order follows the same path in a backtest, in paper trading and in live trading, because the gateway owns this logic and only the venue underneath differs.
- Normalise
- Check
- Send
Normalising the Order
Reject the Unknown First
Inside OrderManager submit, the first questions are about the instrument. If the instrument is unknown, or it has already expired, the order is rejected immediately. There is nothing sensible to round or price for a contract that does not exist, so we stop before doing any work.
- Unknown instrument: reject
- Expired instrument: reject
Quantity: Round Down to Lot
Next comes quantity. Derivatives trade in lots, so the requested quantity goes through roundQtyDown to a whole number of lots. Suppose the lot is twenty five and the alpha asks for seventy. That becomes fifty. Rounding down means we never trade more than was requested. If the result falls below the minimum quantity, the order is rejected.
- roundQtyDown to lot
- 70 with lot 25 gives 50
- Below minQuantity: reject
Prices Never Get Worse
Prices follow one principle: rounding must never give a worse price than the one asked for. A buy limit rounds down. A sell limit rounds up. Take a tick of zero point zero five. A buy limit at one hundred point zero three becomes one hundred point zero zero. A sell limit at the same price becomes one hundred point zero five.
- Buy limit: round DOWN
- Sell limit: round UP
- Never worse than asked
Stops Round the Other Way
Stop orders are the mirror image, because a stop is a trigger, not a limit. A buy stop rounds up, and a sell stop rounds down. Think of it as the stop firing on the safe side of what was asked. At the same tick, a buy stop at one hundred point zero three becomes one hundred point zero five.
- Buy stop: round UP
- Sell stop: round DOWN
- Market: prices cleared
Market Orders and Tick Policy
A market order has no price of its own, so any price fields are cleared. There is also an optional tick size policy called reject. When enabled, instead of rounding an off-tick price, the manager refuses it outright. That suits setups where an off-grid price is more likely a bug than a harmless imprecision.
- Market: clear prices
- tickSize policy 'reject'
- Off-tick price refused
The Normalisation Funnel
Put it together and you have a funnel. A raw request enters wide. Instrument checks, lot rounding, price rounding and the optional tick policy narrow it. What comes out is an order whose quantity and price the market could actually accept, or an early rejection with a reason.
- Raw request in
- Four narrowing steps
- Clean order or reason out
The Risk Input
Building RiskCheckInput
Now the order must be judged. The manager gathers a RiskCheckInput: the last price, the strategy's own position, the net position, same-side pending quantity, and gross exposure. Optionally it adds pending values and margin. Risk then judges a snapshot, rather than reaching into the order manager itself.
- Last price
- Strategy and net position
- Same-side pending, gross exposure
- Optional pending values, margin
Net of Working Reducing Orders
The subtle field is strategyPosition. It is not the raw position. It is the position net of the strategy's own working reducing orders. If the strategy holds a positive amount, we subtract the pending reducing quantity, floored at zero. If it holds a negative amount, we add it, capped at zero.
- Held minus pending reducing
- Floored at zero when long
- Mirrored when short
The Code: Netting
Here is the line, shown as in the source. Held is the position. If held is positive, strategyPosition is the larger of zero and held minus pendingReducing. Otherwise it is the smaller of zero and held plus pendingReducing. The clamp to zero is what stops this number from ever crossing sign.
- Clamp prevents sign flip
Why Net It?
Why bother? Because stacked reducing orders can otherwise add up to more than the position. Each one looks harmless alone. Together they could flip a long position into a short one. By counting earlier working exits against the position, each new order sees only what is still left to reduce. We will run exact numbers shortly.
- Each exit looks harmless alone
- Together they can flip the sign
- Netting shows what is left
The Ordered Risk Checks
First Failure Wins
RiskManager check runs a fixed list of rules in a fixed order, and returns the first failure. That makes rejection reasons deterministic. Two different bad orders always produce the same message, run after run, which matters when you compare backtests and read logs.
- Fixed order
- First failure returned
- Deterministic reasons
Gate One to Four
The first gates are these. Is the instrument known and unexpired? Is the kill switch engaged? That one applies only to non-reducing orders. Is the strategy locked by mark to market? Then a reference price is needed: the limit or stop price, or else the last price. With none available, the reason is no market data.
- Instrument known, unexpired
- Kill switch (non-reducing only)
- MTM-locked strategy
- Reference price or 'no market data'
The Price Collar
Next is the price collar. For limit and stop orders, the absolute value of reference over last, minus one, must be at most the collar percentage. The default is ten percent. It is a fat-finger guard: a stray zero or a wrong digit should not reach the exchange.
- |ref / last - 1| <= priceCollarPct
- Default 0.10
- Limit and stop orders
Worked Example: The Collar
Take a last price of two thousand and a limit order at two thousand two hundred fifty. Divide: one point one two five. Subtract one: zero point one two five. That is twelve and a half percent, greater than ten. The order is rejected by the collar, before any cap is even looked at.
- Last 2000, limit 2250
- 2250 / 2000 - 1 = 0.125
- 12.5% > 10%: reject
Bands, Bans and Notional
After the collar come the price band and the F and O ban check. Then the order notional: quantity times reference price times multiplier must not exceed maxOrderNotional, which applies to non-reducing orders only. The default is two million rupees. One order cannot be enormous.
- Price band, F&O ban
- qty x ref x mult <= maxOrderNotional
- Default Rs 20,00,000
When Net Exposure Grows
If the order would grow net exposure, three more caps apply: maxPositionNotional, five million by default, maxPositionQuantity, and maxGrossExposure, twenty million by default. An order that only shrinks exposure skips these, since it makes the book safer rather than riskier.
- maxPositionNotional: 5,000,000
- maxPositionQuantity
- maxGrossExposure: 20,000,000
The Final Caps
Last come the optional caps: open quantity, day value, a SPAN-like margin, the order to trade ratio, and orders per second. The rate window is the half open interval from now minus one second up to and including now. The default is fifty orders per second.
- Open qty, day value, margin
- Order-to-trade ratio
- Orders/sec in (now-1s, now]
- Default 50
Event Time, Not Wall Time
Notice that the rate limit uses the timestamp of the event, not the wall clock. All times in risk are event time. A backtest that replays a day in seconds sees the same one second windows as a live session would, so the same input history always yields the same decisions.
- All risk times are event time
- Replay speed is irrelevant
- Same input, same decisions
Reducing Orders
What Counts as Reducing
An order is reducing when the position has the opposite sign to the order, and the order's absolute quantity is at most the absolute strategy position. In plain terms, it only closes some or all of what you hold, and does not push through zero into the other side.
- Position sign opposite to order
- |q| <= |strategyPosition|
- Closes, never flips
The Bypass
Reducing orders bypass three things: the kill switch, the notional cap and the rate limit. The reason is simple. When something has gone wrong, the most important thing is to get out. An exit stranded behind a safety rule would turn a safeguard into the problem.
- Bypass: kill switch
- Bypass: notional cap
- Bypass: rate limit
Still Needs Market Data and Collar
But a reducing order still needs market data and still faces the collar. As the design puts it, a reducing limit far from the market is still a fat-finger. Selling a long position at a price off by a factor of ten is closing, yes, but it is still a mistake worth catching.
- Still needs a reference price
- Still faces the collar
- Far reducing limit is a fat-finger
Worked Example: Stacked Sells
Now the example. The strategy is long one hundred, with the kill switch on. A first sell of sixty arrives. Pending reducing is zero, so strategyPosition is one hundred. Sixty is at most one hundred, so it is reducing, and it passes. The second sell of sixty then arrives.
- Long 100, kill switch on
- Sell 60 #1: sees 100, reducing, passes
- Sell 60 #2 arrives
Second Sell Is Blocked
Now pending reducing is sixty, so strategyPosition becomes forty. The order's absolute quantity, sixty, is greater than forty. It is not reducing, so the kill switch applies and the order is blocked. Without the netting, both sells would pass and the position would end at short twenty.
- Pending 60: strategyPosition 40
- |60| > 40: not reducing
- Blocked by kill switch
The Kill Switch
Daily Loss and onEquity
What engages the kill switch? The daily loss rule. On each equity update, onEquity checks whether the day has rolled over. On each new UTC day it resets dayStart to the current equity. Then if equity minus dayStart falls below minus maxDailyLoss, the switch engages.
- New UTC day: reset dayStart
- equity - dayStart < -maxDailyLoss
- Default maxDailyLoss 100,000
Only the First Transition
Here is a detail. onEquity returns true only on the false to true transition. Say dayStart is one million and equity drops to eight hundred ninety nine thousand. The loss is a hundred and one thousand, past the limit, so it returns true once. The core then cancels everything. Later updates return false.
- Returns true once
- Core cancels all working orders
- Later updates return false
The Order Lifecycle
Accepting the Order
If every check passes, the order is accepted. Here is the sequence. It gets an id from nextId. Its status becomes PendingNew. If it is not reducing, the order is recorded for the rate limit. Risk is told about the acceptance. The book adds the order as open, and the venue receives it.
- id = nextId_++
- Status PendingNew
- recordOrder if not reducing
- addOpen, then venue submit
Rejected Orders Stay Quiet
A rejected order is treated differently. It still receives an id and is stored in the closed list, so you can inspect it later. But it is not reported through the listeners. The alpha learns the outcome from the SubmitResult it gets back, not from an event stream.
- Rejected gets an id
- Stored in closed list
- Not sent to listeners
- Alpha reads SubmitResult
The State Machine
Once accepted, an order lives in the OrderBook as a forward only state machine. PendingNew is rank zero. New is rank one. PartiallyFilled is rank two. Filled, Cancelled and Rejected are rank three, and terminal. Status can only move to a higher rank, never back.
- PendingNew 0, New 1
- PartiallyFilled 2
- Filled, Cancelled, Rejected 3
- Forward only
Ignoring Stale Updates
Updates can arrive late or out of order. So onOrderUpdate returns early if the order is already terminal, or if the update's status rank is lower than the order's current rank. The filled quantity also never decreases. A late New arriving after Filled simply bounces off.
- isTerminal: ignore
- Lower rank: ignore
- filledQuantity never decreases
Closed List and Updates Only
When an order reaches a terminal state it moves from the open set to the closed list, which is capped at two thousand entries. And note the rule: status, filled quantity and average price change only from an OrderUpdate, never from a Fill. Fills are a separate stream.
- Terminal moves to closed list
- Capped at 2000
- Only OrderUpdate changes status
Pitfalls and Recap
Pitfall: Fills Changing Status
The first pitfall is letting fills change order status. Fills and updates can arrive in either order. If a fill promoted the status, a later update could contradict it, and the book would depend on arrival order. One source of truth, the OrderUpdate, keeps the machine consistent.
- Two streams, either order
- One source of truth
- Avoids arrival-order bugs
Pitfall: Wall Time in Risk
The second pitfall is wall time in risk. If the rate limit or day rollover used the system clock, a fast replay would trip limits that real time never would, and results would differ between runs. Using event time keeps every backtest reproducible, and the rate window matches what the data says.
- Wall clock breaks replays
- Event time is reproducible
- Same history, same rejections
Recap and Next Time
To recap: normalise so prices are never worse than asked, check in a fixed order and return the first failure, then send. Reducing orders bypass the kill switch, notional cap and rate limit, but not the collar. Status moves forward only. Next, episode nine: how fills turn into positions, cash and profit and loss.
- Normalise, check, send
- Reducing bypass, collar stays
- Forward-only status
- Next: positions and P&L