# Examples: end-to-end order book reference
This folder contains two reference implementations that together show an end-to-end “matching engine + durable event log + queryable read model + client” structure for an order book.
- `live.rs`: a local server that runs the engine, persists events to a WAL, maintains a projection (read model), and exposes HTTP + WebSocket APIs.
- `live_tui.rs`: a terminal UI client that consumes snapshots over WebSocket, sends commands back to the server, and can run embedded trading bots for load/behavior testing.
The TUI ships with a **3-runner soccer market** preset (Home/Draw/Away), but the server itself
starts with a default engine and **no markets**. Create a market from the TUI control pane or over
`ws_ctl`; the architectural pattern is the point: treat the order book as a
**command → event → projection** pipeline.
## Quick start
In two terminals:
1) Start the server:
```bash
cargo run --example live
```
1) Start the TUI client:
```bash
cargo run --example live_tui
```
Optional server tuning (disruptor ring size must be a power of 2):
```bash
cargo run --example live -- --ring-size 32768
```
Use a blocking wait strategy for lower idle CPU usage:
```bash
cargo run --example live -- --wait-strategy blocking-wait
```
After creating a market (the first one is usually `market_id=1`), you can also query the server directly:
```bash
curl http://127.0.0.1:3000/snapshot?market_id=1
curl http://127.0.0.1:3000/risk?market_id=1
curl http://127.0.0.1:3000/completed_orders?market_id=1
curl http://127.0.0.1:3000/metrics
```
## `live.rs` (server): the end-to-end pipeline
At a high level `live.rs` splits the system into:
1) **Write path (commands)**: client submits commands (place/cancel/admin) to the engine.
2) **Event log (WAL)**: accepted commands produce events that are appended to durable storage.
3) **Read path (projection)**: a projection consumes the event stream and builds queryable state.
4) **API layer**: HTTP endpoints for ad-hoc reads, and WebSockets for streaming snapshots + control.
### Components and responsibilities
**Engine (authoritative state + matching)**
- The engine owns the authoritative order book state.
- Clients interact with it via `Command` (e.g. `PlaceOrder`, `CancelOrder`, `SetMarketState`, `AwaitLiveMarket`, `GoLiveMarket`, `CloseMarket`).
- Command submission happens through an `EngineHandler` (`state.engine.submit(...)` / `submit_with_response(...)`).
**WAL (durable event stream)**
- The engine’s outputs are journaled to an LMDB-backed WAL (`WAL_PATH`).
- On restart, the engine can be rebuilt from prior state (either from the WAL alone, or from an engine root snapshot if present).
- Mutable WAL opens take a fail-fast single-writer lock on the WAL root so only one writer process owns a WAL directory at a time.
- This lock is for local filesystems and single-attach block storage only; it is not sufficient fencing for shared RWX storage such as GKE Filestore, NFS, SMB, or similar multi-node shared disks.
**Projection (read model, decoupled from the engine)**
`ProjectionHandler` is registered as an engine handler and receives the same `JournalEvent` stream that is persisted.
It maintains two complementary read-side structures:
- `EngineRoot` (`ProjectionHandler.root`): a compact, query-friendly view that can generate `MarketSnapshot` (state, phase, and a tagged `book` payload for exchange or binary depth).
- `LiveState` (`ProjectionHandler.live_state`): an explicitly materialized “ops” view for convenience queries:
- per-order lifecycle (open vs terminal state),
- a trade table (including voided trades),
- derived sets like `open_orders` to make risk computations fast.
The projection keeps an in-memory `ProjectionSnapshot` (`projection_snapshot`) updated at end-of-batch, which is what the HTTP/WS endpoints serve.
### The data flow (command → event → projection → snapshot)
1) A client sends a JSON `ClientMsg` over `ws_ctl` (or you can call engine APIs directly inside the process).
2) The server translates `ClientMsg` into a `betex::book::protocol::command::Command`.
3) The engine validates and matches:
- emits `BookEventEnvelope` events (order accepted/cancelled, trade matched/voided, market state transitions, batch-process markers, …),
- appends them to the WAL,
- optionally returns a per-command `Response` (used for acks).
4) `ProjectionHandler` consumes the `JournalEvent` stream:
- asserts monotonic sequence (`event.seq`) and applies to `EngineRoot`,
- applies a subset of events to `LiveState` for “live” queries,
- periodically refreshes the shared `ProjectionSnapshot` for readers.
5) HTTP endpoints and WebSocket snapshot streams serve the latest projection snapshot.
### APIs exposed by `live.rs`
**HTTP**
- The HTTP surface serves the default engine only.
- `GET /snapshot`: one JSON snapshot of the market using the `book { kind, data }` shape.
- `GET /completed_orders?market_id=&account_id=&limit=`: recent terminal orders observed by the projection. `account_id` is a string filter.
- `GET /risk?market_id=&account_id=`: simple risk/PnL summary derived from projection trades + open orders. `account_id` is a string filter.
- `GET /metrics`: engine/WAL/projection metrics (polled by the TUI).
- `GET /health`: health probe.
- `GET /debug/live_counts`: sizes of in-memory projection tables (orders, trades, …).
**WebSocket**
- `GET /ws?depth=N&market_id=M`: **read-only** stream of JSON `Snapshot` messages (every ~200ms).
- `GET /ws_ctl?depth=N&market_id=M[&no_ack=1]`: snapshot stream + control plane:
- accepts JSON `ClientMsg` commands from the client, including `CreateMarket`, `AddRunners`, `ListMarkets`, `Subscribe`, trading, and admin commands,
- periodically emits `Status` messages (global sequence + market count),
- optionally emits `Ack`, `OrderResponse`, `MarketCreated`, `MarketUpdated`, and `MarketList` messages.
`no_ack=1` is intended for throughput testing: the server does “fire-and-forget” command submission and avoids per-command response channels and websocket replies.
`CreateMarket.book_type` is required for `EXCHANGE_ODDS` markets. Use `TWO_RUNNER` for implied matching or `MULTI_RUNNER` for direct-only matching, including two-runner direct-only markets:
```json
{
"CREATE_MARKET": {
"name": "Direct-only two-runner market",
"market_model": "EXCHANGE_ODDS",
"book_type": "MULTI_RUNNER",
"market_kind": "IN_PLAY_CAPABLE",
"market_state": "OPEN",
"market_phase": "PRE",
"runner_ids": [1, 2],
"runner_labels": ["A", "B"]
}
}
```
### Notes on durability and recovery
- The WAL path is stable (`target/live.lmdb`) so restarts can recover prior state.
- `ProjectionDiskSnapshot` is persisted as postcard binary at `PROJECTION_PATH`.
- On restart, the live example loads the disk snapshot when it is compatible with the requested market set and falls back to WAL replay when needed.
## `live_tui.rs` (client): UI + bots over `ws_ctl`
`live_tui.rs` connects to the server’s control websocket (defaults to `ws://127.0.0.1:3000/ws_ctl?depth=10`) and runs three loops:
1) **WebSocket loop (`ws_loop`)**
- reconnects with exponential backoff,
- reads `ServerMsg` (`Snapshot`, `Status`, `Ack`, `OrderResponse`) and applies them to a shared `UiState`,
- sends serialized `ClientMsg` commands from two channels:
- user commands (keypress-driven, low volume),
- bot commands (high volume; can be dropped/drained when pausing bots).
2) **Metrics loop (`metrics_loop`)**
- polls `GET /metrics` over a simple TCP HTTP client,
- derives rates (events/sec, commands/sec) over a sliding window,
- computes sticky p50/p99 stage duration estimates from histogram deltas.
3) **UI loop (ratatui + crossterm)**
- renders the latest snapshot and connection status,
- provides key bindings for market/engine controls and bot toggles,
- displays bot counters (orders sent, trades observed).
### Embedded bots
Bots are simple strategy generators that react to the current `MarketSnapshot` and emit `PlaceOrder` commands:
- **Market maker bots**: quote both sides across several ladder levels.
- **Noise trader bots**: place random small orders around the market.
- **Informed trader bots**: occasionally “sweep” (cross) when the snapshot looks favorable.
Correlation IDs encode `account_id` in the upper 32 bits (`(account_id << 32) | seq`), which lets the client attribute `OrderResponse` trades back to the originating bot for UI counters.
### Useful CLI flags
Run `cargo run --example live_tui -- --help` for the full list. Common ones:
- `--url <WS_URL>`: point the TUI at a different server or parameters.
- `--no-ack`: appends `no_ack=1` to the URL to suppress per-order acks (throughput mode).
- `--high-throughput`: faster bots + larger client buffers.
- `--bots <N>` / `--mm-bots` / `--noise-bots` / `--informed-bots`: control bot counts.