use betex::{
book::close_process::DEFAULT_CLOSE_BATCH_EVENTS,
book::protocol::command::{Command, CommandKind, MarketState, Persistence, Side, TimeInForce},
book::{BatchMode, Book, BookEvent, BookMarketState, CancelCause},
types::{AccountId, CorrelationId, MarketId, MarketPhase, Money, OddsX10000, RunnerId},
};
fn exec(book: &mut Book, cmd: Command) -> Vec<betex::book::BookEventEnvelope> {
let (events, _) = book.handle(&cmd).expect("command should succeed");
book.apply_all_events(&events);
events
}
fn place_order_cmd(
market_id: MarketId,
correlation_id: u64,
account_id: u64,
runner_id: u32,
persistence: Persistence,
) -> Command {
Command {
correlation_id: Some(CorrelationId(correlation_id.to_string())),
metadata: None,
market_id,
kind: CommandKind::PlaceOrder {
runner_id: RunnerId(runner_id),
account_id: AccountId::from(account_id),
client_order_id: None,
side: Side::Yes,
odds: OddsX10000(20000),
stake: Money(100),
persistence,
time_in_force: TimeInForce::Gtc,
},
}
}
fn close_cmd(market_id: MarketId) -> Command {
Command {
correlation_id: Some(CorrelationId(1001.to_string())),
metadata: None,
market_id,
kind: CommandKind::CloseMarket {
reason: "TEST_CLOSE".to_string(),
},
}
}
fn suspend_cmd(market_id: MarketId) -> Command {
Command {
correlation_id: Some(CorrelationId(2001.to_string())),
metadata: None,
market_id,
kind: CommandKind::SetMarketState {
state: MarketState::Suspended,
},
}
}
fn go_live_cmd(market_id: MarketId) -> Command {
Command {
correlation_id: Some(CorrelationId(2003.to_string())),
metadata: None,
market_id,
kind: CommandKind::GoLiveMarket,
}
}
fn continue_lapse_cmd(market_id: MarketId) -> Command {
Command {
correlation_id: Some(CorrelationId(2002.to_string())),
metadata: None,
market_id,
kind: CommandKind::ContinueBatchProcess,
}
}
fn batch_cancel_cmd(
market_id: MarketId,
reason: &str,
metadata: Option<serde_json::Value>,
) -> Command {
Command {
correlation_id: Some(CorrelationId(4001.to_string())),
metadata,
market_id,
kind: CommandKind::BatchCancelOrders {
from_created_at_inclusive: None,
to_created_at_inclusive: None,
account_id: None,
runner_id: None,
reason: reason.to_string(),
},
}
}
fn continue_batch_cancel_cmd(market_id: MarketId) -> Command {
Command {
correlation_id: Some(CorrelationId(4002.to_string())),
metadata: None,
market_id,
kind: CommandKind::ContinueBatchProcess,
}
}
#[test]
fn completion_event_emitted_for_each_batch_cause_even_without_orders() {
let mid_close = MarketId(301);
let mut close_book = Book::new_multi_runner(mid_close, [RunnerId(1), RunnerId(2), RunnerId(3)]);
close_book.set_close_batch_max_events(2);
let close_events = exec(&mut close_book, close_cmd(mid_close));
assert!(close_events.iter().any(|e| {
matches!(
e.event,
BookEvent::BatchProcessCompleted {
batch_mode: BatchMode::Close
}
)
}));
let mid_batch = MarketId(302);
let mut batch_book = Book::new_multi_runner(mid_batch, [RunnerId(1), RunnerId(2), RunnerId(3)]);
batch_book.set_close_batch_max_events(10);
let batch_events = exec(
&mut batch_book,
batch_cancel_cmd(mid_batch, "EMPTY_BATCH", None),
);
assert!(batch_events.iter().any(|e| {
matches!(
e.event,
BookEvent::BatchProcessCompleted {
batch_mode: BatchMode::FilteredCancel
}
)
}));
let mid_suspend = MarketId(303);
let mut suspend_book =
Book::new_multi_runner(mid_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let suspend_events = exec(&mut suspend_book, suspend_cmd(mid_suspend));
assert!(suspend_events.iter().any(|e| {
matches!(
e.event,
BookEvent::BatchProcessCompleted {
batch_mode: BatchMode::SuspendLapse
}
)
}));
assert_eq!(suspend_book.market_state(), BookMarketState::Suspended);
let mid_in_play = MarketId(304);
let mut in_play_book =
Book::new_multi_runner(mid_in_play, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let in_play_events = exec(&mut in_play_book, go_live_cmd(mid_in_play));
assert!(in_play_events.iter().any(|e| {
matches!(
e.event,
BookEvent::BatchProcessCompleted {
batch_mode: BatchMode::InPlayLapse
}
)
}));
assert_eq!(in_play_book.market_state(), BookMarketState::Open);
assert_eq!(in_play_book.market_phase(), MarketPhase::Live);
}
#[test]
fn completion_event_emitted_once_for_multi_batch_lapse() {
let mid_suspend = MarketId(305);
let mut suspend_book =
Book::new_multi_runner(mid_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let total = DEFAULT_CLOSE_BATCH_EVENTS as u64 + 3;
for i in 0..total {
let _ = exec(
&mut suspend_book,
place_order_cmd(mid_suspend, i + 1, 50_000 + i, 1, Persistence::Lapse),
);
}
let mut suspend_all = Vec::new();
suspend_all.extend(exec(&mut suspend_book, suspend_cmd(mid_suspend)));
while suspend_book.batch_process_state().is_some() {
suspend_all.extend(exec(&mut suspend_book, continue_lapse_cmd(mid_suspend)));
}
let suspend_completed = suspend_all
.iter()
.filter(|e| {
matches!(
e.event,
BookEvent::BatchProcessCompleted {
batch_mode: BatchMode::SuspendLapse
}
)
})
.count();
assert_eq!(suspend_completed, 1);
let mid_in_play = MarketId(306);
let mut in_play_book =
Book::new_multi_runner(mid_in_play, [RunnerId(1), RunnerId(2), RunnerId(3)]);
for i in 0..total {
let _ = exec(
&mut in_play_book,
place_order_cmd(mid_in_play, i + 1, 55_000 + i, 1, Persistence::Lapse),
);
}
let mut in_play_all = Vec::new();
in_play_all.extend(exec(&mut in_play_book, go_live_cmd(mid_in_play)));
while in_play_book.batch_process_state().is_some() {
in_play_all.extend(exec(&mut in_play_book, continue_lapse_cmd(mid_in_play)));
}
let in_play_completed = in_play_all
.iter()
.filter(|e| {
matches!(
e.event,
BookEvent::BatchProcessCompleted {
batch_mode: BatchMode::InPlayLapse
}
)
})
.count();
assert_eq!(in_play_completed, 1);
}
#[test]
fn batch_cancel_recovery_does_not_reapply_initial_metadata() {
let market_id = MarketId(308);
let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
for i in 0..3 {
let _ = exec(
&mut book,
place_order_cmd(market_id, i + 1, 70_000 + i, 1, Persistence::Persist),
);
}
let metadata = serde_json::json!({"trace":"recovery-meta"});
book.set_close_batch_max_events(2);
let start = exec(
&mut book,
batch_cancel_cmd(market_id, "RECOVERY_BATCH", Some(metadata.clone())),
);
assert!(matches!(
start.first().map(|e| &e.event),
Some(BookEvent::BatchProcessStarted { .. })
));
assert_eq!(
start.first().and_then(|e| e.metadata.clone()),
Some(metadata)
);
let mut restarted = book.clone();
while restarted.batch_process_state().is_some() {
let events = exec(&mut restarted, continue_batch_cancel_cmd(market_id));
assert!(events.iter().all(|env| env.metadata.is_none()));
}
}
#[test]
fn cancel_cause_and_detail_distinguish_sources() {
let mid_user = MarketId(308);
let mut user_book = Book::new_multi_runner(mid_user, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let placed = exec(
&mut user_book,
place_order_cmd(mid_user, 1, 80_000, 1, Persistence::Persist),
);
let order_id = match placed.first().map(|e| &e.event) {
Some(BookEvent::OrderAccepted { order_id, .. }) => *order_id,
_ => panic!("expected OrderAccepted"),
};
let user_cancel = exec(
&mut user_book,
Command {
correlation_id: Some(CorrelationId(2.to_string())),
metadata: None,
market_id: mid_user,
kind: CommandKind::CancelOrder {
account_id: AccountId::from(80_000),
order_id,
},
},
);
assert!(user_cancel.iter().any(|e| {
matches!(
&e.event,
BookEvent::OrderCancelled {
cancel_cause: CancelCause::UserCancel,
cause_detail: Some(detail),
..
} if detail == "USER_CANCEL"
)
}));
let mid_batch = MarketId(309);
let mut batch_book = Book::new_multi_runner(mid_batch, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let _ = exec(
&mut batch_book,
place_order_cmd(mid_batch, 1, 81_000, 1, Persistence::Persist),
);
batch_book.set_close_batch_max_events(10);
let batch_cancel = exec(
&mut batch_book,
batch_cancel_cmd(mid_batch, "CAUSE_BATCH", None),
);
assert!(batch_cancel.iter().any(|e| {
matches!(
&e.event,
BookEvent::OrderCancelledBatched {
batch_mode: BatchMode::FilteredCancel,
detail: Some(detail),
..
} if detail == "CAUSE_BATCH"
)
}));
let mid_close = MarketId(310);
let mut close_book = Book::new_multi_runner(mid_close, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let _ = exec(
&mut close_book,
place_order_cmd(mid_close, 1, 82_000, 1, Persistence::Persist),
);
close_book.set_close_batch_max_events(10);
let close_events = exec(&mut close_book, close_cmd(mid_close));
assert!(close_events.iter().any(|e| {
matches!(
&e.event,
BookEvent::OrderCancelledBatched {
batch_mode: BatchMode::Close,
detail: None,
..
}
)
}));
let mid_suspend = MarketId(311);
let mut suspend_book =
Book::new_multi_runner(mid_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let _ = exec(
&mut suspend_book,
place_order_cmd(mid_suspend, 1, 83_000, 1, Persistence::Lapse),
);
let suspend_events = exec(&mut suspend_book, suspend_cmd(mid_suspend));
assert!(suspend_events.iter().any(|e| {
matches!(
&e.event,
BookEvent::OrderCancelledBatched {
batch_mode: BatchMode::SuspendLapse,
detail: None,
..
}
)
}));
}