# Order Book Specification

The core order book for a single betting market.

## Overview

Each `Book` instance manages one market with multiple runners (selections). Orders are matched using price-time priority within each runner's back/lay sides.

## Market State And Phase

Lifecycle state and trading phase are modeled separately.

### Market lifecycle state

| State | Matchable | Accepts Orders | Description |
|-------|-----------|----------------|-------------|
| `Open` | Yes | Yes | Market is available for trading |
| `Suspended` | No | No | Temporarily paused (e.g. goal scored) |
| `Halted` | No | No | Administrative halt; batch cancel commands still allowed |
| `Closed` | No | No | Event finished, awaiting settlement |
| `Voided` | No | No | Market voided (terminal) |
| `Settled` | No | No | Market settled (terminal) |

### Market trading phase

| Phase | Meaning |
|-------|---------|
| `Pre` | Pre-event trading |
| `Live` | Live / in-play trading |

While a market is `Open`, it may be either `Pre` or `Live`.
Suspends and halts do not change phase; resuming returns the market to `Open` with the prior phase intact.

There is no separate `Closing` market state. Batched close/void progress is tracked in `BatchProcessState` while market state is already `Closed` or `Voided`.
`SetMarketState { state: Closed }` and `CloseMarket` both use the same batched close cancellation flow.
Read-side `MarketSnapshot` payloads expose lifecycle state, trading phase, and depth only; they do not carry close-progress state.

## Market Kinds

Markets are configured with a `MarketKind`:

- `PreEventOnly`: must start in `Pre` and never transition to `Live`.
- `InPlayCapable`: may start in `Pre` or `Live`; runtime transition support is `Pre -> Live`.
- `LiveOnly`: must start in `Live`.

Command rules:

- `SetMarketState` controls lifecycle state: `Open`, `Suspended`, `Closed`.
- `SetMarketPhase` controls trading phase.
- The current runtime transition is `Pre -> Live` for `InPlayCapable` markets.
- `ResumeMarket` returns `Halted -> Open` without needing to reconstruct whether the market was `Pre` or `Live`.

## Order Lifecycle

| State | Description |
|-------|-------------|
| `ExecutableUnmatched` | Resting, no fills yet |
| `ExecutablePartiallyMatched` | Resting, partially filled |
| `ExecutionComplete` | Fully filled |
| `Cancelled` | User cancelled |
| `Lapsed` | Removed due to persistence policy at live-phase transition |
| `Voided` | Market voided |

Time-based order expiry is not supported; orders persist until matched, cancelled, lapsed, or market close/void/settle.

## Matching Rules

1. **Price Priority**: Better prices match first
   - Back orders: higher lay prices match first
   - Lay orders: lower back prices match first

   Odds are interpreted as bettor limits:
   - A BACK price is the minimum odds the backer will accept
   - A LAY price is the maximum odds the layer will accept

2. **Time Priority (FIFO)**: Within same price level, earlier orders match first

3. **Current Implementation**:
   - A BACK taker walks same-runner resting LAY liquidity from the highest eligible odds down to the taker limit. A LAY taker walks same-runner resting BACK liquidity from the lowest eligible odds up to the taker limit.
   - In `TwoRunnerBook`, direct liquidity is merged with implied liquidity from the opposite runner using `derived_odds(o) = floor_tick(o / (o - 1))`.
   - BACK takers rank direct and implied candidates together by highest effective odds first. LAY takers rank them together by lowest effective odds first. Ties use FIFO by `created_at`, then `order_id`.
   - `TwoRunnerBook` does not prefer direct liquidity over implied liquidity. It prefers whichever candidate is better for the incoming bettor.
   - Implied matching converts maker and taker stake. `implied_maker_stake_from_taker` uses floor rounding, while `implied_taker_capacity_from_maker` uses ceil rounding so tiny maker remainders can be fully consumed without leaving artificial dust.

4. **Execution Price**:
   - Direct trades execute at the **maker's price** (resting order's price).
   - In two-runner implied matches, the taker trade view uses the derived effective price on the taker runner, while the maker trade view keeps the maker's original order price on the opposite runner.

5. **Self-Match Prevention**: A taker does not match against resting liquidity from the same `account_id`

## Persistence Types

Applied when market transitions from `Pre` to `Live`:

| Type | Behavior |
|------|----------|
| `Lapse` | Cancel unmatched portion at phase transition via the same bounded batch-lapse process used by suspend |
| `Persist` | Keep order live after the phase transition |
| `MarketOnClose` | Special handling for BSP markets (future) |

## Commands

### PlaceOrder
```rust
PlaceOrder {
    runner_id: RunnerId,
    account_id: AccountId,
    client_order_id: Option<ClientOrderId>,
    side: Side,               // Back | Lay
    odds: OddsX10000,          // Must be valid tick (see tick-ladder.md)
    stake: Money,             // Backer's stake amount
    persistence: Persistence,
    time_in_force: TimeInForce,  // See tif.md
}
```

### PlaceBinaryOrder
```rust
PlaceBinaryOrder {
    account_id: AccountId,
    client_order_id: Option<ClientOrderId>,
    side: Side,                 // Buy(Yes) | Sell(No)
    price_ticks: u16,
    qty_shares: u64,
    time_in_force: TimeInForce,
}
```

### CancelOrder
```rust
CancelOrder {
    order_id: OrderId,
    account_id: AccountId,
}
```

### ReplaceOrder
Atomic cancel + place. If cancel succeeds but place fails, the cancel stands.
```rust
ReplaceOrder {
    account_id: AccountId,
    order_id: OrderId,
    new_odds: Option<OddsX10000>,
    new_stake: Option<Money>,
}
```

### ReplaceBinaryOrder
```rust
ReplaceBinaryOrder {
    account_id: AccountId,
    order_id: OrderId,
    new_price_ticks: Option<u16>,
    new_qty_shares: Option<u64>,
}
```

Note: replacement binary orders are re-entered as `TimeInForce::Gtc`.

## Events

All mutations emit `BookEventEnvelope { market_id, timestamp, event }` entries. Sequencing is handled by the journal `Envelope` (`seq`, `tx_id`, `tx_len`, `tx_ix`) rather than the book event itself.

### Deterministic ordering

The canonical strict order is the event stream sequence (`seq`), not the timestamp.

When a tie-breaker is needed within the book (e.g. implied matching candidates at the same price),
ordering is:
1. price priority (best price first)
2. FIFO by `created_at`
3. `order_id` as a final deterministic tie-breaker when timestamps collide

| Event | Description |
|-------|-------------|
| `MarketCreated` | Market initialized in the event stream |
| `MarketStateChanged` | State transition |
| `MarketPhaseChanged` | Phase transition |
| `OrderAccepted` | Order entered the book |
| `BinaryOrderAccepted` | Prediction-market order accepted |
| `OrderCancelled` | Single live order cancelled |
| `OrderCancelledBatched` | Batch chunk of cancelled live orders |
| `TradeMatched` | Execution occurred |
| `BinaryTradeMatched` | Prediction-market trade matched |
| `VoidTradesFromTime` | Administrative compensation marker |
| `RunnerRemoved` | Runner removed with optional reduction factor |
| `MarketSettled` | Market settled with results |
| `BatchCancelStarted` | Batch-cancel process start marker |
| `BatchProcessCompleted` | Generic completion marker for batched close/cancel/lapse/void |
| `MarketRemoved` | Terminal market removal marker |

`OrderCancelled` carries a single `cancelled_order: CancelledOrderEntry` plus
`cancel_cause` and optional `cause_detail`.

`OrderCancelledBatched` carries `cancelled_orders: Vec<CancelledOrderEntry>` plus an optional
typed `cursor_after { order_id, account_id }` object for batched close/cancel/lapse/void flows.
`BatchProcessCompleted` remains the authoritative completion signal for those batched flows.

`TradeMatched` and `BinaryTradeMatched` carry `market_phase` at the moment the match is emitted,
so consumers can distinguish pre-event from live executions without overloading lifecycle state.

## Trade Voiding

Trade voiding uses `VoidTradesFromTime` as an administrative forward-only marker.
The current book model does not restore historical order quantities when this event is emitted.

## Validation

Orders are rejected for:
- `MARKET_NOT_OPEN`: Market not in matchable state
- `RUNNER_NOT_FOUND`: Invalid runner ID
- `INVALID_ODDS`: Price not on tick ladder or < 1.01
- `INVALID_STAKE`: Stake <= 0
- `ORDER_ID_REUSE`: Duplicate order ID

## Idempotency

Commands include a `correlation_id`. Duplicate submissions are expected to be deduplicated by the caller; the engine does not emit a separate `CmdDeduplicated` event.
`CorrelationId`/`IdempotencyKey` are string newtypes in the current type layer.
