betex 0.7.8

Betfair / Prediction Market Exchange
Documentation
//! Shared types for order book implementations.

use crate::{
    book::protocol::command::{Persistence, Side},
    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,
    /// Close in progress (not matchable; cancels draining in batches)
    Closing,
    /// 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::Closing | Self::Closed | Self::Voided | Self::Settled
        )
    }

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

// ============================================================================
// 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)]
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,
}

// ============================================================================
// 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,
}

/// Trade lifecycle as owned by the book.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    serde::Serialize,
    serde::Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub enum BookTradeState {
    Live,
    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, Copy, serde::Serialize, serde::Deserialize)]
pub struct BookOrderInfo {
    pub order_id: OrderId,
    pub account_id: AccountId,
    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))
    }
}

// ============================================================================
// Book Trade
// ============================================================================

/// A single execution (match fill) emitted by the book.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookTrade {
    pub trade_id: TradeId,
    pub maker_order_id: OrderId,
    pub taker_order_id: OrderId,
    pub runner_id: RunnerId,
    pub price: OddsX10000,
    pub stake: Money,
    pub matched_at: DateTime,
    pub state: BookTradeState,
}

// ============================================================================
// 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()
    }
}