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,
RunnerRemovalCancel,
}
#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub enum BatchProcessTarget {
AllLiveOrders,
LapseOrders,
RunnerRemoval {
runner_ids: Vec<RunnerId>,
},
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(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum BatchProcessTargetWire {
AllLiveOrders {},
LapseOrders {},
RunnerRemoval {
runner_ids: Vec<RunnerId>,
},
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>,
},
}
impl From<&BatchProcessTarget> for BatchProcessTargetWire {
fn from(target: &BatchProcessTarget) -> Self {
match target {
BatchProcessTarget::AllLiveOrders => Self::AllLiveOrders {},
BatchProcessTarget::LapseOrders => Self::LapseOrders {},
BatchProcessTarget::RunnerRemoval { runner_ids } => Self::RunnerRemoval {
runner_ids: runner_ids.clone(),
},
BatchProcessTarget::Filtered {
started_at_ms,
from_created_at_inclusive_ms,
to_created_at_inclusive_ms,
account_filter,
runner_filter,
} => Self::Filtered {
started_at_ms: *started_at_ms,
from_created_at_inclusive_ms: *from_created_at_inclusive_ms,
to_created_at_inclusive_ms: *to_created_at_inclusive_ms,
account_filter: account_filter.clone(),
runner_filter: *runner_filter,
},
}
}
}
impl From<BatchProcessTargetWire> for BatchProcessTarget {
fn from(target: BatchProcessTargetWire) -> Self {
match target {
BatchProcessTargetWire::AllLiveOrders {} => Self::AllLiveOrders,
BatchProcessTargetWire::LapseOrders {} => Self::LapseOrders,
BatchProcessTargetWire::RunnerRemoval { runner_ids } => {
Self::RunnerRemoval { runner_ids }
}
BatchProcessTargetWire::Filtered {
started_at_ms,
from_created_at_inclusive_ms,
to_created_at_inclusive_ms,
account_filter,
runner_filter,
} => Self::Filtered {
started_at_ms,
from_created_at_inclusive_ms,
to_created_at_inclusive_ms,
account_filter,
runner_filter,
},
}
}
}
impl serde::Serialize for BatchProcessTarget {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde::Serialize::serialize(&BatchProcessTargetWire::from(self), serializer)
}
}
impl<'de> serde::Deserialize<'de> for BatchProcessTarget {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
<BatchProcessTargetWire as serde::Deserialize>::deserialize(deserializer).map(Self::from)
}
}
impl BatchProcessTarget {
#[inline]
pub fn is_valid_for_mode(&self, batch_mode: BatchMode) -> bool {
matches!(
(batch_mode, self),
(BatchMode::Close, BatchProcessTarget::AllLiveOrders)
| (
BatchMode::SuspendLapse | BatchMode::InPlayLapse,
BatchProcessTarget::LapseOrders
)
| (
BatchMode::RunnerRemovalCancel,
BatchProcessTarget::RunnerRemoval { .. }
)
| (
BatchMode::FilteredCancel,
BatchProcessTarget::Filtered { .. }
)
)
}
#[inline]
pub fn assert_valid_for_mode(&self, batch_mode: BatchMode) {
assert!(
self.is_valid_for_mode(batch_mode),
"batch target {:?} is incompatible with batch mode {:?}",
self,
batch_mode
);
}
}
#[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 BatchProcessDescriptor {
pub batch_mode: BatchMode,
pub batch_max_events: u16,
pub target: BatchProcessTarget,
pub detail: Option<String>,
}
impl BatchProcessDescriptor {
pub fn new(
batch_mode: BatchMode,
batch_max_events: u16,
target: BatchProcessTarget,
detail: Option<String>,
) -> Self {
target.assert_valid_for_mode(batch_mode);
Self {
batch_mode,
batch_max_events,
target,
detail,
}
}
}
#[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 runner_removal(batch_max_events: u16, runner_ids: Vec<RunnerId>) -> Self {
Self {
batch_mode: BatchMode::RunnerRemovalCancel,
batch_max_events,
cursor_after: None,
processed_total: 0,
chunks_done: 0,
target: BatchProcessTarget::RunnerRemoval { runner_ids },
detail: None,
context: BatchProcessContext::None,
}
}
pub fn lapse(batch_max_events: u16, batch_mode: BatchMode) -> Self {
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,
) {
target.assert_valid_for_mode(batch_mode);
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()
}
}