betex 0.13.0

Betfair / Prediction Market Exchange
Documentation
//! Book event types.

use super::types::*;
use crate::{
    book::protocol::command::{Persistence, Side, TimeInForce},
    types::*,
};
use betex_macros::TaggedEnumBridge;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smallvec::SmallVec;
use std::fmt;

/// A reusable event buffer that avoids heap allocation for typical operations (1-4 events).
pub type EventVec = SmallVec<[BookEventEnvelope; 4]>;

/// Additional context attached to an emitted book event envelope.
pub type EventMetadata = Option<Value>;

/// A single event in the book's stream.
#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct BookEventEnvelope {
    pub market_id: MarketId,
    pub market_name: String,
    /// Per-market event sequence number (starts at 1 for MarketCreated).
    pub market_seq: u64,
    /// Unix timestamp in milliseconds (UTC) shared across all events in a tx.
    #[rkyv(with = crate::types::DateTimeUtcAsUnixMillis)]
    pub timestamp: DateTime,
    #[rkyv(with = crate::types::JsonValueOptionAsStringOption)]
    pub metadata: EventMetadata,
    pub event: BookEvent,
}

#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    Serialize,
    Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CancelCause {
    UserCancel,
    Replace,
    FokRemainder,
    BatchCancel,
    CloseCancel,
    SuspendLapse,
    InPlayLapse,
    MarketVoid,
    RunnerRemoved,
    Admin,
}

#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TradeRole {
    Maker,
    Taker,
}

/// Book event types.
#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    TaggedEnumBridge,
    Serialize,
    Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub enum BookEvent {
    /// Engine-level: create and initialize a market/book (must be journaled before any per-market events).
    MarketCreated {
        correlation_id: Option<CorrelationId>,
        name: String,
        market_model: MarketModel,
        market_kind: MarketKind,
        /// Empty means "dynamic multi-runner" (runners may be added later).
        runner_ids: Vec<RunnerId>,
        runner_labels: Vec<String>,
    },
    MarketStateChanged {
        to: BookMarketState,
        reason: String,
    },
    OrderAccepted {
        correlation_id: Option<CorrelationId>,
        order_id: OrderId,
        account_id: AccountId,
        runner_id: RunnerId,
        side: Side,
        price: OddsX10000,
        stake: Money,
        persistence: Persistence,
        time_in_force: TimeInForce,
    },
    BinaryOrderAccepted {
        correlation_id: Option<CorrelationId>,
        order_id: OrderId,
        account_id: AccountId,
        side: Side,
        price_ticks: u16,
        qty_shares: u64,
        time_in_force: TimeInForce,
    },
    OrderCancelled {
        order_ids: Vec<OrderId>,
        account_ids: Vec<AccountId>,
        cursor_after: Option<OrderId>,
        cursor_after_account_id: Option<AccountId>,
        is_final: bool,
        cancel_cause: CancelCause,
        cause_detail: Option<String>,
    },
    TradeMatched {
        order_id: OrderId,
        account_id: AccountId,
        role: TradeRole,
        runner_id: RunnerId,
        side: Side,
        price: OddsX10000,
        stake: Money,
        counter_party: OrderId,
        counter_party_account_id: AccountId,
        remaining_stake: Money,
        matched_delta: Money,
    },
    BinaryTradeMatched {
        order_id: OrderId,
        account_id: AccountId,
        role: TradeRole,
        side: Side,
        price_ticks: u16,
        counter_party: OrderId,
        counter_party_account_id: AccountId,
        remaining_qty_shares: u64,
        matched_delta_shares: u64,
    },
    /// Administrative no-op marker for downstream systems.
    VoidTradesFromTime {
        #[rkyv(with = crate::types::DateTimeUtcAsUnixMillis)]
        from_matched_at_inclusive: DateTime,
        reason: String,
    },
    RunnerRemoved {
        runner_id: RunnerId,
        reduction_factor_bps: Option<u32>,
    },
    MarketSettled {
        runner_results: Vec<(RunnerId, RunnerResult)>,
        dead_heat_divisor: Option<u32>,
    },
    /// Batch-cancel process started with deterministic parameters.
    BatchCancelStarted {
        batch_max_events: u16,
        started_at_ms: i64,
        from_created_at_inclusive_ms: Option<i64>,
        to_created_at_inclusive_ms: Option<i64>,
        account_filter: Option<AccountId>,
        runner_filter: Option<RunnerId>,
        reason: String,
        final_event_metadata_json: Option<String>,
    },
    /// Generic completion marker for a batched order-cancel process.
    BatchProcessCompleted {
        cancel_cause: CancelCause,
    },
    /// Engine-level: market removed from memory (terminal cleanup).
    MarketRemoved {
        reason: String,
    },
}

impl fmt::Display for BookEventEnvelope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "BOOK_EVENT_ENVELOPE market_id={:?} market_name={} timestamp_ms={} event={}",
            self.market_id,
            self.market_name,
            self.timestamp.timestamp_millis(),
            self.event
        )
    }
}

impl fmt::Display for BookEvent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let kind = match self {
            BookEvent::MarketCreated { .. } => "MARKET_CREATED",
            BookEvent::MarketStateChanged { .. } => "MARKET_STATE_CHANGED",
            BookEvent::OrderAccepted { .. } => "ORDER_ACCEPTED",
            BookEvent::BinaryOrderAccepted { .. } => "BINARY_ORDER_ACCEPTED",
            BookEvent::OrderCancelled { .. } => "ORDER_CANCELLED",
            BookEvent::TradeMatched { .. } => "TRADE_MATCHED",
            BookEvent::BinaryTradeMatched { .. } => "BINARY_TRADE_MATCHED",
            BookEvent::VoidTradesFromTime { .. } => "VOID_TRADES_FROM_TIME",
            BookEvent::RunnerRemoved { .. } => "RUNNER_REMOVED",
            BookEvent::MarketSettled { .. } => "MARKET_SETTLED",
            BookEvent::BatchCancelStarted { .. } => "BATCH_CANCEL_STARTED",
            BookEvent::BatchProcessCompleted { .. } => "BATCH_PROCESS_COMPLETED",
            BookEvent::MarketRemoved { .. } => "MARKET_REMOVED",
        };
        f.write_str(kind)
    }
}