betex 0.35.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,
    Reduce,
    IocRemainder,
    FokRemainder,
    RunnerRemoved,
    Admin,
}

impl CancelCause {
    pub(crate) fn detail(self) -> &'static str {
        match self {
            Self::UserCancel => "USER_CANCEL",
            Self::Reduce => "REDUCE",
            Self::IocRemainder => "IOC_REMAINDER",
            Self::FokRemainder => "FOK_REMAINDER",
            Self::RunnerRemoved => "RUNNER_REMOVED",
            Self::Admin => "ADMIN",
        }
    }
}

pub(crate) fn time_in_force_remainder_cancel_cause(
    time_in_force: TimeInForce,
    has_remaining: bool,
) -> Option<CancelCause> {
    if !has_remaining {
        return None;
    }
    match time_in_force {
        TimeInForce::ImmediateOrCancel => Some(CancelCause::IocRemainder),
        TimeInForce::FillOrKill { .. } => Some(CancelCause::FokRemainder),
        TimeInForce::Gtc => None,
    }
}

#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct CancelledOrderEntry {
    pub order_id: OrderId,
    pub account_id: AccountId,
    pub correlation_id: Option<CorrelationId>,
}

#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct OrderCancellationCursor {
    pub order_id: OrderId,
    pub account_id: AccountId,
}

#[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,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
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,
        /// Concrete exchange book selection used to recreate exchange books during replay.
        book_type: Option<BookType>,
        market_kind: MarketKind,
        market_state: BookMarketState,
        market_phase: MarketPhase,
        /// Empty means "dynamic multi-runner" (runners may be added later).
        runner_ids: Vec<RunnerId>,
        runner_labels: Vec<String>,
    },
    RunnersAdded {
        runner_ids: Vec<RunnerId>,
        runner_labels: Vec<String>,
    },
    RunnersRemoved {
        runner_ids: Vec<RunnerId>,
        runner_labels: Vec<String>,
        reduction_factor_bps: Option<u32>,
    },
    MarketStateChanged {
        to: BookMarketState,
        reason: String,
        /// Present when the state transition starts a batched close process.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        close_batch_max_events: Option<u16>,
    },
    MarketPhaseChanged {
        to: MarketPhase,
        reason: String,
    },
    OrderAccepted {
        correlation_id: Option<CorrelationId>,
        order_id: OrderId,
        account_id: AccountId,
        runner_id: RunnerId,
        runner_label: String,
        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 {
        cancelled_order: CancelledOrderEntry,
        cancel_cause: CancelCause,
        cause_detail: Option<String>,
    },
    OrderCancelledBatched {
        cancelled_orders: Vec<CancelledOrderEntry>,
        cursor_after: Option<OrderCancellationCursor>,
        batch_mode: BatchMode,
        detail: Option<String>,
    },
    TradeMatched {
        correlation_id: Option<CorrelationId>,
        order_id: OrderId,
        account_id: AccountId,
        role: TradeRole,
        runner_id: RunnerId,
        runner_label: String,
        side: Side,
        market_phase: MarketPhase,
        price: OddsX10000,
        stake: Money,
        counter_party: OrderId,
        counter_party_account_id: AccountId,
        remaining_stake: Money,
        matched_delta: Money,
    },
    BinaryTradeMatched {
        correlation_id: Option<CorrelationId>,
        order_id: OrderId,
        account_id: AccountId,
        role: TradeRole,
        side: Side,
        market_phase: MarketPhase,
        price_ticks: u16,
        counter_party: OrderId,
        counter_party_account_id: AccountId,
        remaining_qty_shares: u64,
        matched_delta_shares: u64,
    },
    /// Administrative passthrough marker for downstream systems.
    VoidTrades {
        market_phase: MarketPhase,
        #[rkyv(with = crate::types::DateTimeUtcAsUnixMillis)]
        start_time: DateTime,
        #[rkyv(with = crate::types::DateTimeUtcAsUnixMillis)]
        end_time: DateTime,
        void_reason: String,
    },
    RunnerRemoved {
        runner_id: RunnerId,
        runner_label: String,
        reduction_factor_bps: Option<u32>,
    },
    /// Batched cleanup process queued behind the current active process.
    BatchProcessQueued {
        batch_mode: BatchMode,
        batch_max_events: u16,
        target: BatchProcessTarget,
        detail: Option<String>,
    },
    /// Batched cleanup process started with deterministic parameters.
    BatchProcessStarted {
        batch_mode: BatchMode,
        batch_max_events: u16,
        target: BatchProcessTarget,
        detail: Option<String>,
    },
    /// Explicitly supersede the current batch mode with a new deterministic target.
    BatchProcessRetargeted {
        from_mode: BatchMode,
        to_mode: BatchMode,
        batch_max_events: u16,
        target: BatchProcessTarget,
        detail: Option<String>,
        abandoned_detail: Option<String>,
    },
    /// Generic completion marker for a batched order-cancel process.
    BatchProcessCompleted {
        batch_mode: BatchMode,
    },
    /// 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::RunnersAdded { .. } => "RUNNERS_ADDED",
            BookEvent::RunnersRemoved { .. } => "RUNNERS_REMOVED",
            BookEvent::MarketStateChanged { .. } => "MARKET_STATE_CHANGED",
            BookEvent::MarketPhaseChanged { .. } => "MARKET_PHASE_CHANGED",
            BookEvent::OrderAccepted { .. } => "ORDER_ACCEPTED",
            BookEvent::BinaryOrderAccepted { .. } => "BINARY_ORDER_ACCEPTED",
            BookEvent::OrderCancelled { .. } => "ORDER_CANCELLED",
            BookEvent::OrderCancelledBatched { .. } => "ORDER_CANCELLED_BATCHED",
            BookEvent::TradeMatched { .. } => "TRADE_MATCHED",
            BookEvent::BinaryTradeMatched { .. } => "BINARY_TRADE_MATCHED",
            BookEvent::VoidTrades { .. } => "VOID_TRADES",
            BookEvent::RunnerRemoved { .. } => "RUNNER_REMOVED",
            BookEvent::BatchProcessQueued { .. } => "BATCH_PROCESS_QUEUED",
            BookEvent::BatchProcessStarted { .. } => "BATCH_PROCESS_STARTED",
            BookEvent::BatchProcessRetargeted { .. } => "BATCH_PROCESS_RETARGETED",
            BookEvent::BatchProcessCompleted { .. } => "BATCH_PROCESS_COMPLETED",
            BookEvent::MarketRemoved { .. } => "MARKET_REMOVED",
        };
        f.write_str(kind)
    }
}