betex 0.7.7

Betfair / Prediction Market Exchange
Documentation
use super::super::RunnerResult;
use crate::types::*;
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(
    Debug,
    Clone,
    Copy,
    Serialize,
    Deserialize,
    PartialEq,
    Eq,
    Hash,
    strum::Display,
    strum::EnumString,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum Side {
    /// Yes/long side.
    ///
    /// Mapping:
    /// - Exchange odds: Back
    /// - Binary: Buy
    Yes,
    /// No/short side.
    ///
    /// Mapping:
    /// - Exchange odds: Lay
    /// - Binary: Sell
    No,
}

#[allow(non_upper_case_globals)]
impl Side {
    /// Alias for `Yes` (exchange BACK).
    pub const Back: Self = Self::Yes;
    /// Alias for `No` (exchange LAY).
    pub const Lay: Self = Self::No;
    /// Alias for `Yes` (binary BUY).
    pub const Buy: Self = Self::Yes;
    /// Alias for `No` (binary SELL).
    pub const Sell: Self = Self::No;
}

pub type BinarySide = Side;
pub type BinaryTimeInForce = TimeInForce;

#[derive(
    Debug,
    Clone,
    Copy,
    Serialize,
    Deserialize,
    PartialEq,
    Eq,
    Hash,
    strum::Display,
    strum::EnumString,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum Persistence {
    Lapse,
    Persist,
    MarketOnClose,
}

/// Time-in-force policy for order execution.
#[derive(
    Debug,
    Clone,
    Copy,
    Serialize,
    Deserialize,
    PartialEq,
    Eq,
    Hash,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TimeInForce {
    /// Good-til-Cancelled: match what's available now, then rest the remainder.
    Gtc,
    /// Fill-or-Kill with an optional minimum fill size.
    ///
    /// Units:
    /// - Exchange orders: stake (Money quanta)
    /// - Binary orders: shares
    FillOrKill { min_fill: Option<Quantity> },
}

#[derive(
    Debug,
    Clone,
    Copy,
    Serialize,
    Deserialize,
    PartialEq,
    Eq,
    Hash,
    strum::Display,
    strum::EnumString,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum MarketState {
    /// Market is open and accepts matchable orders.
    Open,
    /// Market is open and matchable while in-play.
    InPlay,
    /// Market is temporarily suspended (no matching).
    Suspended,
    /// Market is closed (no new orders; awaiting settlement).
    Closed,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Command {
    /// Optional client-provided correlation id (UUID-like string, max 36 bytes).
    ///
    /// Propagated into market/order creation events for downstream correlation when present.
    #[serde(default)]
    pub correlation_id: Option<CorrelationId>,
    /// Target market id for the command.
    pub market_id: MarketId,
    /// The command payload.
    pub kind: CommandKind,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CommandKind {
    /// Engine-level: create and initialize a market/book.
    CreateMarket {
        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>,
    },
    /// Place a new order on a market/runner.
    PlaceOrder {
        runner_id: RunnerId,
        account_id: AccountId,
        client_order_id: Option<ClientOrderId>,
        side: Side,
        odds: OddsX10000,
        stake: Money,
        persistence: Persistence,
        time_in_force: TimeInForce,
    },
    /// Place a new canonical YES-only prediction-market order.
    PlaceBinaryOrder {
        account_id: AccountId,
        client_order_id: Option<ClientOrderId>,
        side: Side,
        /// Uniform probability ticks in `[0..max_price_ticks]` for this market.
        price_ticks: u16,
        /// Quantity in shares (each share pays out `max_price_ticks` quanta on YES resolution).
        qty_shares: u64,
        time_in_force: TimeInForce,
    },
    /// Cancel an existing order.
    CancelOrder {
        account_id: AccountId,
        order_id: OrderId,
    },
    /// Replace an existing order by updating price and/or stake.
    ReplaceOrder {
        account_id: AccountId,
        order_id: OrderId,
        new_odds: Option<OddsX10000>,
        new_stake: Option<Money>,
    },
    /// Replace an existing prediction-market order by updating price and/or quantity.
    ReplaceBinaryOrder {
        account_id: AccountId,
        order_id: OrderId,
        new_price_ticks: Option<u16>,
        new_qty_shares: Option<u64>,
    },
    /// Cash out matched exposure on a runner.
    CashoutRunner {
        account_id: AccountId,
        runner_id: RunnerId,
        percent_bps: u16,
        max_slippage_bps: u16,
        depth_levels: u16,
    },
    /// Update market trading state.
    SetMarketState { state: MarketState },
    /// Close a market in bounded event batches (starts a close process).
    CloseMarket { batch_max_events: u16 },
    /// Internal: continue an in-progress close process (emits another cancel batch).
    ContinueCloseMarket,
    /// Remove a runner from the market, optionally applying a reduction factor (in bps).
    RemoveRunner {
        runner_id: RunnerId,
        reduction_factor_bps: Option<u32>,
    },
    /// Void all trades matched at/after a timestamp (administrative).
    VoidTradesFromTime {
        from_matched_at_inclusive: DateTime,
        reason: String,
    },
    /// Void a specific set of trades by id (administrative).
    VoidTradeIds {
        trade_ids: Vec<TradeId>,
        reason: String,
    },
    /// Void a market (terminal).
    VoidMarket { reason: String },
    /// Settle a market (terminal).
    SettleMarket {
        runner_results: Vec<(RunnerId, RunnerResult)>,
        dead_heat_divisor: Option<u32>,
    },
    /// Halt a market/book (administrative).
    HaltMarket { reason: u32 },
    /// Resume a market/book after a halt (administrative).
    ResumeMarket,
    /// Engine-level: remove a terminal market from memory.
    RemoveMarket,
}

impl Command {
    /// Extract the market_id from the command.
    pub fn market_id(&self) -> MarketId {
        self.market_id
    }
}

impl fmt::Display for Command {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.kind)
    }
}

impl fmt::Display for CommandKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            CommandKind::CreateMarket { .. } => "CREATE_MARKET",
            CommandKind::PlaceOrder { .. } => "PLACE_ORDER",
            CommandKind::PlaceBinaryOrder { .. } => "PLACE_BINARY_ORDER",
            CommandKind::CancelOrder { .. } => "CANCEL_ORDER",
            CommandKind::ReplaceOrder { .. } => "REPLACE_ORDER",
            CommandKind::ReplaceBinaryOrder { .. } => "REPLACE_BINARY_ORDER",
            CommandKind::SetMarketState { .. } => "SET_MARKET_STATE",
            CommandKind::CloseMarket { .. } => "CLOSE_MARKET",
            CommandKind::ContinueCloseMarket => "CONTINUE_CLOSE_MARKET",
            CommandKind::RemoveRunner { .. } => "REMOVE_RUNNER",
            CommandKind::VoidTradesFromTime { .. } => "VOID_TRADES_FROM_TIME",
            CommandKind::VoidTradeIds { .. } => "VOID_TRADE_IDS",
            CommandKind::VoidMarket { .. } => "VOID_MARKET",
            CommandKind::SettleMarket { .. } => "SETTLE_MARKET",
            CommandKind::HaltMarket { .. } => "HALT_MARKET",
            CommandKind::ResumeMarket => "RESUME_MARKET",
            CommandKind::CashoutRunner { .. } => "CASHOUT_RUNNER",
            CommandKind::RemoveMarket => "REMOVE_MARKET",
        };
        write!(f, "{s}")
    }
}