use crate::{
book::protocol::{
command::{Persistence, Side},
reject::RejectReason,
},
types::*,
};
use std::collections::VecDeque;
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
strum::Display,
strum::AsRefStr,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum BookMarketState {
Open,
Suspended,
Halted,
Closed,
Deactivated,
}
impl BookMarketState {
pub fn is_matchable(self) -> bool {
matches!(self, Self::Open)
}
pub fn is_terminal(self) -> bool {
matches!(self, Self::Closed)
}
pub fn is_halted(self) -> bool {
matches!(self, Self::Halted)
}
pub fn supports_initial_phase_for(
self,
market_kind: MarketKind,
market_phase: MarketPhase,
) -> bool {
if self.is_terminal() {
return false;
}
if market_kind == MarketKind::InPlayCapable && market_phase == MarketPhase::PreAwaitLive {
return matches!(self, Self::Open | Self::Suspended | Self::Deactivated);
}
market_kind.supports_phase(market_phase)
}
}
#[track_caller]
pub fn assert_kind_supports_phase(market_kind: MarketKind, market_phase: MarketPhase) {
assert!(
market_kind.supports_phase(market_phase),
"market kind {market_kind:?} does not support phase {market_phase:?}"
);
}
pub fn ensure_phase_command_allowed_state(state: BookMarketState) -> Result<(), RejectReason> {
match state {
BookMarketState::Open
| BookMarketState::Suspended
| BookMarketState::Halted
| BookMarketState::Deactivated => Ok(()),
BookMarketState::Closed => Err(RejectReason::MarketTerminal),
}
}
pub fn ensure_can_accept_new_orders(state: BookMarketState) -> Result<(), RejectReason> {
if state.is_matchable() {
Ok(())
} else {
Err(RejectReason::MarketNotOpen)
}
}
pub fn ensure_state_change(
current: BookMarketState,
to: BookMarketState,
) -> Result<(), crate::book::protocol::reject::RejectReason> {
if current == to {
return Err(crate::book::protocol::reject::RejectReason::NoChange);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{BookMarketState, ensure_phase_command_allowed_state};
use crate::{
book::protocol::reject::RejectReason,
types::{MarketKind, MarketPhase},
};
#[test]
fn phase_command_allows_pre_close_states() {
for state in [
BookMarketState::Open,
BookMarketState::Suspended,
BookMarketState::Halted,
BookMarketState::Deactivated,
] {
assert_eq!(ensure_phase_command_allowed_state(state), Ok(()));
}
}
#[test]
fn phase_command_rejects_terminal_states() {
assert_eq!(
ensure_phase_command_allowed_state(BookMarketState::Closed),
Err(RejectReason::MarketTerminal)
);
}
#[test]
fn open_or_suspended_in_play_capable_market_may_start_in_pre_await_live() {
assert!(
BookMarketState::Suspended
.supports_initial_phase_for(MarketKind::InPlayCapable, MarketPhase::PreAwaitLive)
);
assert!(
BookMarketState::Deactivated
.supports_initial_phase_for(MarketKind::InPlayCapable, MarketPhase::PreAwaitLive)
);
assert!(
BookMarketState::Open
.supports_initial_phase_for(MarketKind::InPlayCapable, MarketPhase::PreAwaitLive)
);
assert!(
!BookMarketState::Closed
.supports_initial_phase_for(MarketKind::InPlayCapable, MarketPhase::PreAwaitLive)
);
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BatchMode {
FilteredCancel,
Close,
SuspendLapse,
InPlayLapse,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub enum BatchProcessTarget {
AllLiveOrders,
LapseOrders,
Filtered {
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>,
},
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub enum BatchProcessContext {
None,
Close { total_live_orders: u64 },
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub struct BatchProcessState {
pub batch_mode: BatchMode,
pub batch_max_events: u16,
pub cursor_after: Option<OrderId>,
pub processed_total: u64,
pub chunks_done: u32,
pub target: BatchProcessTarget,
pub detail: Option<String>,
pub context: BatchProcessContext,
}
impl BatchProcessState {
pub fn close(batch_max_events: u16, total_live_orders: u64) -> Self {
Self {
batch_mode: BatchMode::Close,
batch_max_events,
cursor_after: None,
processed_total: 0,
chunks_done: 0,
target: BatchProcessTarget::AllLiveOrders,
detail: None,
context: BatchProcessContext::Close { total_live_orders },
}
}
#[allow(clippy::too_many_arguments)]
pub fn cancel(
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>,
detail: String,
) -> Self {
Self {
batch_mode: BatchMode::FilteredCancel,
batch_max_events,
cursor_after: None,
processed_total: 0,
chunks_done: 0,
target: BatchProcessTarget::Filtered {
started_at_ms,
from_created_at_inclusive_ms,
to_created_at_inclusive_ms,
account_filter,
runner_filter,
},
detail: Some(detail),
context: BatchProcessContext::None,
}
}
pub fn lapse(batch_max_events: u16, batch_mode: BatchMode) -> Self {
debug_assert!(matches!(
batch_mode,
BatchMode::SuspendLapse | BatchMode::InPlayLapse
));
Self {
batch_mode,
batch_max_events,
cursor_after: None,
processed_total: 0,
chunks_done: 0,
target: BatchProcessTarget::LapseOrders,
detail: None,
context: BatchProcessContext::None,
}
}
#[inline]
pub fn is_close(&self) -> bool {
self.batch_mode == BatchMode::Close
}
#[inline]
pub fn retarget(
&mut self,
batch_mode: BatchMode,
batch_max_events: u16,
target: BatchProcessTarget,
detail: Option<String>,
context: BatchProcessContext,
) {
self.batch_mode = batch_mode;
self.batch_max_events = batch_max_events;
self.cursor_after = None;
self.processed_total = 0;
self.chunks_done = 0;
self.target = target;
self.detail = detail;
self.context = context;
}
pub fn record_chunk(&mut self, cursor_after: Option<OrderId>, processed_count: u64) {
self.cursor_after = cursor_after;
self.processed_total = self.processed_total.saturating_add(processed_count);
self.chunks_done = self.chunks_done.saturating_add(1);
}
}
#[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,
}
#[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)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookOrderInfo {
pub order_id: OrderId,
pub account_id: AccountId,
pub correlation_id: Option<CorrelationId>,
pub side: Side,
pub state: BookOrderState,
pub created_at: DateTime,
pub last_updated_at: DateTime,
}
#[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 {
pub fn remaining(&self) -> Money {
Money(self.stake.0.saturating_sub(self.matched.0).max(0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PriceSize {
pub price: OddsX10000,
pub size: Money,
}
#[derive(Debug, Clone, Default)]
pub struct RunnerPrices {
pub runner_id: RunnerId,
pub available_to_back: Vec<PriceSize>,
pub available_to_lay: Vec<PriceSize>,
}
#[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,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BinaryDepth {
pub max_price_ticks: u16,
pub bids: Vec<BinaryPriceSize>,
pub asks: Vec<BinaryPriceSize>,
}
#[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()
}
}