betex 0.35.0

Betfair / Prediction Market Exchange
Documentation
# 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.