Trading bot state persistence: restart without trading blind
A process restart should be boring. If a bot forgets an accepted order, an unprocessed fill, or the relationship between a position and its protective exit, a harmless crash can become duplicate exposure. State persistence is the evidence trail that lets the bot stop, compare local intent with venue reality, and recover safely.
Persist evidence, not every temporary variable
Market caches, rolling indicators, and UI state can usually be reconstructed from source data. Money-moving state cannot. If a record answers “what did the bot intend, what did the venue accept, and what exposure exists?”, it belongs on durable storage before the process acknowledges success.
| Record | Why retain it | Key fields |
|---|---|---|
| Trade intent | Explains why an order exists | strategy version, signal ID, symbol, side, target size |
| Order | Connects retries and venue state | client ID, venue ID, type, limit, status, timestamps |
| Fill | Builds actual exposure and cost | fill ID, order ID, quantity, price, fee, event time |
| Position lot | Tracks basis and exit coverage | quantity, average price, realized P&L, protection IDs |
| Risk state | Preserves circuit breakers | daily loss, drawdown peak, cooldown, halt reason |
| Checkpoint | Bounds recovery work | channel, last sequence, last successful reconciliation |
Use the venue's stable identifiers and enforce uniqueness locally. A fill ID should not post twice; a client order ID should map to one intent; an order transition should be append-only or auditable. Keep raw venue payloads where practical so normalization bugs can be investigated later through trading-bot logs.
The venue and local database know different truths
The exchange or broker is authoritative for what it accepted and filled. The local system is authoritative for why it acted, which strategy owned an order, and which event it has processed. Neither view is sufficient alone:
- An order can exist at the venue even if the acknowledgement never reached the database.
- A durable intent can exist locally even if the network request never reached the venue.
- A private-stream fill can be missed during disconnection while venue position state still changes.
- A manual trade can alter the account without a local intent.
Reconciliation joins the two views. It matches client IDs and venue IDs, imports previously unseen fills, compares open-order sets, calculates exposure from authoritative positions or balances, and flags anything it cannot explain. The order state machine in partial fills and reconciliation handles normal live transitions; persistence makes those transitions survive a restart.
An empty local positions table after a crash does not mean the account is flat. Until the venue responds and discrepancies are resolved, block new entries and assume exposure may exist.
Record intent before sending the order
The dangerous boundary is between a local write and a remote side effect. A practical order path is:
- create a deterministic or unique client order ID;
- validate the signal through risk and precision gates;
- write the intent and a pending-submit record in one local transaction;
- send the order with that client ID;
- store the acknowledgement and venue order ID;
- consume fills idempotently and update exposure;
- mark the intent complete only when its lifecycle is resolved.
minimal durable order record{
"intent_id": "meanrev-BTCUSD-20260718T101500Z",
"client_order_id": "mr-7f3c...",
"status": "SUBMITTING",
"symbol": "BTC/USD",
"side": "buy",
"quantity": "0.0025",
"strategy_version": "meanrev-12",
"created_at": "2026-07-18T10:15:00.412Z"
}
If the process dies after step three, recovery can decide whether to submit. If it dies after step four, the stable client ID lets recovery query before acting. That is the core of duplicate-order prevention.
A small single-bot deployment can use an embedded transactional database with backups and integrity checks. A plain JSON file rewritten in place is fragile: a crash during write can truncate it, concurrent tasks can overwrite one another, and there is no transaction joining order intent to risk state. Technology matters less than atomic durability and testable semantics.
A safe startup recovery sequence
- Start read-only. Disable new entries before connecting any strategy loop.
- Load and validate local state. Check database integrity, schema version, strategy version, and unfinished intents.
- Fetch venue snapshots. Obtain current open orders, recent orders and fills, balances, and positions where the API supports them.
- Match identifiers. Resolve pending submissions by client ID before considering any retry.
- Replay unseen events. Insert fills with uniqueness constraints and recalculate exposure.
- Compare invariants. Position quantity, open-order coverage, reserved balance, and risk counters must agree.
- Quarantine exceptions. Unknown orders, unexplained positions, and missing protection require an alert and an explicit policy.
- Restore protection. Verify server-side stops or other approved risk-reducing orders without blindly duplicating them.
- Resume deliberately. Enable strategy entries only after reconciliation records a healthy checkpoint.
This sequence is part of uptime and reliability. Automatic restart without recovery merely repeats a failure faster. If reconciliation cannot establish a known state, connect the condition to the kill-switch policy and require operator review.
Crash at every awkward moment
Happy-path unit tests do not prove persistence. Use a fake venue with deterministic responses, then terminate the bot immediately before and after every durable write and network call. Include these cases:
- intent saved, order never sent;
- order accepted, acknowledgement lost;
- partial fill received, local transaction interrupted;
- fill event delivered twice;
- private stream disconnected while an order fills;
- manual venue order appears during downtime;
- symbol rules or strategy version changed before restart;
- database is unavailable, locked, or fails an integrity check.
The invariant is stronger than “the process came back”: recovery must not double an order, discard a fill, reset a risk halt, or claim healthy status while local and venue exposure disagree. Monitor time to reconcile, unknown-order count, unmatched-fill count, checkpoint age, and backup restore tests.
Frequently asked questions
What state should a trading bot persist?
Persist strategy identity and version, trade intent, client and venue order IDs, order transitions, fills, position lots, risk counters, protective-order relationships, and reconciliation checkpoints. Cached prices and indicators can usually be rebuilt, but evidence of money-moving actions must survive a crash.
Is the trading bot database the source of truth?
The venue is authoritative for orders, fills, balances, and positions that exist there, while the local database is authoritative for the bot's intent, decisions, and observations. Safe recovery compares both. Treating either side alone as complete can create duplicate orders or orphan exposure.
When may a restarted trading bot resume entries?
Only after loading durable state, obtaining fresh venue snapshots, matching known client and venue order IDs, importing missed fills, resolving or quarantining discrepancies, restoring protective-order coverage, and confirming risk limits. Unknown state should block new exposure.
How should trading bot persistence be tested?
Terminate the process at every boundary around order submission and fill processing, including before write, after durable intent, after network send, after venue acceptance, and before local acknowledgement. Restart against a deterministic fake venue and verify that exposure never doubles and every discrepancy produces an alert.