betex 0.4.0

Betfair / Prediction Market Exchange
Documentation
//! Market snapshot types for bot synchronization.
//!
//! Bots use snapshots to get the current market state, then apply
//! streaming events to maintain a local replica.

use crate::book::{BinaryPriceSize, BookMarketState, PriceSize};
use crate::types::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MarketCloseSnapshot {
    pub batch_max_events: u16,
    pub total_live_orders: u64,
    pub cancelled_total: u64,
    pub chunks_done: u32,
    pub cursor_after: Option<OrderId>,
}

/// Complete snapshot of a market's depth at a specific sequence number.
///
/// This provides the synchronization point for bots: after receiving
/// a snapshot at sequence `seq`, apply all events with sequence > `seq`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MarketSnapshot {
    /// Market identifier.
    pub market_id: MarketId,
    /// Global sequence number when snapshot was taken.
    pub seq: u64,
    /// Market model (odds vs prediction).
    pub market_model: MarketModel,
    /// Current market state.
    pub state: BookMarketState,
    /// Close process progress (present while `state == Closing` and may be retained briefly after completion).
    #[serde(default)]
    pub close: Option<MarketCloseSnapshot>,
    /// Depth for each runner.
    pub runners: Vec<RunnerSnapshot>,
    /// Depth for prediction markets (present for `market_model == BinaryYes`).
    #[serde(default)]
    pub binary: Option<BinaryMarketSnapshot>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryMarketSnapshot {
    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>,
}

/// Snapshot of a single runner's order book depth.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunnerSnapshot {
    /// Runner identifier.
    pub runner_id: RunnerId,
    /// Prices available to back (sorted best-first, i.e., highest odds first).
    /// These are the lay orders on the book that you can back against.
    pub available_to_back: Vec<PriceSize>,
    /// Prices available to lay (sorted best-first, i.e., lowest odds first).
    /// These are the back orders on the book that you can lay against.
    pub available_to_lay: Vec<PriceSize>,
    /// Total matched volume on this runner.
    pub matched_volume: Money,
}

impl MarketSnapshot {
    /// Get runner snapshot by ID.
    pub fn runner(&self, runner_id: RunnerId) -> Option<&RunnerSnapshot> {
        self.runners.iter().find(|r| r.runner_id == runner_id)
    }

    /// Number of runners in the market.
    pub fn runner_count(&self) -> usize {
        self.runners.len()
    }
}

impl RunnerSnapshot {
    /// Best price to back at (highest odds available).
    pub fn best_back(&self) -> Option<&PriceSize> {
        self.available_to_back.first()
    }

    /// Best price to lay at (lowest odds available).
    pub fn best_lay(&self) -> Option<&PriceSize> {
        self.available_to_lay.first()
    }

    /// Spread in ticks between best back and lay.
    pub fn spread_ticks(&self) -> Option<u32> {
        let back = self.best_back()?;
        let lay = self.best_lay()?;
        back.price
            .ticks_between(lay.price)
            .map(|t| t.unsigned_abs())
    }

    /// Total available volume to back.
    pub fn total_back_volume(&self) -> Money {
        Money(self.available_to_back.iter().map(|p| p.size.0).sum())
    }

    /// Total available volume to lay.
    pub fn total_lay_volume(&self) -> Money {
        Money(self.available_to_lay.iter().map(|p| p.size.0).sum())
    }
}