use super::error::JournalError;
use super::journal::Journal;
use super::types::{SequencerCommand, SequencerEvent, SequencerResult};
use crate::orderbook::clock::Clock;
use crate::orderbook::fees::FeeSchedule;
use crate::orderbook::stp::STPMode;
use crate::orderbook::{OrderBook, OrderBookError, OrderBookSnapshot};
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, Default)]
pub struct ReplayBookConfig {
pub fee_schedule: Option<FeeSchedule>,
pub stp_mode: STPMode,
pub tick_size: Option<u128>,
pub lot_size: Option<u64>,
pub min_order_size: Option<u64>,
pub max_order_size: Option<u64>,
}
impl ReplayBookConfig {
#[must_use]
pub fn new(
fee_schedule: Option<FeeSchedule>,
stp_mode: STPMode,
tick_size: Option<u128>,
lot_size: Option<u64>,
min_order_size: Option<u64>,
max_order_size: Option<u64>,
) -> Self {
Self {
fee_schedule,
stp_mode,
tick_size,
lot_size,
min_order_size,
max_order_size,
}
}
fn apply_to<T>(&self, book: &mut OrderBook<T>)
where
T: Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + Default + 'static,
{
book.set_fee_schedule(self.fee_schedule);
book.set_stp_mode(self.stp_mode);
book.set_tick_size_opt(self.tick_size);
book.set_lot_size_opt(self.lot_size);
if let Some(min) = self.min_order_size {
book.set_min_order_size(min);
}
if let Some(max) = self.max_order_size {
book.set_max_order_size(max);
}
}
}
#[derive(Debug, Error)]
pub enum ReplayError {
#[error("journal is empty — nothing to replay")]
EmptyJournal,
#[error("invalid from_sequence {from_sequence}: journal last sequence is {last_sequence}")]
InvalidSequence {
from_sequence: u64,
last_sequence: u64,
},
#[error("sequence gap detected: expected {expected}, found {found}")]
SequenceGap {
expected: u64,
found: u64,
},
#[error("replay sequence counter overflowed u64 at sequence {at}")]
SequenceOverflow {
at: u64,
},
#[error("order book error during replay at sequence {sequence_num}: {source}")]
OrderBookError {
sequence_num: u64,
#[source]
source: OrderBookError,
},
#[error("snapshot mismatch: replayed state diverges from expected snapshot")]
SnapshotMismatch,
#[error("journal error during replay: {0}")]
JournalError(#[from] JournalError),
}
pub struct ReplayEngine<T> {
_phantom: PhantomData<T>,
}
impl<T> ReplayEngine<T>
where
T: Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + Default + 'static,
{
#[must_use = "replay result carries the reconstructed book and the last applied sequence"]
pub fn replay_from(
journal: &impl Journal<T>,
from_sequence: u64,
symbol: &str,
) -> Result<(OrderBook<T>, u64), ReplayError> {
Self::replay_from_with_progress(journal, from_sequence, symbol, |_, _| {})
}
#[must_use = "replay result carries the reconstructed book and the last applied sequence"]
pub fn replay_from_with_progress(
journal: &impl Journal<T>,
from_sequence: u64,
symbol: &str,
progress: impl Fn(u64, u64),
) -> Result<(OrderBook<T>, u64), ReplayError> {
let last_seq = match journal.last_sequence() {
Some(seq) => seq,
None => return Err(ReplayError::EmptyJournal),
};
if from_sequence > last_seq {
return Err(ReplayError::InvalidSequence {
from_sequence,
last_sequence: last_seq,
});
}
let book = OrderBook::new(symbol);
let last_applied_seq = Self::replay_into(&book, journal, from_sequence, progress)?;
Ok((book, last_applied_seq))
}
#[must_use = "replay result carries the reconstructed book and the last applied sequence"]
pub fn replay_from_with_config(
journal: &impl Journal<T>,
from_sequence: u64,
symbol: &str,
config: &ReplayBookConfig,
) -> Result<(OrderBook<T>, u64), ReplayError> {
let last_seq = match journal.last_sequence() {
Some(seq) => seq,
None => return Err(ReplayError::EmptyJournal),
};
if from_sequence > last_seq {
return Err(ReplayError::InvalidSequence {
from_sequence,
last_sequence: last_seq,
});
}
let mut book = OrderBook::new(symbol);
config.apply_to(&mut book);
let last_applied_seq = Self::replay_into(&book, journal, from_sequence, |_, _| {})?;
Ok((book, last_applied_seq))
}
#[must_use = "replay result carries the reconstructed book and the last applied sequence"]
pub fn replay_from_with_clock(
journal: &impl Journal<T>,
from_sequence: u64,
symbol: &str,
clock: Arc<dyn Clock>,
) -> Result<(OrderBook<T>, u64), ReplayError> {
Self::replay_from_with_clock_and_progress(journal, from_sequence, symbol, clock, |_, _| {})
}
#[must_use = "replay result carries the reconstructed book and the last applied sequence"]
pub fn replay_from_with_clock_and_progress(
journal: &impl Journal<T>,
from_sequence: u64,
symbol: &str,
clock: Arc<dyn Clock>,
progress: impl Fn(u64, u64),
) -> Result<(OrderBook<T>, u64), ReplayError> {
let last_seq = match journal.last_sequence() {
Some(seq) => seq,
None => return Err(ReplayError::EmptyJournal),
};
if from_sequence > last_seq {
return Err(ReplayError::InvalidSequence {
from_sequence,
last_sequence: last_seq,
});
}
let book = OrderBook::with_clock(symbol, clock);
let last_applied_seq = Self::replay_into(&book, journal, from_sequence, progress)?;
Ok((book, last_applied_seq))
}
#[must_use = "replay result carries the reconstructed book and the last applied sequence"]
pub fn replay_from_with_clock_and_config(
journal: &impl Journal<T>,
from_sequence: u64,
symbol: &str,
clock: Arc<dyn Clock>,
config: &ReplayBookConfig,
) -> Result<(OrderBook<T>, u64), ReplayError> {
let last_seq = match journal.last_sequence() {
Some(seq) => seq,
None => return Err(ReplayError::EmptyJournal),
};
if from_sequence > last_seq {
return Err(ReplayError::InvalidSequence {
from_sequence,
last_sequence: last_seq,
});
}
let mut book = OrderBook::with_clock(symbol, clock);
config.apply_to(&mut book);
let last_applied_seq = Self::replay_into(&book, journal, from_sequence, |_, _| {})?;
Ok((book, last_applied_seq))
}
fn replay_into(
book: &OrderBook<T>,
journal: &impl Journal<T>,
from_sequence: u64,
progress: impl Fn(u64, u64),
) -> Result<u64, ReplayError> {
let mut last_applied_seq = 0u64;
let mut count = 0u64;
let mut expected_seq = from_sequence;
let iter = journal.read_from(from_sequence)?;
for entry_result in iter {
let entry = entry_result?;
let event = &entry.event;
if event.sequence_num != expected_seq {
return Err(ReplayError::SequenceGap {
expected: expected_seq,
found: event.sequence_num,
});
}
let applied = !matches!(event.result, SequencerResult::Rejected { .. });
Self::apply_event(book, event)?;
expected_seq = expected_seq
.checked_add(1)
.ok_or(ReplayError::SequenceOverflow { at: expected_seq })?;
if applied {
last_applied_seq = event.sequence_num;
count = count
.checked_add(1)
.ok_or(ReplayError::SequenceOverflow { at: count })?;
progress(count, last_applied_seq);
}
}
Ok(last_applied_seq)
}
pub fn verify(
journal: &impl Journal<T>,
expected_snapshot: &OrderBookSnapshot,
) -> Result<bool, ReplayError> {
let (book, _) = Self::replay_from(journal, 0, &expected_snapshot.symbol)?;
let actual = book.create_snapshot(usize::MAX);
Ok(snapshots_match(&actual, expected_snapshot))
}
fn apply_event(book: &OrderBook<T>, event: &SequencerEvent<T>) -> Result<(), ReplayError> {
if matches!(event.result, SequencerResult::Rejected { .. }) {
return Ok(());
}
match &event.command {
SequencerCommand::AddOrder(order) => {
book.add_order(order.clone())
.map_err(|e| ReplayError::OrderBookError {
sequence_num: event.sequence_num,
source: e,
})?;
}
SequencerCommand::CancelOrder(id) => {
book.cancel_order(*id)
.map_err(|e| ReplayError::OrderBookError {
sequence_num: event.sequence_num,
source: e,
})?;
}
SequencerCommand::UpdateOrder(update) => {
book.update_order(*update)
.map_err(|e| ReplayError::OrderBookError {
sequence_num: event.sequence_num,
source: e,
})?;
}
SequencerCommand::MarketOrder { id, quantity, side } => {
book.submit_market_order(*id, *quantity, *side)
.map_err(|e| ReplayError::OrderBookError {
sequence_num: event.sequence_num,
source: e,
})?;
}
SequencerCommand::MarketOrderByAmount { id, amount, side } => {
book.submit_market_order_by_amount(*id, *amount, *side)
.map_err(|e| ReplayError::OrderBookError {
sequence_num: event.sequence_num,
source: e,
})?;
}
SequencerCommand::CancelAll => {
let _ = book.cancel_all_orders();
}
SequencerCommand::CancelBySide { side } => {
let _ = book.cancel_orders_by_side(*side);
}
SequencerCommand::CancelByUser { user_id } => {
let _ = book.cancel_orders_by_user(*user_id);
}
SequencerCommand::CancelByPriceRange {
side,
min_price,
max_price,
} => {
let _ = book.cancel_orders_by_price_range(*side, *min_price, *max_price);
}
}
Ok(())
}
}
#[must_use]
pub fn snapshots_match(actual: &OrderBookSnapshot, expected: &OrderBookSnapshot) -> bool {
if actual.symbol != expected.symbol {
return false;
}
let mut actual_bids: Vec<_> = actual.bids.iter().collect();
let mut expected_bids: Vec<_> = expected.bids.iter().collect();
actual_bids.sort_by_key(|b| std::cmp::Reverse(b.price()));
expected_bids.sort_by_key(|b| std::cmp::Reverse(b.price()));
if actual_bids.len() != expected_bids.len() {
return false;
}
for (a, b) in actual_bids.iter().zip(expected_bids.iter()) {
if a.price() != b.price()
|| a.visible_quantity() != b.visible_quantity()
|| a.hidden_quantity() != b.hidden_quantity()
|| a.order_count() != b.order_count()
{
return false;
}
}
let mut actual_asks: Vec<_> = actual.asks.iter().collect();
let mut expected_asks: Vec<_> = expected.asks.iter().collect();
actual_asks.sort_by_key(|l| l.price());
expected_asks.sort_by_key(|l| l.price());
if actual_asks.len() != expected_asks.len() {
return false;
}
for (a, b) in actual_asks.iter().zip(expected_asks.iter()) {
if a.price() != b.price()
|| a.visible_quantity() != b.visible_quantity()
|| a.hidden_quantity() != b.hidden_quantity()
|| a.order_count() != b.order_count()
{
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::orderbook::clock::{MonotonicClock, StubClock};
use crate::orderbook::sequencer::InMemoryJournal;
use crate::orderbook::trade::TradeResult;
use pricelevel::{
Hash32, Id, MatchResult, OrderType, Price, Quantity, Side, TimeInForce, TimestampMs,
};
fn make_add_event(seq: u64, id: Id, price: u128, qty: u64, side: Side) -> SequencerEvent<()> {
let order = OrderType::Standard {
id,
price: Price::new(price),
quantity: Quantity::new(qty),
side,
time_in_force: TimeInForce::Gtc,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(0),
extra_fields: (),
};
SequencerEvent {
sequence_num: seq,
timestamp_ns: 0,
command: SequencerCommand::AddOrder(order),
result: SequencerResult::OrderAdded { order_id: id },
}
}
#[test]
fn test_snapshots_match_compares_hidden_quantity_and_order_count() {
fn lvl(
price: u128,
visible: u64,
hidden: u64,
count: usize,
) -> pricelevel::PriceLevelSnapshot {
serde_json::from_value(serde_json::json!({
"price": price,
"visible_quantity": visible,
"hidden_quantity": hidden,
"order_count": count,
"orders": []
}))
.expect("valid snapshot JSON")
}
let base = OrderBookSnapshot {
symbol: "TEST".to_string(),
timestamp: 0,
bids: vec![lvl(100, 10, 5, 2)],
asks: Vec::new(),
};
assert!(
snapshots_match(&base, &base.clone()),
"identical snapshots must match"
);
let diff_hidden = OrderBookSnapshot {
symbol: "TEST".to_string(),
timestamp: 0,
bids: vec![lvl(100, 10, 7, 2)],
asks: Vec::new(),
};
assert!(
!snapshots_match(&base, &diff_hidden),
"a hidden-quantity divergence must not be reported equal"
);
let diff_count = OrderBookSnapshot {
symbol: "TEST".to_string(),
timestamp: 0,
bids: vec![lvl(100, 10, 5, 3)],
asks: Vec::new(),
};
assert!(
!snapshots_match(&base, &diff_count),
"an order-count divergence must not be reported equal"
);
}
#[test]
fn test_replay_sequence_counter_overflow_is_a_typed_error() {
let journal: InMemoryJournal<()> = InMemoryJournal::new();
let ev = make_add_event(u64::MAX, Id::new_uuid(), 100, 10, Side::Buy);
assert!(journal.append(&ev).is_ok());
match ReplayEngine::<()>::replay_from(&journal, u64::MAX, "TEST") {
Err(ReplayError::SequenceOverflow { at }) => assert_eq!(at, u64::MAX),
Err(other) => panic!("expected SequenceOverflow {{ at: u64::MAX }}, got {other:?}"),
Ok(_) => panic!("advancing past u64::MAX must error"),
}
}
#[test]
fn test_replay_from_with_clock_uses_injected_clock() {
let journal: InMemoryJournal<()> = InMemoryJournal::new();
for (seq, price) in [(0u64, 100u128), (1, 101), (2, 102)] {
let ev = make_add_event(seq, Id::new_uuid(), price, 10, Side::Buy);
assert!(journal.append(&ev).is_ok());
}
let clock: Arc<dyn Clock> = Arc::new(StubClock::starting_at(42_000));
let result = ReplayEngine::<()>::replay_from_with_clock(&journal, 0, "TEST", clock);
assert!(result.is_ok(), "replay_from_with_clock should succeed");
let (book, last_seq) = result.expect("replay succeeded");
assert_eq!(last_seq, 2);
let now = book.clock().now_millis();
assert!(
now.as_u64() >= 42_000,
"expected injected clock value, got {}",
now.as_u64()
);
}
#[test]
fn test_replay_from_with_clock_preserves_behavior_of_replay_from() {
let journal: InMemoryJournal<()> = InMemoryJournal::new();
let ids: Vec<Id> = (0..3).map(|_| Id::new_uuid()).collect();
let events = [
make_add_event(0, ids[0], 100, 5, Side::Buy),
make_add_event(1, ids[1], 101, 7, Side::Buy),
make_add_event(2, ids[2], 105, 3, Side::Sell),
];
for ev in &events {
assert!(journal.append(ev).is_ok());
}
let (book_plain, last_seq_plain) = ReplayEngine::<()>::replay_from(&journal, 0, "TEST")
.expect("plain replay should succeed");
let clock: Arc<dyn Clock> = Arc::new(MonotonicClock);
let (book_with_clock, last_seq_with_clock) =
ReplayEngine::<()>::replay_from_with_clock(&journal, 0, "TEST", clock)
.expect("clock-aware replay should succeed");
assert_eq!(last_seq_plain, last_seq_with_clock);
assert_eq!(last_seq_plain, 2);
let snap_plain = book_plain.create_snapshot(usize::MAX);
let snap_with_clock = book_with_clock.create_snapshot(usize::MAX);
assert!(
snapshots_match(&snap_plain, &snap_with_clock),
"snapshots must match across replay variants"
);
}
#[test]
fn test_replay_from_with_clock_propagates_sequence_gap() {
let journal: InMemoryJournal<()> = InMemoryJournal::new();
let events = [
make_add_event(0, Id::new_uuid(), 100, 1, Side::Buy),
make_add_event(1, Id::new_uuid(), 101, 1, Side::Buy),
make_add_event(2, Id::new_uuid(), 102, 1, Side::Buy),
make_add_event(4, Id::new_uuid(), 104, 1, Side::Buy),
];
for ev in &events {
assert!(journal.append(ev).is_ok());
}
let clock: Arc<dyn Clock> = Arc::new(StubClock::new());
let result = ReplayEngine::<()>::replay_from_with_clock(&journal, 0, "TEST", clock);
match result {
Err(ReplayError::SequenceGap { expected, found }) => {
assert_eq!(expected, 3);
assert_eq!(found, 4);
}
Err(other) => panic!(
"expected SequenceGap {{ expected: 3, found: 4 }}, got {:?}",
other
),
Ok(_) => panic!("expected SequenceGap {{ expected: 3, found: 4 }}, got Ok(_)"),
}
}
#[test]
fn test_replay_market_order_by_amount_matches_live_book() {
let journal: InMemoryJournal<()> = InMemoryJournal::new();
let mut seq = 0u64;
for price in [100u128, 101, 102] {
let ev = make_add_event(seq, Id::new_uuid(), price, 10, Side::Sell);
assert!(journal.append(&ev).is_ok());
seq += 1;
}
let taker_id = Id::new_uuid();
let ev = SequencerEvent::<()> {
sequence_num: seq,
timestamp_ns: 0,
command: SequencerCommand::MarketOrderByAmount {
id: taker_id,
amount: 1_500,
side: Side::Buy,
},
result: SequencerResult::TradeExecuted {
trade_result: TradeResult::new(
"TEST".to_string(),
MatchResult::new(taker_id, Quantity::new(0)),
),
},
};
assert!(journal.append(&ev).is_ok());
let live_book: crate::OrderBook<()> = crate::OrderBook::new("TEST");
for price in [100u128, 101, 102] {
live_book
.add_order(OrderType::Standard {
id: Id::new_uuid(),
price: Price::new(price),
quantity: Quantity::new(10),
side: Side::Sell,
time_in_force: TimeInForce::Gtc,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(0),
extra_fields: (),
})
.expect("seed ask");
}
let _ = live_book.match_market_order_by_amount(taker_id, 1_500, Side::Buy);
let (replayed, last_seq) =
ReplayEngine::<()>::replay_from(&journal, 0, "TEST").expect("replay must succeed");
assert_eq!(last_seq, seq);
let live_snap = live_book.create_snapshot(usize::MAX);
let replayed_snap = replayed.create_snapshot(usize::MAX);
assert!(
snapshots_match(&live_snap, &replayed_snap),
"live and replayed snapshots must match after notional market order"
);
}
#[test]
fn test_replay_from_with_config_applies_every_field() {
let journal: InMemoryJournal<()> = InMemoryJournal::new();
let ev = make_add_event(0, Id::new_uuid(), 100, 10, Side::Buy);
assert!(journal.append(&ev).is_ok());
let (book, _) = ReplayEngine::<()>::replay_from_with_config(
&journal,
0,
"TEST",
&ReplayBookConfig::default(),
)
.expect("default config replay");
assert_eq!(book.fee_schedule(), None);
assert_eq!(book.stp_mode(), STPMode::None);
assert_eq!(book.tick_size(), None);
assert_eq!(book.lot_size(), None);
assert_eq!(book.min_order_size(), None);
assert_eq!(book.max_order_size(), None);
let fee = FeeSchedule::new(-2, 5);
let config = ReplayBookConfig::new(
Some(fee),
STPMode::None,
Some(10),
Some(5),
Some(1),
Some(1_000),
);
let (book, _) = ReplayEngine::<()>::replay_from_with_config(&journal, 0, "TEST", &config)
.expect("populated config replay");
assert_eq!(book.fee_schedule(), Some(fee));
assert_eq!(book.tick_size(), Some(10));
assert_eq!(book.lot_size(), Some(5));
assert_eq!(book.min_order_size(), Some(1));
assert_eq!(book.max_order_size(), Some(1_000));
}
#[test]
fn test_replay_with_config_pre_checks_match_plain_variants() {
let empty: InMemoryJournal<()> = InMemoryJournal::new();
assert!(matches!(
ReplayEngine::<()>::replay_from_with_config(
&empty,
0,
"TEST",
&ReplayBookConfig::default()
),
Err(ReplayError::EmptyJournal)
));
let journal: InMemoryJournal<()> = InMemoryJournal::new();
let ev = make_add_event(0, Id::new_uuid(), 100, 10, Side::Buy);
assert!(journal.append(&ev).is_ok());
let clock: Arc<dyn Clock> = Arc::new(StubClock::new());
match ReplayEngine::<()>::replay_from_with_clock_and_config(
&journal,
5,
"TEST",
clock,
&ReplayBookConfig::default(),
) {
Err(ReplayError::InvalidSequence {
from_sequence,
last_sequence,
}) => {
assert_eq!(from_sequence, 5);
assert_eq!(last_sequence, 0);
}
Err(other) => panic!("expected InvalidSequence, got {other:?}"),
Ok(_) => panic!("expected InvalidSequence, got Ok(_)"),
}
}
#[test]
fn test_market_order_by_amount_command_serde_json_roundtrip() {
let cmd: SequencerCommand<()> = SequencerCommand::MarketOrderByAmount {
id: Id::new_uuid(),
amount: 12_345_678,
side: Side::Buy,
};
let json = serde_json::to_vec(&cmd).expect("serialize");
let decoded: SequencerCommand<()> = serde_json::from_slice(&json).expect("deserialize");
match decoded {
SequencerCommand::MarketOrderByAmount { amount, side, .. } => {
assert_eq!(amount, 12_345_678);
assert_eq!(side, Side::Buy);
}
other => panic!("expected MarketOrderByAmount, got {other:?}"),
}
}
#[cfg(feature = "bincode")]
#[test]
fn test_market_order_by_amount_command_bincode_roundtrip() {
use bincode::config::standard;
use bincode::serde::{decode_from_slice, encode_to_vec};
let cmd: SequencerCommand<()> = SequencerCommand::MarketOrderByAmount {
id: Id::new_uuid(),
amount: 999_999,
side: Side::Sell,
};
let bytes = encode_to_vec(&cmd, standard()).expect("encode");
let (decoded, n) =
decode_from_slice::<SequencerCommand<()>, _>(&bytes, standard()).expect("decode");
assert_eq!(n, bytes.len());
match decoded {
SequencerCommand::MarketOrderByAmount { amount, side, .. } => {
assert_eq!(amount, 999_999);
assert_eq!(side, Side::Sell);
}
other => panic!("expected MarketOrderByAmount, got {other:?}"),
}
}
}