mod binary_yes;
pub mod common;
mod error;
mod multi_runner;
pub mod protocol;
mod two_runner;
use crate::types::*;
pub use binary_yes::BinaryYesBook;
pub use common::*;
pub use error::BookError;
pub use multi_runner::MultiRunnerBook;
use protocol::command::Command;
pub use protocol::command::{Persistence, TimeInForce};
pub use two_runner::TwoRunnerBook;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Book {
TwoRunner(Box<TwoRunnerBook>),
MultiRunner(Box<MultiRunnerBook>),
BinaryYes(Box<BinaryYesBook>),
}
impl Book {
const DEFAULT_ORDER_STORE_CAPACITY: usize = 20_000;
pub fn new(market_id: MarketId, runner_ids: impl IntoIterator<Item = RunnerId>) -> Self {
Self::new_with_kind(market_id, MarketKind::InPlayCapable, runner_ids)
}
pub fn new_with_kind(
market_id: MarketId,
market_kind: MarketKind,
runner_ids: impl IntoIterator<Item = RunnerId>,
) -> Self {
let runners: Vec<RunnerId> = runner_ids.into_iter().collect();
match runners.len() {
0 | 1 => panic!("Book requires at least 2 runners"),
2 => Book::TwoRunner(Box::new(TwoRunnerBook::new_with_capacity(
market_id,
market_kind,
runners[0],
runners[1],
Self::DEFAULT_ORDER_STORE_CAPACITY,
))),
_ => Book::MultiRunner(Box::new(MultiRunnerBook::new_with_capacity(
market_id,
market_kind,
runners,
Self::DEFAULT_ORDER_STORE_CAPACITY,
))),
}
}
pub fn new_engine(market_id: MarketId) -> Self {
Self::new_engine_with_kind(market_id, MarketKind::InPlayCapable)
}
pub fn new_engine_with_kind(market_id: MarketId, market_kind: MarketKind) -> Self {
Book::MultiRunner(Box::new(MultiRunnerBook::new_engine_with_capacity(
market_id,
market_kind,
Self::DEFAULT_ORDER_STORE_CAPACITY,
)))
}
pub fn new_engine_with_capacity(market_id: MarketId, order_store_capacity: usize) -> Self {
Self::new_engine_with_kind_and_capacity(
market_id,
MarketKind::InPlayCapable,
order_store_capacity,
)
}
pub fn new_engine_with_kind_and_capacity(
market_id: MarketId,
market_kind: MarketKind,
order_store_capacity: usize,
) -> Self {
Book::MultiRunner(Box::new(MultiRunnerBook::new_engine_with_capacity(
market_id,
market_kind,
order_store_capacity,
)))
}
pub fn new_two_runner(market_id: MarketId, runner_a: RunnerId, runner_b: RunnerId) -> Self {
Self::new_two_runner_with_kind(market_id, MarketKind::InPlayCapable, runner_a, runner_b)
}
pub fn new_two_runner_with_kind(
market_id: MarketId,
market_kind: MarketKind,
runner_a: RunnerId,
runner_b: RunnerId,
) -> Self {
Book::TwoRunner(Box::new(TwoRunnerBook::new_with_capacity(
market_id,
market_kind,
runner_a,
runner_b,
Self::DEFAULT_ORDER_STORE_CAPACITY,
)))
}
pub fn new_two_runner_with_capacity(
market_id: MarketId,
runner_a: RunnerId,
runner_b: RunnerId,
order_store_capacity: usize,
) -> Self {
Self::new_two_runner_with_kind_and_capacity(
market_id,
MarketKind::InPlayCapable,
runner_a,
runner_b,
order_store_capacity,
)
}
pub fn new_two_runner_with_kind_and_capacity(
market_id: MarketId,
market_kind: MarketKind,
runner_a: RunnerId,
runner_b: RunnerId,
order_store_capacity: usize,
) -> Self {
Book::TwoRunner(Box::new(TwoRunnerBook::new_with_capacity(
market_id,
market_kind,
runner_a,
runner_b,
order_store_capacity,
)))
}
pub fn new_multi_runner(
market_id: MarketId,
runner_ids: impl IntoIterator<Item = RunnerId>,
) -> Self {
Self::new_multi_runner_with_kind(market_id, MarketKind::InPlayCapable, runner_ids)
}
pub fn new_multi_runner_with_kind(
market_id: MarketId,
market_kind: MarketKind,
runner_ids: impl IntoIterator<Item = RunnerId>,
) -> Self {
Book::MultiRunner(Box::new(MultiRunnerBook::new_with_capacity(
market_id,
market_kind,
runner_ids,
Self::DEFAULT_ORDER_STORE_CAPACITY,
)))
}
pub fn new_multi_runner_with_capacity(
market_id: MarketId,
runner_ids: impl IntoIterator<Item = RunnerId>,
order_store_capacity: usize,
) -> Self {
Self::new_multi_runner_with_kind_and_capacity(
market_id,
MarketKind::InPlayCapable,
runner_ids,
order_store_capacity,
)
}
pub fn new_multi_runner_with_kind_and_capacity(
market_id: MarketId,
market_kind: MarketKind,
runner_ids: impl IntoIterator<Item = RunnerId>,
order_store_capacity: usize,
) -> Self {
Book::MultiRunner(Box::new(MultiRunnerBook::new_with_capacity(
market_id,
market_kind,
runner_ids,
order_store_capacity,
)))
}
pub fn new_binary_yes_with_kind_and_capacity(
market_id: MarketId,
market_kind: MarketKind,
yes_runner_id: RunnerId,
no_runner_id: RunnerId,
max_price_ticks: u16,
order_store_capacity: usize,
) -> Self {
Book::BinaryYes(Box::new(BinaryYesBook::new_with_capacity(
market_id,
market_kind,
yes_runner_id,
no_runner_id,
max_price_ticks,
order_store_capacity,
)))
}
pub fn new_binary_yes_with_kind(
market_id: MarketId,
market_kind: MarketKind,
yes_runner_id: RunnerId,
no_runner_id: RunnerId,
max_price_ticks: u16,
) -> Self {
Self::new_binary_yes_with_kind_and_capacity(
market_id,
market_kind,
yes_runner_id,
no_runner_id,
max_price_ticks,
Self::DEFAULT_ORDER_STORE_CAPACITY,
)
}
pub fn new_binary_yes(
market_id: MarketId,
yes_runner_id: RunnerId,
no_runner_id: RunnerId,
max_price_ticks: u16,
) -> Self {
Self::new_binary_yes_with_kind(
market_id,
MarketKind::InPlayCapable,
yes_runner_id,
no_runner_id,
max_price_ticks,
)
}
pub fn market_id(&self) -> MarketId {
match self {
Book::TwoRunner(b) => b.market_id(),
Book::MultiRunner(b) => b.market_id(),
Book::BinaryYes(b) => b.market_id(),
}
}
pub fn market_model(&self) -> MarketModel {
match self {
Book::TwoRunner(_) | Book::MultiRunner(_) => MarketModel::ExchangeOdds,
Book::BinaryYes(b) => MarketModel::BinaryYes {
max_price_ticks: b.max_price_ticks(),
},
}
}
pub fn binary_depth(&self, depth: usize) -> Option<BinaryDepth> {
match self {
Book::BinaryYes(b) => Some(b.depth(depth)),
_ => None,
}
}
pub fn market_state(&self) -> BookMarketState {
match self {
Book::TwoRunner(b) => b.market_state(),
Book::MultiRunner(b) => b.market_state(),
Book::BinaryYes(b) => b.market_state(),
}
}
pub fn is_halted(&self) -> bool {
match self {
Book::TwoRunner(b) => b.is_halted(),
Book::MultiRunner(b) => b.is_halted(),
Book::BinaryYes(b) => b.is_halted(),
}
}
pub fn get_order(&self, order_id: OrderId) -> Option<&BookOrder> {
match self {
Book::TwoRunner(b) => b.get_order(order_id),
Book::MultiRunner(b) => b.get_order(order_id),
Book::BinaryYes(_) => None,
}
}
pub fn get_trade(&self, trade_id: TradeId) -> Option<&BookTrade> {
match self {
Book::TwoRunner(b) => b.get_trade(trade_id),
Book::MultiRunner(b) => b.get_trade(trade_id),
Book::BinaryYes(_) => None,
}
}
pub fn is_resting(&self, order_id: OrderId) -> bool {
match self {
Book::TwoRunner(b) => b.is_resting(order_id),
Book::MultiRunner(b) => b.is_resting(order_id),
Book::BinaryYes(b) => b.is_resting(order_id),
}
}
pub fn active_order_count(&self) -> usize {
match self {
Book::TwoRunner(b) => b.active_order_count(),
Book::MultiRunner(b) => b.active_order_count(),
Book::BinaryYes(b) => b.active_order_count(),
}
}
pub fn close_process_state(&self) -> Option<CloseProcessState> {
match self {
Book::TwoRunner(b) => b.close_process_state(),
Book::MultiRunner(b) => b.close_process_state(),
Book::BinaryYes(b) => b.close_process_state(),
}
}
pub fn runners(&self) -> Box<dyn Iterator<Item = RunnerId> + '_> {
match self {
Book::TwoRunner(b) => Box::new(b.runners()),
Book::MultiRunner(b) => Box::new(b.runners()),
Book::BinaryYes(b) => Box::new(b.runners()),
}
}
pub fn runner_prices(&self, runner_id: RunnerId, depth: usize) -> RunnerPrices {
match self {
Book::TwoRunner(b) => b.runner_prices(runner_id, depth),
Book::MultiRunner(b) => b.runner_prices(runner_id, depth),
Book::BinaryYes(b) => b.runner_prices(runner_id, depth),
}
}
pub fn best_back_price(&self, runner_id: RunnerId) -> Option<PriceSize> {
match self {
Book::TwoRunner(b) => b.best_back_price(runner_id),
Book::MultiRunner(b) => b.best_back_price(runner_id),
Book::BinaryYes(b) => b.best_back_price(runner_id),
}
}
pub fn best_lay_price(&self, runner_id: RunnerId) -> Option<PriceSize> {
match self {
Book::TwoRunner(b) => b.best_lay_price(runner_id),
Book::MultiRunner(b) => b.best_lay_price(runner_id),
Book::BinaryYes(b) => b.best_lay_price(runner_id),
}
}
pub fn runner_matched_volume(&self, runner_id: RunnerId) -> Money {
match self {
Book::TwoRunner(b) => b.runner_matched_volume(runner_id),
Book::MultiRunner(b) => b.runner_matched_volume(runner_id),
Book::BinaryYes(b) => b.runner_matched_volume(runner_id),
}
}
pub fn total_matched(&self) -> Money {
match self {
Book::TwoRunner(b) => b.total_matched(),
Book::MultiRunner(b) => b.total_matched(),
Book::BinaryYes(b) => b.total_matched(),
}
}
pub fn handle(
&mut self,
cmd: &Command,
) -> Result<(Vec<BookEventEnvelope>, CommandResponse), BookError> {
match self {
Book::TwoRunner(b) => {
let (events, resp) = b.handle_command(cmd)?;
Ok((events.into_vec(), resp))
}
Book::MultiRunner(b) => b.handle_command(cmd),
Book::BinaryYes(b) => b.handle_command(cmd),
}
}
pub fn apply_event(&mut self, env: &BookEventEnvelope) {
debug_assert_eq!(
env.market_id,
self.market_id(),
"event market_id mismatch: expected {:?}, got {:?}",
self.market_id(),
env.market_id
);
match self {
Book::TwoRunner(b) => b.apply_event(env),
Book::MultiRunner(b) => b.apply_event(env),
Book::BinaryYes(b) => b.apply_event(env),
}
}
pub fn apply_all_events(&mut self, envs: &[BookEventEnvelope]) {
for env in envs {
self.apply_event(env);
}
}
}
#[cfg(test)]
mod tests {
use super::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce};
use super::*;
use crate::types::CorrelationId;
#[allow(clippy::too_many_arguments)]
fn place_cmd(
market_id: MarketId,
correlation_id: u64,
account_id: AccountId,
runner_id: RunnerId,
side: Side,
odds: OddsX10000,
stake: Money,
) -> Command {
Command {
correlation_id: CorrelationId(correlation_id),
market_id,
kind: CommandKind::PlaceOrder {
runner_id,
account_id,
client_order_id: None,
side,
odds,
stake,
persistence: Persistence::Persist,
time_in_force: TimeInForce::Gtc,
},
}
}
#[test]
fn test_book_selects_two_runner() {
let market_id = MarketId(1);
let book = Book::new(market_id, [RunnerId(1), RunnerId(2)]);
assert!(matches!(book, Book::TwoRunner(_)));
}
#[test]
fn test_book_selects_multi_runner() {
let market_id = MarketId(1);
let book = Book::new(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
assert!(matches!(book, Book::MultiRunner(_)));
}
#[test]
#[should_panic(expected = "Book requires at least 2 runners")]
fn test_book_panics_on_single_runner() {
let market_id = MarketId(1);
let _ = Book::new(market_id, [RunnerId(1)]);
}
#[test]
fn test_two_runner_implied_matching() {
let market_id = MarketId(1);
let mut book = Book::new(market_id, [RunnerId(1), RunnerId(2)]);
let account1 = AccountId(100);
let account2 = AccountId(200);
let cmd1 = place_cmd(
market_id,
1,
account1,
RunnerId(2),
Side::Yes,
OddsX10000(20000),
Money(1000),
);
let (events1, _) = book.handle(&cmd1).expect("should succeed");
book.apply_all_events(&events1);
let cmd2 = place_cmd(
market_id,
2,
account2,
RunnerId(1),
Side::Yes,
OddsX10000(20000),
Money(1000),
);
let (events, _) = book.handle(&cmd2).expect("should succeed");
book.apply_all_events(&events);
let trade_matched = events
.iter()
.any(|e| matches!(&e.event, BookEvent::TradeMatched { .. }));
assert!(trade_matched, "Expected implied match in two-runner book");
}
#[test]
fn test_multi_runner_no_implied_matching() {
let market_id = MarketId(1);
let mut book = Book::new(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let account1 = AccountId(100);
let account2 = AccountId(200);
let cmd1 = place_cmd(
market_id,
1,
account1,
RunnerId(2),
Side::Yes,
OddsX10000(20000),
Money(1000),
);
let (events1, _) = book.handle(&cmd1).expect("should succeed");
book.apply_all_events(&events1);
let cmd2 = place_cmd(
market_id,
2,
account2,
RunnerId(1),
Side::Yes,
OddsX10000(20000),
Money(1000),
);
let (events, _) = book.handle(&cmd2).expect("should succeed");
book.apply_all_events(&events);
let trade_matched = events
.iter()
.any(|e| matches!(&e.event, BookEvent::TradeMatched { .. }));
assert!(
!trade_matched,
"Multi-runner book should not do implied matching"
);
}
#[test]
fn test_direct_matching_works_in_both() {
for runner_count in [2, 3] {
let market_id = MarketId(1);
let runners: Vec<RunnerId> = (1..=runner_count).map(|i| RunnerId(i as u32)).collect();
let mut book = Book::new(market_id, runners);
let account1 = AccountId(100);
let account2 = AccountId(200);
let cmd1 = place_cmd(
market_id,
1,
account1,
RunnerId(1),
Side::No,
OddsX10000(20000),
Money(1000),
);
let (events1, _) = book.handle(&cmd1).expect("should succeed");
book.apply_all_events(&events1);
let cmd2 = place_cmd(
market_id,
2,
account2,
RunnerId(1),
Side::Yes,
OddsX10000(20000),
Money(1000),
);
let (events, _) = book.handle(&cmd2).expect("should succeed");
book.apply_all_events(&events);
let trade_matched = events
.iter()
.any(|e| matches!(&e.event, BookEvent::TradeMatched { .. }));
assert!(
trade_matched,
"Direct matching should work with {} runners",
runner_count
);
}
}
}