What happens to your orders when a trading process dies? Kestrel's answer: the order service owns the truth, and the strategy keeps only a shadow.
Intro
What If The Trader Dies?
Imagine your strategy process is holding fifty shares of RELIANCE, with a limit order resting in the market. At two minutes past eleven, the process crashes. Who owns that position now? Who cancels that order? And when you restart, how does the strategy learn what it already holds?
- 50 RELIANCE held
- Working limit order
- Process crashes at 11:02
Recap and Today's Plan
In the previous episode we made a single order placement safe: uncertain outcomes, tags and a write-ahead ledger. Today we zoom out to the service that owns all of that. We will cover sessions, the order protocol, idempotent submits, the trader's gateway, resync, and the two crash scenarios.
- Episode 23: safe placement
- Today: the order service
- Sessions, resync, crashes
The Problem
Where The Truth Lives
The core design rule is simple: the authoritative book lives in kestrel_orders, not in the strategy. The trader process runs alphas and keeps only a shadow book, a local copy it uses to make decisions. It contains no broker code at all. If the trader is the only place a position exists, a crash erases it.
- kestrel_orders: authoritative
- kestrel_trader: shadow only
- No broker code in trader
Pitfall: Positions Only In The Trader
The first pitfall is exactly that: keeping positions only inside the strategy process. After a crash, the restarted process believes it is flat while the account is not. It may buy again, doubling exposure, or never square off. Putting the truth in a separate service removes that whole class of bug.
- Crash erases local state
- Restart believes it is flat
- Truth must outlive the strategy
The Protocol
Same Frames, A Small Vocabulary
The order link uses the same four-byte length-prefixed JSON frames as every local link in Kestrel. Each message carries a field called t, its type. The runner side speaks hello, new, cancel, cancelAll, flatten, killSwitch, and bye. The service side answers with welcome, refused, ack, and events.
- Same framing as other links
- Runner: hello, new, cancel, bye
- Service: welcome, refused, ack
The Hello Message
A session begins with hello. It carries the strategy identifier, a run identifier, the process id, the mode, the account, the costs, the risk limits, the alphas, the instruments, the capital, and an archive directory. In other words, the runner describes itself completely, and the service decides whether to accept it. Account and token values are placeholders, never shown.
- strategyId, runId, pid, mode
- costs, risk, alphas, instruments
- capital, archive dir
Welcome Carries Full State
If the service accepts, it replies with welcome, and welcome carries the full state: positions, cash, open orders, today's fills, the kill switch and the carry. The runner does not have to ask for anything else. That single message is the foundation of recovery, which we will return to shortly.
- welcome = full state
- Positions, cash, open orders
- Fills today, kill switch, carry
Refusals And Events
The service can also say no, with a refused message and a code. The codes include duplicate session, auth, live disabled, account, login, and instrument conflict. After that, the stream is acknowledgements plus events: order, fill, risk, blocked and settled. Every refusal is explicit, so a runner never has to guess why it was turned away.
- refused{code}
- duplicate_session, auth, live_disabled
- account, login, instrument_conflict
The Full Sequence
Put it together. The trader sends hello, receives welcome, then sends new with a client order id. The service answers with ack, and later pushes an order event and a fill event as the venue acts. Notice that submit is a request and response, while fills arrive asynchronously as events.
- hello, welcome
- new, ack
- order and fill events
Sessions
One Session Per Strategy
The service keeps exactly one session per strategy name. If a second live runner says hello with the same name while the first is alive, it gets refused with duplicate session. This prevents two copies of one strategy from trading the same book at once, a classic accident after a hurried restart.
- One session per strategy name
- Second live runner refused
- Prevents double trading
Takeover And Config Digest
There is a deliberate exception. A hello with a new run identifier takes over the session, because the old process is evidently gone or replaced. And if the config digest has changed, the service does not trust old in-memory state. It rebuilds the book from the journal, so the new configuration starts from recorded facts.
- New runId takes over
- Changed digest: rebuild from journal
- Facts over assumptions
Idempotent Submit
Submit Is A Synchronous RPC
Inside the service, Session submit is a synchronous call answered from memory. It does not wait on the broker to build its reply. First it checks whether this client order id has been seen. Then it applies gates, and finally it hands the order to ExecutionCore submit, where risk is checked.
- Answered from memory
- Check clOrdId first
- Gates, then ExecutionCore risk
Idempotency With Numbers
Here is the idempotent part. Suppose the runner sends order r7-12 to buy ten shares, and the ack is lost in transit. The runner retries r7-12. The service finds it in its table and returns the stored ack, marked duplicate true. Only one order exists. Ten shares, not twenty.
- Send r7-12: buy 10
- Retry same clOrdId
- Stored ack, duplicate: true
Pitfall: Non-Idempotent Retries
The second pitfall is retrying without a stable identifier. If every retry mints a fresh id, a lost reply turns into a second order. Retries are only safe when the receiver can recognise them. So the client order id must be assigned before sending, and reused on every retry.
- Fresh id per retry: double order
- Stable id: safe retry
- Assign id before sending
Paper Fills Without Transport
In paper mode, orders that pass risk go to a PaperVenue. That is the same SimulatedVenue from the backtester, fed by the service's own market data link. There is no transport hop to a broker at all. So paper trading exercises the same fill rules as research, with live bars.
- PaperVenue = SimulatedVenue
- Fed by service's own md link
- No broker transport
The Trader Side
RemoteOrderGateway And ShadowBook
On the trader side, the engine talks to an order gateway interface, as in every mode. In paper and live, that gateway is RemoteOrderGateway. It keeps a local book, positions and risk, and exposes them read-only as a ShadowBook. Alphas can read it, but cannot change it, only the service's events do.
- Same IOrderGateway interface
- Local book exposed read-only
- ShadowBook updated by events
Client Order Ids
Each client order id is the run identifier, a dash, and a local counter. So run r7, order twelve, becomes r7 dash twelve. The run identifier keeps ids unique across restarts, and the counter keeps them unique within a run. That is what makes the idempotent lookup safe.
- clOrdId = runId + '-' + id
- Unique across restarts
- Unique within a run
The Timeout Path In Code
Now the critical code, lightly simplified. The gateway calls roundTrip and waits up to the submit timeout, one thousand milliseconds by default. If it is not done, three things happen at once: the gateway sets degraded, remembers this order as timed out, and sends a cancel for that same client order id.
- roundTrip waits 1000 ms
- degraded = true
- Defensive cancel sent
Reject Locally, Log, Return
Then it logs a warning, builds a local order marked Rejected with the reason order service timeout, and returns a failed SubmitResult. The strategy sees a clean rejection. It never has to wonder whether the order might exist, because the cancel is already on its way.
- Warn log with order id
- Local status Rejected
- Reason: order service timeout
Fail Closed
Why this shape? The timeout is ambiguous: the order may or may not have reached the exchange. Failing closed means we assume the worst, cancel defensively, and stop trading. While degraded, every further submit is rejected until a late ack proves the link is healthy again.
- Timeout is ambiguous
- Cancel defensively
- Reject until late ack arrives
Resync
Resync On Every Welcome
Every time a welcome arrives, the trader resyncs. That means the first connection, a reconnect after a blip, and a restart all take the same code path. One path is easier to test than three. And it is the path that rebuilds the shadow from the service's truth.
- Every welcome triggers resync
- First connect = reconnect = restart
- One path to test
What Resync Does
Positions and cash are reset from the service, overwriting local guesses. Open orders are reconciled: if an order is gone from the service, its result is applied from closed orders, or it is marked Cancelled. The kill switch and carry are copied. And if the clock differs by more than one second, it warns.
- Positions and cash reset
- Gone orders: closedOrders or Cancelled
- Kill switch and carry copied
- Clock skew over 1 s warned
seenFills Prevents Double Counting
Fills need special care. The welcome includes all of today's fills, but some may already be applied locally. So each fill is applied only if its fill id is not in the seenFills set. Suppose fill f3 buys twenty shares and is already seen. Replaying it would turn a position of fifty into seventy. The set prevents that.
- fillsToday in welcome
- Apply only if fillId unseen
- f3 replay would make 50 into 70
Crashes
When The Trader Dies
Now the crashes. If the trader dies without sending bye, the service notices the disconnect. It waits a grace period, ten seconds by default, controlled by the disconnect cancel open after seconds setting. After that, the strategy's working orders are cancelled.
- No bye received
- Grace period: 10 s
- Then working orders cancelled
The Service Keeps Working
Crucially, the service does not stop acting for the dead trader. Square-off still runs, settlement still runs, and end-of-day files are still written. The run archive is finished with a crashed status, so the record shows honestly what happened. The position is managed even though no strategy is alive.
- Square-off continues
- Settlement and EOD continue
- Archive finished as crashed
Worked Example
11:02, Fifty RELIANCE
Let us run our example. The trader holds fifty RELIANCE and has a working limit order. At eleven oh two it crashes. The service sees the broken link and starts its ten second timer. Nothing else changes yet: the position of fifty is safely recorded in the service's book.
- 11:02 crash
- Position 50 in service book
- Limit order still working
11:02:10, The Cancel
At eleven oh two and ten seconds the timer expires. The service cancels the working limit order, so nothing sits in the market with no one watching. The position of fifty remains, to be handled by square-off at the normal time, or by the restarted strategy.
- 11:02:10 cancel working limit
- Position 50 remains
- No orphan order in the market
11:05, Restart And Welcome
At eleven oh five the trader restarts and sends hello with a new run id, so it takes over the session. The welcome carries a position of fifty and today's fills. The fresh shadow book has an empty seenFills set, so it applies each fill once and lands on fifty. It never reaches one hundred.
- 11:05 hello with new runId
- Welcome: position 50 + fills
- Shadow lands on 50
Service Restart
When The Service Restarts
The other direction: the service itself restarts. For each strategy that has fills in today's journal, or orders in the ledger, it rebuilds from the session file, with the token stripped, plus the journal. Hellos from runners are deferred until that restore finishes, so no one sees half-built state.
- Rebuild from .session.json
- Token stripped
- Journal replay
- Hellos deferred until done
Journal Replay In Motion
The journal is written with an fsync per fill, so a fill that was acknowledged is on disk. Replay applies fills in order: buy fifty at one price, say, and the position becomes fifty with that average cost. Same code, same inputs, same book. That determinism is the payoff of recording facts rather than state.
- fsync per fill
- Replay fills in order
- Same inputs, same book
Files And Status
Files Per Strategy
Each strategy gets a small set of files. End-of-day results go in a folder per alpha, one csv per date. The journal is a jsonl file per date. Then there is dot session dot json for session state, and a live jsonl file per date. Together they let you audit or rebuild any day.
- EOD alpha/date.csv
- journal/date.jsonl
- session.json
- live/date.jsonl
The Status API
For operators, the service exposes a status API on loopback port seven one zero two. GET endpoints list sessions and positions. POST endpoints trigger the kill switch and flatten. Because it is bound to loopback, only local processes can reach it, a topic from episode eighteen.
- 127.0.0.1:7102
- GET sessions, positions
- POST killswitch, flatten
Recap
Design Rules To Keep
Recap the rules. Truth lives in the service; the trader holds a shadow. Submits are idempotent by client order id. A timeout fails closed, with a defensive cancel and degraded mode. Every welcome triggers the same resync. And fills are journalled before they are trusted.
- Service owns truth
- Idempotent clOrdId
- Fail closed on timeout
- Resync on every welcome
Honest Caveats
A fair caveat: these live order paths are heavily guarded, but they are presented here as design, not as proven in production. The examples use illustrative numbers, and nothing here is financial advice. Test any such system thoroughly with paper trading before trusting it.
- Design, not production proof
- Illustrative numbers
- Not financial advice
Next: Ten Layers
In the next episode we look at the ten layers of safety that stand between a strategy and a real order.
- Next: ten safety layers
- Subscribe for episode 25