betex 0.18.0

Betfair / Prediction Market Exchange
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Shared types for order book implementations.

use crate::{
    book::common::events::CancelCause,
    book::protocol::{
        command::{Persistence, Side},
        reject::RejectReason,
    },
    types::*,
};
use std::collections::VecDeque;

// ============================================================================
// Market State
// ============================================================================

/// Book-local market states (single market per book).
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
    strum::Display,
    strum::AsRefStr,
)]
pub enum BookMarketState {
    /// Pre-event trading (matchable)
    Open,
    /// Live in-play trading (matchable)
    TurnInPlayEnabled,
    /// Temporarily halted (not matchable)
    Suspended,
    /// Administratively halted (not matchable; cancellations allowed)
    Halted,
    /// Event finished, awaiting settlement (not matchable)
    Closed,
    /// Market voided, all bets cancelled (terminal)
    Voided,
    /// Market settled with results (terminal)
    Settled,
}

impl BookMarketState {
    /// Whether matching is allowed in this state.
    pub fn is_matchable(self) -> bool {
        matches!(self, Self::Open | Self::TurnInPlayEnabled)
    }

    /// Whether the market is terminal (reject all commands that mutate trading state).
    pub fn is_terminal(self) -> bool {
        matches!(self, Self::Closed | Self::Voided | Self::Settled)
    }

    /// Whether the market is administratively halted.
    pub fn is_halted(self) -> bool {
        matches!(self, Self::Halted)
    }
}

/// Rejects order-entry commands when the market is not currently matchable.
pub fn ensure_can_accept_new_orders(state: BookMarketState) -> Result<(), RejectReason> {
    if state.is_matchable() {
        Ok(())
    } else {
        Err(RejectReason::MarketNotOpen)
    }
}

/// Returns `Err(NoChange)` when the book is already in the requested state.
pub fn ensure_state_change(
    current: BookMarketState,
    to: BookMarketState,
) -> Result<(), crate::book::protocol::reject::RejectReason> {
    if current == to {
        return Err(crate::book::protocol::reject::RejectReason::NoChange);
    }
    Ok(())
}

// ============================================================================
// Close Process
// ============================================================================

/// State tracking for the batched market close process.
///
/// When a market closes with many resting orders, cancellations are split
/// into batches to avoid unbounded event emission. This struct tracks
/// progress through the close process.
///
/// ## Cursor Semantics
///
/// The `cursor_after` field implements pagination over the order set:
///
/// - Orders are processed in ascending `OrderId` order (BTreeMap iteration)
/// - After each batch, `cursor_after` is set to the last processed `OrderId`
/// - The next batch processes orders where `order_id > cursor_after`
/// - `None` means "start from beginning" (first batch)
///
/// This cursor-based approach ensures **idempotent recovery**: if events
/// are replayed during recovery, the cursor prevents re-cancelling orders
/// that were already processed.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct CloseProcessState {
    /// Maximum events per batch (controls batch size).
    ///
    /// Set from the `CloseMarket` command's `batch_max_events` parameter.
    /// Minimum is 2 (to fit state-change event).
    pub batch_max_events: u16,

    /// Pagination cursor: last processed OrderId, or None if starting.
    ///
    /// The next batch will process orders with `order_id > cursor_after`.
    /// This enables resumption after crash/restart without double-cancellation.
    pub cursor_after: Option<OrderId>,

    /// Total live orders when close started (for progress tracking).
    ///
    /// This is the count of ExecutableUnmatched + ExecutablePartiallyMatched
    /// orders at the moment the close process began.
    pub total_live_orders: u64,

    /// Running count of orders cancelled so far.
    ///
    /// Updated after each batch. When `cancelled_total >= total_live_orders`,
    /// the close process is complete.
    pub cancelled_total: u64,

    /// Number of batches completed (for monitoring/debugging).
    pub chunks_done: u32,
}

/// State tracking for an in-progress batch-cancel command.
#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct CancelProcessState {
    /// Maximum number of order cancellations emitted per batch event.
    pub batch_max_events: u16,
    /// Pagination cursor: last processed OrderId.
    pub cursor_after: Option<OrderId>,
    /// Command start timestamp in unix millis (freeze horizon for deterministic filtering).
    pub started_at_ms: i64,
    /// Inclusive created-at lower bound.
    pub from_created_at_inclusive_ms: Option<i64>,
    /// Inclusive created-at upper bound.
    pub to_created_at_inclusive_ms: Option<i64>,
    /// Optional account filter.
    pub account_filter: Option<AccountId>,
    /// Optional runner filter.
    pub runner_filter: Option<RunnerId>,
    /// Batch reason propagated to emitted events.
    pub reason: String,
    /// Metadata JSON that must be attached only to the final batch event.
    pub final_event_metadata_json: Option<String>,
    /// Running count of cancelled orders.
    pub cancelled_total: u64,
    /// Number of emitted chunks.
    pub chunks_done: u32,
}

/// State tracking for generic batched order-state mutation processes.
#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct OrderBatchProcessState {
    /// Maximum number of order ids emitted in each batch event.
    pub batch_max_events: u16,
    /// Pagination cursor: last processed OrderId.
    pub cursor_after: Option<OrderId>,
    /// Cancellation cause propagated into emitted events.
    pub cancel_cause: CancelCause,
    /// Optional detail propagated into emitted events.
    pub cause_detail: Option<String>,
    /// Running count of processed orders.
    pub processed_total: u64,
    /// Number of emitted chunks.
    pub chunks_done: u32,
}

/// Unified process slot for batched operations. At most one can be active per market.
#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub enum BatchProcessState {
    Close(CloseProcessState),
    Cancel(CancelProcessState),
    Lapse(OrderBatchProcessState),
    Void(OrderBatchProcessState),
}

// ============================================================================
// Order Types
// ============================================================================

/// Order lifecycle as owned by the book.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
    strum::Display,
    strum::AsRefStr,
)]
pub enum BookOrderState {
    ExecutableUnmatched,
    ExecutablePartiallyMatched,
    ExecutionComplete,
    Cancelled,
    Lapsed,
    Voided,
}

// ============================================================================
// Settlement Types
// ============================================================================

/// Result for a runner at settlement.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub enum RunnerResult {
    Winner,
    Loser,
    Removed,
    RemovedVacant,
}

/// Signed money for P&L calculations.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct SignedMoney(pub i64);

impl SignedMoney {
    pub fn zero() -> Self {
        Self(0)
    }
}

// ============================================================================
// Book Order
// ============================================================================

/// Canonical order metadata shared across books.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookOrderInfo {
    pub order_id: OrderId,
    pub account_id: AccountId,
    pub correlation_id: Option<CorrelationId>,
    pub side: Side,
    pub state: BookOrderState,
    pub created_at: DateTime,
    pub last_updated_at: DateTime,
}

/// Canonical order representation stored by the book.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookOrder {
    pub info: BookOrderInfo,
    pub runner_id: RunnerId,
    pub price: OddsX10000,
    pub stake: Money,
    pub matched: Money,
    pub persistence: Persistence,
}

impl BookOrder {
    /// Remaining unmatched stake.
    pub fn remaining(&self) -> Money {
        Money(self.stake.0.saturating_sub(self.matched.0).max(0))
    }
}

// ============================================================================
// Market Depth
// ============================================================================

/// Price and size at a single level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PriceSize {
    pub price: OddsX10000,
    pub size: Money,
}

/// Available prices for a runner.
#[derive(Debug, Clone, Default)]
pub struct RunnerPrices {
    pub runner_id: RunnerId,
    pub available_to_back: Vec<PriceSize>,
    pub available_to_lay: Vec<PriceSize>,
}

/// Price and size for prediction markets (canonical YES-only).
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct BinaryPriceSize {
    pub price_ticks: u16,
    pub size_shares: u64,
}

/// Depth snapshot for a canonical YES-only prediction market.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BinaryDepth {
    pub max_price_ticks: u16,
    /// Bid levels (highest `price_ticks` first).
    pub bids: Vec<BinaryPriceSize>,
    /// Ask levels (lowest `price_ticks` first).
    pub asks: Vec<BinaryPriceSize>,
}

// ============================================================================
// Price Level (internal)
// ============================================================================

/// Internal per-price FIFO queue.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PriceLevel {
    pub fifo: VecDeque<OrderId>,
    pub total_remaining: Money,
}

impl PriceLevel {
    pub fn new() -> Self {
        Self {
            fifo: VecDeque::new(),
            total_remaining: Money::zero(),
        }
    }
}

impl Default for PriceLevel {
    fn default() -> Self {
        Self::new()
    }
}