use ahash::AHashSet;
use nautilus_core::UnixNanos;
use rstest::{fixture, rstest};
use rust_decimal_macros::dec;
use crate::{
data::{
OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick, depth::OrderBookDepth10,
order::BookOrder, stubs::*,
},
enums::{
AggressorSide, BookAction, BookType, OrderSide, OrderSideSpecified, OrderStatus, OrderType,
RecordFlag, TimeInForce,
},
identifiers::{ClientOrderId, InstrumentId, TradeId, TraderId, VenueOrderId},
orderbook::{
BookIntegrityError, BookPrice, BookViewError, OrderBook, OwnBookOrder,
analysis::book_check_integrity,
own::{OwnBookLadder, OwnBookLevel, OwnOrderBook},
},
stubs::TestDefault,
types::{Price, Quantity},
};
#[rstest]
#[case::valid_book(
BookType::L2_MBP,
vec![
(OrderSide::Buy, "99.00", 100, 1001),
(OrderSide::Sell, "101.00", 100, 2001),
],
Ok(())
)]
#[case::crossed_book(
BookType::L2_MBP,
vec![
(OrderSide::Buy, "101.00", 100, 1001),
(OrderSide::Sell, "99.00", 100, 2001),
],
Err(BookIntegrityError::OrdersCrossed(
BookPrice::new(Price::from("101.00"), OrderSideSpecified::Buy),
BookPrice::new(Price::from("99.00"), OrderSideSpecified::Sell),
))
)]
#[case::l1_ghost_levels_handled(
BookType::L1_MBP,
vec![
(OrderSide::Buy, "99.00", 100, 1001),
(OrderSide::Buy, "98.00", 100, 1002),
],
Ok(())
)]
fn test_book_integrity_cases(
#[case] book_type: BookType,
#[case] orders: Vec<(OrderSide, &str, i64, u64)>,
#[case] expected: Result<(), BookIntegrityError>,
) {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, book_type);
for (side, price, size, id) in orders {
let order = BookOrder::new(side, Price::from(price), Quantity::from(size), id);
book.add(order, 0, id, id.into());
}
assert_eq!(book_check_integrity(&book), expected);
}
#[rstest]
fn test_book_integrity_price_boundaries() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let min_bid = BookOrder::new(OrderSide::Buy, Price::min(2), Quantity::from(100), 1);
let max_ask = BookOrder::new(OrderSide::Sell, Price::max(2), Quantity::from(100), 2);
book.add(min_bid, 0, 1, 1.into());
book.add(max_ask, 0, 2, 2.into());
assert!(book_check_integrity(&book).is_ok());
}
#[rstest]
#[case::small_quantity(100)]
#[case::medium_quantity(1000)]
#[case::large_quantity(1000000)]
fn test_book_integrity_quantity_sizes(#[case] quantity: i64) {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(quantity),
1,
);
book.add(bid, 0, 1, 1.into());
assert!(book_check_integrity(&book).is_ok());
assert_eq!(book.best_bid_size().unwrap().as_f64() as i64, quantity);
}
#[rstest]
fn test_book_display() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
assert_eq!(
book.to_string(),
"OrderBook(instrument_id=ETHUSDT-PERP.BINANCE, book_type=L2_MBP, update_count=0)"
);
}
#[rstest]
fn test_book_empty_state() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
assert_eq!(book.best_bid_price(), None);
assert_eq!(book.best_ask_price(), None);
assert_eq!(book.best_bid_size(), None);
assert_eq!(book.best_ask_size(), None);
assert!(!book.has_bid());
assert!(!book.has_ask());
}
#[rstest]
fn test_book_single_bid_state() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
1,
);
book.add(order1, 0, 1, 100.into());
assert_eq!(book.best_bid_price(), Some(Price::from("1.000")));
assert_eq!(book.best_bid_size(), Some(Quantity::from("1.0")));
assert!(book.has_bid());
}
#[rstest]
fn test_book_single_ask_state() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("2.0"),
2,
);
book.add(order, 0, 2, 200.into());
assert_eq!(book.best_ask_price(), Some(Price::from("2.000")));
assert_eq!(book.best_ask_size(), Some(Quantity::from("2.0")));
assert!(book.has_ask());
}
#[rstest]
fn test_book_empty_book_spread() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L3_MBO);
assert_eq!(book.spread(), None);
}
#[rstest]
fn test_book_spread_with_orders() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
1,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("2.0"),
2,
);
book.add(bid1, 0, 1, 100.into());
book.add(ask1, 0, 2, 200.into());
assert_eq!(book.spread(), Some(1.0));
}
#[rstest]
fn test_book_empty_book_midpoint() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
assert_eq!(book.midpoint(), None);
}
#[rstest]
fn test_book_midpoint_with_orders() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
1,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("2.0"),
2,
);
book.add(bid1, 0, 1, 100.into());
book.add(ask1, 0, 2, 200.into());
assert_eq!(book.midpoint(), Some(1.5));
}
#[rstest]
fn test_book_get_price_for_quantity_no_market() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let qty = Quantity::from(1);
assert_eq!(book.get_avg_px_for_quantity(qty, OrderSide::Buy), 0.0);
assert_eq!(book.get_avg_px_for_quantity(qty, OrderSide::Sell), 0.0);
}
#[rstest]
fn test_book_get_quantity_for_price_no_market() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let price = Price::from("1.0");
assert_eq!(book.get_quantity_for_price(price, OrderSide::Buy), 0.0);
assert_eq!(book.get_quantity_for_price(price, OrderSide::Sell), 0.0);
}
#[rstest]
fn test_book_get_price_for_quantity() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("2.010"),
Quantity::from("2.0"),
0, );
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("1.0"),
0, );
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
0, );
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("0.990"),
Quantity::from("2.0"),
0, );
book.add(bid1, 0, 1, 2.into());
book.add(bid2, 0, 1, 2.into());
book.add(ask1, 0, 1, 2.into());
book.add(ask2, 0, 1, 2.into());
let qty = Quantity::from("1.5");
assert_eq!(
book.get_avg_px_for_quantity(qty, OrderSide::Buy),
2.003_333_333_333_333_4
);
assert_eq!(
book.get_avg_px_for_quantity(qty, OrderSide::Sell),
0.996_666_666_666_666_7
);
}
#[rstest]
fn test_book_get_quantity_for_price() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask3 = BookOrder::new(
OrderSide::Sell,
Price::from("2.011"),
Quantity::from("3.0"),
0, );
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("2.010"),
Quantity::from("2.0"),
0, );
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("1.0"),
0, );
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
0, );
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("0.990"),
Quantity::from("2.0"),
0, );
let bid3 = BookOrder::new(
OrderSide::Buy,
Price::from("0.989"),
Quantity::from("3.0"),
0, );
book.add(bid1, 0, 0, 1.into());
book.add(bid2, 0, 1, 2.into());
book.add(bid3, 0, 2, 3.into());
book.add(ask1, 0, 3, 4.into());
book.add(ask2, 0, 4, 5.into());
book.add(ask3, 0, 5, 6.into());
assert_eq!(
book.get_quantity_for_price(Price::from("2.010"), OrderSide::Buy),
3.0
);
assert_eq!(
book.get_quantity_for_price(Price::from("0.990"), OrderSide::Sell),
3.0
);
}
#[rstest]
fn test_book_get_quantity_at_level_empty_book() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let price = Price::from("1.0");
assert_eq!(
book.get_quantity_at_level(price, OrderSide::Buy, 1),
Quantity::zero(1)
);
assert_eq!(
book.get_quantity_at_level(price, OrderSide::Sell, 1),
Quantity::zero(1)
);
}
#[rstest]
fn test_book_get_quantity_at_level_single_level() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("100.0"),
1,
);
let bid = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("50.0"),
2,
);
book.add(ask, 0, 1, 1.into());
book.add(bid, 0, 2, 2.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.000"), OrderSide::Buy, 1),
Quantity::from("100.0")
);
assert_eq!(
book.get_quantity_at_level(Price::from("1.000"), OrderSide::Sell, 1),
Quantity::from("50.0")
);
}
#[rstest]
fn test_book_get_quantity_at_level_multiple_levels() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("100.0"),
1,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("2.010"),
Quantity::from("200.0"),
2,
);
let ask3 = BookOrder::new(
OrderSide::Sell,
Price::from("2.020"),
Quantity::from("300.0"),
3,
);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("50.0"),
4,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("0.990"),
Quantity::from("75.0"),
5,
);
let bid3 = BookOrder::new(
OrderSide::Buy,
Price::from("0.980"),
Quantity::from("125.0"),
6,
);
book.add(ask1, 0, 1, 1.into());
book.add(ask2, 0, 2, 2.into());
book.add(ask3, 0, 3, 3.into());
book.add(bid1, 0, 4, 4.into());
book.add(bid2, 0, 5, 5.into());
book.add(bid3, 0, 6, 6.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.000"), OrderSide::Buy, 1),
Quantity::from("100.0")
);
assert_eq!(
book.get_quantity_at_level(Price::from("2.010"), OrderSide::Buy, 1),
Quantity::from("200.0")
);
assert_eq!(
book.get_quantity_at_level(Price::from("2.020"), OrderSide::Buy, 1),
Quantity::from("300.0")
);
assert_eq!(
book.get_quantity_at_level(Price::from("1.000"), OrderSide::Sell, 1),
Quantity::from("50.0")
);
assert_eq!(
book.get_quantity_at_level(Price::from("0.990"), OrderSide::Sell, 1),
Quantity::from("75.0")
);
assert_eq!(
book.get_quantity_at_level(Price::from("0.980"), OrderSide::Sell, 1),
Quantity::from("125.0")
);
}
#[rstest]
fn test_book_get_quantity_at_level_nonexistent_price() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("100.0"),
1,
);
let bid = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("50.0"),
2,
);
book.add(ask, 0, 1, 1.into());
book.add(bid, 0, 2, 2.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.005"), OrderSide::Buy, 1),
Quantity::zero(1)
);
assert_eq!(
book.get_quantity_at_level(Price::from("0.995"), OrderSide::Sell, 1),
Quantity::zero(1)
);
}
#[rstest]
fn test_book_get_quantity_at_level_vs_cumulative() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("100.0"),
1,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("2.010"),
Quantity::from("200.0"),
2,
);
let ask3 = BookOrder::new(
OrderSide::Sell,
Price::from("2.020"),
Quantity::from("300.0"),
3,
);
book.add(ask1, 0, 1, 1.into());
book.add(ask2, 0, 2, 2.into());
book.add(ask3, 0, 3, 3.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.010"), OrderSide::Buy, 1),
Quantity::from("200.0")
);
assert_eq!(
book.get_quantity_for_price(Price::from("2.010"), OrderSide::Buy),
300.0
);
}
#[rstest]
fn test_book_get_quantity_at_level_after_update() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("100.0"),
1,
);
book.add(ask, 0, 1, 1.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.000"), OrderSide::Buy, 1),
Quantity::from("100.0")
);
let ask_updated = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("150.0"),
1,
);
book.update(ask_updated, 0, 2, 2.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.000"), OrderSide::Buy, 1),
Quantity::from("150.0")
);
}
#[rstest]
fn test_book_get_quantity_at_level_after_delete() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("100.0"),
1,
);
book.add(ask, 0, 1, 1.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.000"), OrderSide::Buy, 1),
Quantity::from("100.0")
);
let ask_delete = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("0.0"),
1,
);
book.delete(ask_delete, 0, 2, 2.into());
assert_eq!(
book.get_quantity_at_level(Price::from("2.000"), OrderSide::Buy, 1),
Quantity::zero(1)
);
}
#[rstest]
fn test_book_get_price_for_exposure_no_market() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let qty = Quantity::from(1);
assert_eq!(
book.get_avg_px_qty_for_exposure(qty, OrderSide::Buy),
(0.0, 0.0, 0.0)
);
assert_eq!(
book.get_avg_px_qty_for_exposure(qty, OrderSide::Sell),
(0.0, 0.0, 0.0)
);
}
#[rstest]
fn test_book_get_price_for_exposure(stub_depth10: OrderBookDepth10) {
let depth = stub_depth10;
let instrument_id = InstrumentId::from("AAPL.XNAS"); let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.apply_depth(&depth).unwrap();
let qty = Quantity::from(1);
assert_eq!(
book.get_avg_px_qty_for_exposure(qty, OrderSide::Buy),
(100.0, 0.01, 100.0)
);
}
#[rstest]
fn test_book_apply_depth(stub_depth10: OrderBookDepth10) {
let depth = stub_depth10;
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.apply_depth(&depth).unwrap();
assert_eq!(book.best_bid_price().unwrap(), Price::from("99.00"));
assert_eq!(book.best_ask_price().unwrap(), Price::from("100.00"));
assert_eq!(book.best_bid_size().unwrap(), Quantity::from("100.0"));
assert_eq!(book.best_ask_size().unwrap(), Quantity::from("100.0"));
}
#[rstest]
fn test_book_apply_depth_all_levels(stub_depth10: OrderBookDepth10) {
let depth = stub_depth10;
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.apply_depth(&depth).unwrap();
let bid_levels: Vec<_> = book.bids(None).collect();
assert_eq!(bid_levels.len(), 10, "Should have exactly 10 bid levels");
let ask_levels: Vec<_> = book.asks(None).collect();
assert_eq!(ask_levels.len(), 10, "Should have exactly 10 ask levels");
let expected_bid_prices = vec![
Price::from("99.0"),
Price::from("98.0"),
Price::from("97.0"),
Price::from("96.0"),
Price::from("95.0"),
Price::from("94.0"),
Price::from("93.0"),
Price::from("92.0"),
Price::from("91.0"),
Price::from("90.0"),
];
for (i, level) in bid_levels.iter().enumerate() {
assert_eq!(
level.price.value, expected_bid_prices[i],
"Bid level {i} price mismatch"
);
assert!(level.size() > 0.0, "Bid level {i} has zero size");
}
let expected_ask_prices = vec![
Price::from("100.0"),
Price::from("101.0"),
Price::from("102.0"),
Price::from("103.0"),
Price::from("104.0"),
Price::from("105.0"),
Price::from("106.0"),
Price::from("107.0"),
Price::from("108.0"),
Price::from("109.0"),
];
for (i, level) in ask_levels.iter().enumerate() {
assert_eq!(
level.price.value, expected_ask_prices[i],
"Ask level {i} price mismatch"
);
assert!(level.size() > 0.0, "Ask level {i} has zero size");
}
let expected_sizes = [
100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0,
];
for (i, level) in bid_levels.iter().enumerate() {
assert_eq!(
level.size(),
expected_sizes[i],
"Bid level {i} size mismatch"
);
}
for (i, level) in ask_levels.iter().enumerate() {
assert_eq!(
level.size(),
expected_sizes[i],
"Ask level {i} size mismatch"
);
}
}
#[rstest]
fn test_book_apply_depth_empty_snapshot() {
use crate::data::depth::DEPTH10_LEN;
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let empty_order = BookOrder::new(
OrderSide::NoOrderSide,
Price::from("0.0"),
Quantity::from("0"),
0,
);
let depth = OrderBookDepth10::new(
instrument_id,
[empty_order; DEPTH10_LEN],
[empty_order; DEPTH10_LEN],
[0; DEPTH10_LEN],
[0; DEPTH10_LEN],
0,
12345,
UnixNanos::from(1000),
UnixNanos::from(2000),
);
book.apply_depth(&depth).unwrap();
assert_eq!(
book.best_bid_price(),
None,
"Empty snapshot should have no bids"
);
assert_eq!(
book.best_ask_price(),
None,
"Empty snapshot should have no asks"
);
assert!(!book.has_bid(), "Empty snapshot should not have bid");
assert!(!book.has_ask(), "Empty snapshot should not have ask");
assert_eq!(book.bids(None).count(), 0, "Should have 0 bid levels");
assert_eq!(book.asks(None).count(), 0, "Should have 0 ask levels");
assert_eq!(book.sequence, 12345);
assert_eq!(book.ts_last, UnixNanos::from(1000));
assert_eq!(book.update_count, 1);
}
#[rstest]
fn test_book_apply_depth_partial_snapshot() {
use crate::data::depth::DEPTH10_LEN;
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut bids = [BookOrder::new(
OrderSide::NoOrderSide,
Price::from("0.0"),
Quantity::from("0"),
0,
); DEPTH10_LEN];
let mut asks = [BookOrder::new(
OrderSide::NoOrderSide,
Price::from("0.0"),
Quantity::from("0"),
0,
); DEPTH10_LEN];
bids[0] = BookOrder::new(
OrderSide::Buy,
Price::from("99.0"),
Quantity::from("100"),
1,
);
bids[1] = BookOrder::new(
OrderSide::Buy,
Price::from("98.0"),
Quantity::from("200"),
2,
);
bids[2] = BookOrder::new(
OrderSide::Buy,
Price::from("97.0"),
Quantity::from("300"),
3,
);
asks[0] = BookOrder::new(
OrderSide::Sell,
Price::from("100.0"),
Quantity::from("100"),
11,
);
asks[1] = BookOrder::new(
OrderSide::Sell,
Price::from("101.0"),
Quantity::from("200"),
12,
);
asks[2] = BookOrder::new(
OrderSide::Sell,
Price::from("102.0"),
Quantity::from("300"),
13,
);
let depth = OrderBookDepth10::new(
instrument_id,
bids,
asks,
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
0,
54321,
UnixNanos::from(3000),
UnixNanos::from(4000),
);
book.apply_depth(&depth).unwrap();
let bid_levels: Vec<_> = book.bids(None).collect();
let ask_levels: Vec<_> = book.asks(None).collect();
assert_eq!(bid_levels.len(), 3, "Should have exactly 3 bid levels");
assert_eq!(ask_levels.len(), 3, "Should have exactly 3 ask levels");
for level in &bid_levels {
assert!(
level.price.value > Price::from("0.0"),
"No zero-price bid levels"
);
assert!(level.size() > 0.0, "No zero-size bid levels");
}
for level in &ask_levels {
assert!(
level.price.value > Price::from("0.0"),
"No zero-price ask levels"
);
assert!(level.size() > 0.0, "No zero-size ask levels");
}
assert_eq!(book.sequence, 54321);
assert_eq!(book.ts_last, UnixNanos::from(3000));
assert_eq!(book.update_count, 1);
}
#[rstest]
fn test_book_apply_depth_updates_metadata_once(stub_depth10: OrderBookDepth10) {
let depth = stub_depth10;
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.apply_depth(&depth).unwrap();
assert_eq!(book.sequence, depth.sequence);
assert_eq!(book.ts_last, depth.ts_event);
assert_eq!(
book.update_count, 1,
"Should increment update_count exactly once"
);
}
#[rstest]
fn test_book_apply_depth_instrument_mismatch(stub_depth10: OrderBookDepth10) {
let depth = stub_depth10; let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE"); let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let result = book.apply_depth(&depth);
assert!(result.is_err());
match result.unwrap_err() {
BookIntegrityError::InstrumentMismatch(book_id, delta_id) => {
assert_eq!(book_id.to_string(), "ETHUSDT-PERP.BINANCE");
assert_eq!(delta_id.to_string(), "AAPL.XNAS");
}
other => panic!("Expected InstrumentMismatch error, was {other:?}"),
}
assert_eq!(book.update_count, 0);
assert!(!book.has_bid());
assert!(!book.has_ask());
}
#[rstest]
fn test_book_orderbook_creation() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
assert_eq!(book.instrument_id, instrument_id);
assert_eq!(book.book_type, BookType::L2_MBP);
assert_eq!(book.sequence, 0);
assert_eq!(book.ts_last, 0);
assert_eq!(book.update_count, 0);
}
#[rstest]
fn test_book_orderbook_reset() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
book.sequence = 10;
book.ts_last = 100.into();
book.update_count = 3;
book.reset();
assert_eq!(book.book_type, BookType::L1_MBP);
assert_eq!(book.sequence, 0);
assert_eq!(book.ts_last, 0);
assert_eq!(book.update_count, 0);
}
#[rstest]
fn test_book_update_quote_tick_l1() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let quote = QuoteTick::new(
InstrumentId::from("ETHUSDT-PERP.BINANCE"),
Price::from("5000.000"),
Price::from("5100.000"),
Quantity::from("100.00000000"),
Quantity::from("99.00000000"),
0.into(),
0.into(),
);
book.update_quote_tick("e).unwrap();
assert_eq!(book.best_bid_price().unwrap(), quote.bid_price);
assert_eq!(book.best_ask_price().unwrap(), quote.ask_price);
assert_eq!(book.best_bid_size().unwrap(), quote.bid_size);
assert_eq!(book.best_ask_size().unwrap(), quote.ask_size);
}
#[rstest]
fn test_book_update_trade_tick_l1() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let price = Price::from("15000.000");
let size = Quantity::from("10.00000000");
let trade = TradeTick::new(
instrument_id,
price,
size,
AggressorSide::Buyer,
TradeId::new("123456789"),
0.into(),
0.into(),
);
book.update_trade_tick(&trade).unwrap();
assert_eq!(book.best_bid_price().unwrap(), price);
assert_eq!(book.best_ask_price().unwrap(), price);
assert_eq!(book.best_bid_size().unwrap(), size);
assert_eq!(book.best_ask_size().unwrap(), size);
}
#[rstest]
fn test_book_update_quote_tick_advances_sequence() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
assert_eq!(book.sequence, 0);
assert_eq!(book.update_count, 0);
let quote = QuoteTick::new(
instrument_id,
Price::from("5000.000"),
Price::from("5100.000"),
Quantity::from("100.00000000"),
Quantity::from("99.00000000"),
UnixNanos::from(1000),
UnixNanos::from(2000),
);
book.update_quote_tick("e).unwrap();
assert_eq!(book.sequence, 1, "Sequence should increment to 1");
assert_eq!(book.ts_last, UnixNanos::from(1000), "ts_last should update");
assert_eq!(book.update_count, 1, "update_count should increment");
let quote2 = QuoteTick::new(
instrument_id,
Price::from("5050.000"),
Price::from("5150.000"),
Quantity::from("110.00000000"),
Quantity::from("89.00000000"),
UnixNanos::from(2000),
UnixNanos::from(3000),
);
book.update_quote_tick("e2).unwrap();
assert_eq!(book.sequence, 2, "Sequence should increment to 2");
assert_eq!(
book.ts_last,
UnixNanos::from(2000),
"ts_last should update again"
);
assert_eq!(book.update_count, 2, "update_count should increment to 2");
}
#[rstest]
fn test_book_update_trade_tick_advances_sequence() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
assert_eq!(book.sequence, 0);
assert_eq!(book.update_count, 0);
let trade = TradeTick::new(
instrument_id,
Price::from("15000.000"),
Quantity::from("10.00000000"),
AggressorSide::Buyer,
TradeId::new("123456789"),
UnixNanos::from(5000),
UnixNanos::from(6000),
);
book.update_trade_tick(&trade).unwrap();
assert_eq!(book.sequence, 1, "Sequence should increment to 1");
assert_eq!(book.ts_last, UnixNanos::from(5000), "ts_last should update");
assert_eq!(book.update_count, 1, "update_count should increment");
let trade2 = TradeTick::new(
instrument_id,
Price::from("15100.000"),
Quantity::from("20.00000000"),
AggressorSide::Seller,
TradeId::new("987654321"),
UnixNanos::from(7000),
UnixNanos::from(8000),
);
book.update_trade_tick(&trade2).unwrap();
assert_eq!(book.sequence, 2, "Sequence should increment to 2");
assert_eq!(
book.ts_last,
UnixNanos::from(7000),
"ts_last should update again"
);
assert_eq!(book.update_count, 2, "update_count should increment to 2");
}
#[rstest]
fn test_book_update_stale_trade_tick_does_not_mutate_l1() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let quote = QuoteTick::new(
instrument_id,
Price::from("10.000"),
Price::from("10.000"),
Quantity::from("1.00000000"),
Quantity::from("1.00000000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
book.update_quote_tick("e).unwrap();
assert_eq!(book.ts_last, UnixNanos::from(2));
assert_eq!(book.best_bid_price().unwrap(), Price::from("10.000"));
assert_eq!(book.best_ask_price().unwrap(), Price::from("10.000"));
let stale_trade = TradeTick::new(
instrument_id,
Price::from("11.000"),
Quantity::from("1.00000000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
book.update_trade_tick(&stale_trade).unwrap();
assert_eq!(book.ts_last, UnixNanos::from(2));
assert_eq!(book.best_bid_price().unwrap(), Price::from("10.000"));
assert_eq!(book.best_ask_price().unwrap(), Price::from("10.000"));
}
#[rstest]
fn test_book_update_stale_quote_tick_does_not_mutate_l1() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let trade = TradeTick::new(
instrument_id,
Price::from("10.000"),
Quantity::from("1.00000000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(2),
UnixNanos::from(2),
);
book.update_trade_tick(&trade).unwrap();
assert_eq!(book.ts_last, UnixNanos::from(2));
assert_eq!(book.best_bid_price().unwrap(), Price::from("10.000"));
assert_eq!(book.best_ask_price().unwrap(), Price::from("10.000"));
let stale_quote = QuoteTick::new(
instrument_id,
Price::from("11.000"),
Price::from("12.000"),
Quantity::from("1.00000000"),
Quantity::from("1.00000000"),
UnixNanos::from(1),
UnixNanos::from(1),
);
book.update_quote_tick(&stale_quote).unwrap();
assert_eq!(book.ts_last, UnixNanos::from(2));
assert_eq!(book.best_bid_price().unwrap(), Price::from("10.000"));
assert_eq!(book.best_ask_price().unwrap(), Price::from("10.000"));
}
#[rstest]
fn test_book_pprint() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
1,
);
let order2 = BookOrder::new(
OrderSide::Buy,
Price::from("1.500"),
Quantity::from("2.0"),
2,
);
let order3 = BookOrder::new(
OrderSide::Buy,
Price::from("2.000"),
Quantity::from("3.0"),
3,
);
let order4 = BookOrder::new(
OrderSide::Sell,
Price::from("3.000"),
Quantity::from("3.0"),
4,
);
let order5 = BookOrder::new(
OrderSide::Sell,
Price::from("4.000"),
Quantity::from("4.0"),
5,
);
let order6 = BookOrder::new(
OrderSide::Sell,
Price::from("5.000"),
Quantity::from("8.0"),
6,
);
book.add(order1, 0, 1, 100.into());
book.add(order2, 0, 2, 200.into());
book.add(order3, 0, 3, 300.into());
book.add(order4, 0, 4, 400.into());
book.add(order5, 0, 5, 500.into());
book.add(order6, 0, 6, 600.into());
let pprint_output = book.pprint(3, None);
let expected_output = "bid_levels: 3\n\
ask_levels: 3\n\
sequence: 6\n\
update_count: 6\n\
ts_last: 600\n\
â•───────┬───────┬───────╮\n\
│ bids │ price │ asks │\n\
├───────┼───────┼───────┤\n\
│ │ 5.000 │ [8.0] │\n\
│ │ 4.000 │ [4.0] │\n\
│ │ 3.000 │ [3.0] │\n\
│ [3.0] │ 2.000 │ │\n\
│ [2.0] │ 1.500 │ │\n\
│ [1.0] │ 1.000 │ │\n\
╰───────┴───────┴───────╯";
println!("{pprint_output}");
assert_eq!(pprint_output, expected_output);
}
#[rstest]
fn test_book_group_empty_book() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let grouped_bids = book.group_bids(dec!(1), None);
let grouped_asks = book.group_asks(dec!(1), None);
assert!(grouped_bids.is_empty());
assert!(grouped_asks.is_empty());
}
#[rstest]
fn test_book_group_price_levels() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let orders = vec![
BookOrder::new(OrderSide::Buy, Price::from("1.1"), Quantity::from(1), 1),
BookOrder::new(OrderSide::Buy, Price::from("1.2"), Quantity::from(2), 2),
BookOrder::new(OrderSide::Buy, Price::from("1.8"), Quantity::from(3), 3),
BookOrder::new(OrderSide::Sell, Price::from("2.1"), Quantity::from(1), 4),
BookOrder::new(OrderSide::Sell, Price::from("2.2"), Quantity::from(2), 5),
BookOrder::new(OrderSide::Sell, Price::from("2.8"), Quantity::from(3), 6),
];
for (i, order) in orders.into_iter().enumerate() {
book.add(order, 0, i as u64, 100.into());
}
let grouped_bids = book.group_bids(dec!(0.5), Some(10));
let grouped_asks = book.group_asks(dec!(0.5), Some(10));
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_bids.get(&dec!(1.0)), Some(&dec!(3))); assert_eq!(grouped_bids.get(&dec!(1.5)), Some(&dec!(3))); assert_eq!(grouped_asks.get(&dec!(2.5)), Some(&dec!(3))); assert_eq!(grouped_asks.get(&dec!(3.0)), Some(&dec!(3))); }
#[rstest]
fn test_book_group_with_depth_limit() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let orders = vec![
BookOrder::new(OrderSide::Buy, Price::from("1.0"), Quantity::from(1), 1),
BookOrder::new(OrderSide::Buy, Price::from("2.0"), Quantity::from(2), 2),
BookOrder::new(OrderSide::Buy, Price::from("3.0"), Quantity::from(3), 3),
BookOrder::new(OrderSide::Sell, Price::from("4.0"), Quantity::from(1), 4),
BookOrder::new(OrderSide::Sell, Price::from("5.0"), Quantity::from(2), 5),
BookOrder::new(OrderSide::Sell, Price::from("6.0"), Quantity::from(3), 6),
];
for (i, order) in orders.into_iter().enumerate() {
book.add(order, 0, i as u64, 100.into());
}
let grouped_bids = book.group_bids(dec!(1), Some(2));
let grouped_asks = book.group_asks(dec!(1), Some(2));
assert_eq!(grouped_bids.len(), 2); assert_eq!(grouped_asks.len(), 2); assert_eq!(grouped_bids.get(&dec!(3)), Some(&dec!(3)));
assert_eq!(grouped_bids.get(&dec!(2)), Some(&dec!(2)));
assert_eq!(grouped_asks.get(&dec!(4)), Some(&dec!(1)));
assert_eq!(grouped_asks.get(&dec!(5)), Some(&dec!(2)));
}
#[rstest]
fn test_book_group_price_realistic() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let orders = vec![
BookOrder::new(
OrderSide::Buy,
Price::from("100.00000"),
Quantity::from(1000),
1,
),
BookOrder::new(
OrderSide::Buy,
Price::from("99.00000"),
Quantity::from(2000),
2,
),
BookOrder::new(
OrderSide::Buy,
Price::from("98.00000"),
Quantity::from(3000),
3,
),
BookOrder::new(
OrderSide::Sell,
Price::from("101.00000"),
Quantity::from(1000),
4,
),
BookOrder::new(
OrderSide::Sell,
Price::from("102.00000"),
Quantity::from(2000),
5,
),
BookOrder::new(
OrderSide::Sell,
Price::from("103.00000"),
Quantity::from(3000),
6,
),
];
for (i, order) in orders.into_iter().enumerate() {
book.add(order, 0, i as u64, 100.into());
}
let grouped_bids = book.group_bids(dec!(2), Some(10));
let grouped_asks = book.group_asks(dec!(2), Some(10));
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(1000)));
assert_eq!(grouped_bids.get(&dec!(98.0)), Some(&dec!(5000))); assert_eq!(grouped_asks.get(&dec!(102.0)), Some(&dec!(3000))); assert_eq!(grouped_asks.get(&dec!(104.0)), Some(&dec!(3000)));
}
#[rstest]
fn test_book_filtered_book_empty_own_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask_order = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
2,
);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let bids_filtered = book.bids_filtered_as_map(None, None, None, None, None);
let asks_filtered = book.asks_filtered_as_map(None, None, None, None, None);
let bids_regular = book.bids_as_map(None);
let asks_regular = book.asks_as_map(None);
assert_eq!(bids_filtered, bids_regular);
assert_eq!(asks_filtered, asks_regular);
}
#[rstest]
fn test_book_filtered_book_with_own_orders() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order1 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let bid_order2 = BookOrder::new(OrderSide::Buy, Price::from("99.00"), Quantity::from(200), 2);
let ask_order1 = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
3,
);
let ask_order2 = BookOrder::new(
OrderSide::Sell,
Price::from("102.00"),
Quantity::from(200),
4,
);
book.add(bid_order1, 0, 1, 1.into());
book.add(bid_order2, 0, 2, 2.into());
book.add(ask_order1, 0, 3, 3.into());
book.add(ask_order2, 0, 4, 4.into());
let own_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(50),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(50),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
own_book.add(own_bid_order);
own_book.add(own_ask_order);
let bids_filtered = book.bids_filtered_as_map(None, Some(&own_book), None, None, None);
let asks_filtered = book.asks_filtered_as_map(None, Some(&own_book), None, None, None);
assert_eq!(bids_filtered.get(&dec!(100.00)), Some(&dec!(50))); assert_eq!(bids_filtered.get(&dec!(99.00)), Some(&dec!(200))); assert_eq!(asks_filtered.get(&dec!(101.00)), Some(&dec!(50))); assert_eq!(asks_filtered.get(&dec!(102.00)), Some(&dec!(200))); }
#[rstest]
fn test_book_filtered_with_own_orders_exact_size() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask_order = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
2,
);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let own_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(100),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(100),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
own_book.add(own_bid_order);
own_book.add(own_ask_order);
let bids_filtered = book.bids_filtered_as_map(None, Some(&own_book), None, None, None);
let asks_filtered = book.asks_filtered_as_map(None, Some(&own_book), None, None, None);
assert!(!bids_filtered.contains_key(&dec!(100.00)));
assert!(!asks_filtered.contains_key(&dec!(101.00)));
}
#[rstest]
fn test_book_filtered_with_own_orders_larger_size() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask_order = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
2,
);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let own_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(150),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(150),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
own_book.add(own_bid_order);
own_book.add(own_ask_order);
let bids_filtered = book.bids_filtered_as_map(None, Some(&own_book), None, None, None);
let asks_filtered = book.asks_filtered_as_map(None, Some(&own_book), None, None, None);
assert!(!bids_filtered.contains_key(&dec!(100.00)));
assert!(!asks_filtered.contains_key(&dec!(101.00)));
}
#[rstest]
fn test_book_get_worst_price_for_quantity() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("2.010"),
Quantity::from("2.0"),
1,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("1.0"),
2,
);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
3,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("0.990"),
Quantity::from("2.0"),
4,
);
book.add(bid1, 0, 1, 2.into());
book.add(bid2, 0, 1, 2.into());
book.add(ask1, 0, 1, 2.into());
book.add(ask2, 0, 1, 2.into());
let qty = Quantity::from("1.5");
assert_eq!(
book.get_worst_px_for_quantity(qty, OrderSide::Buy),
Some(Price::from("2.010"))
);
assert_eq!(
book.get_worst_px_for_quantity(qty, OrderSide::Sell),
Some(Price::from("0.990"))
);
}
#[rstest]
fn test_book_get_worst_price_for_quantity_no_market() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let qty = Quantity::from(1);
assert_eq!(book.get_worst_px_for_quantity(qty, OrderSide::Buy), None);
assert_eq!(book.get_worst_px_for_quantity(qty, OrderSide::Sell), None);
}
#[rstest]
fn test_book_filtered_with_own_orders_different_level() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask_order = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
2,
);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let own_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("99.00"),
Quantity::from(50),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("102.00"),
Quantity::from(50),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
own_book.add(own_bid_order);
own_book.add(own_ask_order);
let bids_filtered = book.bids_filtered_as_map(None, Some(&own_book), None, None, None);
let asks_filtered = book.asks_filtered_as_map(None, Some(&own_book), None, None, None);
assert_eq!(bids_filtered.get(&dec!(100.00)), Some(&dec!(100)));
assert_eq!(asks_filtered.get(&dec!(101.00)), Some(&dec!(100)));
}
#[rstest]
fn test_book_filtered_with_synthetic_orders() {
let instrument_yes_id = InstrumentId::from("YES.XNAS");
let instrument_no_id = InstrumentId::from("NO.XNAS");
let mut book = OrderBook::new(instrument_yes_id, BookType::L2_MBP);
let mut synthetic_book = OwnOrderBook::new(instrument_no_id);
let own_book = OwnOrderBook::new(instrument_yes_id);
let bid_order = BookOrder::new(OrderSide::Buy, Price::from("0.40"), Quantity::from(100), 1);
let ask_order = BookOrder::new(OrderSide::Sell, Price::from("0.60"), Quantity::from(100), 2);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let synthetic_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("SYN-ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("0.60"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let synthetic_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("SYN-BID-1"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("0.40"),
Quantity::from(20),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
synthetic_book.add(synthetic_ask_order);
synthetic_book.add(synthetic_bid_order);
let combined_own = own_book.combined_with_opposite(&synthetic_book).unwrap();
let bids_filtered = book.bids_filtered_as_map(Some(10), Some(&combined_own), None, None, None);
let asks_filtered = book.asks_filtered_as_map(Some(10), Some(&combined_own), None, None, None);
assert_eq!(bids_filtered.get(&dec!(0.40)), Some(&dec!(70))); assert_eq!(asks_filtered.get(&dec!(0.60)), Some(&dec!(80))); }
#[rstest]
fn test_book_filtered_with_own_and_synthetic_orders() {
let instrument_id = InstrumentId::from("YES.XNAS");
let instrument_no_id = InstrumentId::from("NO.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let mut synthetic_book = OwnOrderBook::new(instrument_no_id);
let bid_order = BookOrder::new(OrderSide::Buy, Price::from("0.40"), Quantity::from(100), 1);
let ask_order = BookOrder::new(OrderSide::Sell, Price::from("0.60"), Quantity::from(100), 2);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let own_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("OWN-BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("0.40"),
Quantity::from(10),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("OWN-ASK-1"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("0.60"),
Quantity::from(5),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
own_book.add(own_bid_order);
own_book.add(own_ask_order);
let synthetic_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("SYN-ASK-1"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("0.60"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
3.into(),
3.into(),
3.into(),
3.into(),
);
let synthetic_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("SYN-BID-1"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Buy,
Price::from("0.40"),
Quantity::from(20),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
4.into(),
4.into(),
4.into(),
4.into(),
);
synthetic_book.add(synthetic_ask_order);
synthetic_book.add(synthetic_bid_order);
let combined_own = own_book.combined_with_opposite(&synthetic_book).unwrap();
let bids_filtered = book.bids_filtered_as_map(Some(10), Some(&combined_own), None, None, None);
let asks_filtered = book.asks_filtered_as_map(Some(10), Some(&combined_own), None, None, None);
assert_eq!(bids_filtered.get(&dec!(0.40)), Some(&dec!(60))); assert_eq!(asks_filtered.get(&dec!(0.60)), Some(&dec!(75))); }
#[rstest]
fn test_order_book_filtered_view_with_combined_own_orders() {
let instrument_yes_id = InstrumentId::from("YES.XNAS");
let instrument_no_id = InstrumentId::from("NO.XNAS");
let mut public_book = OrderBook::new(instrument_yes_id, BookType::L2_MBP);
let mut own_yes = OwnOrderBook::new(instrument_yes_id);
let mut own_no = OwnOrderBook::new(instrument_no_id);
public_book.add(
BookOrder::new(OrderSide::Buy, Price::from("0.40"), Quantity::from(100), 1),
0,
1,
1.into(),
);
public_book.add(
BookOrder::new(OrderSide::Sell, Price::from("0.60"), Quantity::from(100), 2),
0,
2,
2.into(),
);
own_yes.add(OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("OWN-BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("0.40"),
Quantity::from(10),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
));
own_yes.add(OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("OWN-ASK-1"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("0.60"),
Quantity::from(5),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
));
own_no.add(OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("NO-ASK-1"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("0.60"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
3.into(),
3.into(),
3.into(),
3.into(),
));
own_no.add(OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("NO-BID-1"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Buy,
Price::from("0.40"),
Quantity::from(20),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
4.into(),
4.into(),
4.into(),
4.into(),
));
let combined_own = own_yes.combined_with_opposite(&own_no).unwrap();
let filtered = public_book.filtered_view(Some(&combined_own), Some(10), None, None, None);
let bids = filtered.bids_as_map(None);
let asks = filtered.asks_as_map(None);
assert_eq!(bids.get(&dec!(0.40)), Some(&dec!(60))); assert_eq!(asks.get(&dec!(0.60)), Some(&dec!(75))); }
#[rstest]
fn test_order_book_filtered_view_book_and_own_book_instrument_mismatch() {
let instrument_yes_id = InstrumentId::from("YES.XNAS");
let instrument_no_id = InstrumentId::from("NO.XNAS");
let book = OrderBook::new(instrument_yes_id, BookType::L2_MBP);
let own_book = OwnOrderBook::new(instrument_no_id);
let result = book.filtered_view_checked(Some(&own_book), Some(10), None, None, None);
assert!(result.is_err());
match result.unwrap_err() {
BookViewError::InstrumentMismatch(book_id, own_book_id) => {
assert_eq!(book_id.to_string(), "YES.XNAS");
assert_eq!(own_book_id.to_string(), "NO.XNAS");
}
other => panic!("Expected InstrumentMismatch error, was {other:?}"),
}
}
#[rstest]
fn test_own_order_book_combined_with_opposite_instrument_must_differ() {
let instrument_yes_id = InstrumentId::from("YES.XNAS");
let own_book = OwnOrderBook::new(instrument_yes_id);
let synthetic_book = OwnOrderBook::new(instrument_yes_id);
let result = own_book.combined_with_opposite(&synthetic_book);
assert!(result.is_err());
match result.unwrap_err() {
BookViewError::OppositeInstrumentMatch(own_book_id, opposite_id) => {
assert_eq!(own_book_id.to_string(), "YES.XNAS");
assert_eq!(opposite_id.to_string(), "YES.XNAS");
}
other => panic!("Expected OppositeInstrumentMatch error, was {other:?}"),
}
}
#[rstest]
fn test_order_book_filtered_view_optional_books() {
let instrument_id = InstrumentId::from("YES.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid_order = BookOrder::new(OrderSide::Buy, Price::from("0.40"), Quantity::from(100), 1);
let ask_order = BookOrder::new(OrderSide::Sell, Price::from("0.60"), Quantity::from(200), 2);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let filtered = book
.filtered_view_checked(None, None, None, None, None)
.unwrap();
assert_eq!(filtered.best_bid_size(), Some(Quantity::from(100)));
assert_eq!(filtered.best_ask_size(), Some(Quantity::from(200)));
}
#[rstest]
fn test_order_book_filtered_view_preserves_metadata_when_empty() {
let instrument_id = InstrumentId::from("YES.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(OrderSide::Buy, Price::from("0.40"), Quantity::from(100), 1);
book.add(bid_order, 0, 42, 999.into());
own_book.add(OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("OWN-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("0.40"),
Quantity::from(100),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
));
let filtered = book.filtered_view(Some(&own_book), None, None, None, None);
assert!(filtered.best_bid_price().is_none());
assert_eq!(filtered.sequence, 42);
assert_eq!(filtered.ts_last, UnixNanos::from(999));
}
#[rstest]
fn test_book_filtered_with_status_filter() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask_order = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
2,
);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let own_bid_accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_bid_submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(40),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
2.into(),
2.into(),
2.into(),
2.into(),
);
let own_ask_accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
3.into(),
3.into(),
3.into(),
3.into(),
);
let own_ask_submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(40),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
4.into(),
4.into(),
4.into(),
4.into(),
);
own_book.add(own_bid_accepted);
own_book.add(own_bid_submitted);
own_book.add(own_ask_accepted);
own_book.add(own_ask_submitted);
let mut status_filter = AHashSet::new();
status_filter.insert(OrderStatus::Accepted);
let bids_filtered =
book.bids_filtered_as_map(None, Some(&own_book), Some(&status_filter), None, None);
let asks_filtered =
book.asks_filtered_as_map(None, Some(&own_book), Some(&status_filter), None, None);
assert_eq!(bids_filtered.get(&dec!(100.00)), Some(&dec!(70))); assert_eq!(asks_filtered.get(&dec!(101.00)), Some(&dec!(70)));
let bids_all_filtered = book.bids_filtered_as_map(None, Some(&own_book), None, None, None);
let asks_all_filtered = book.asks_filtered_as_map(None, Some(&own_book), None, None, None);
assert_eq!(bids_all_filtered.get(&dec!(100.00)), Some(&dec!(30))); assert_eq!(asks_all_filtered.get(&dec!(101.00)), Some(&dec!(30))); }
#[rstest]
fn test_book_filtered_with_depth_limit() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_orders = [
BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
),
BookOrder::new(OrderSide::Buy, Price::from("99.00"), Quantity::from(200), 2),
BookOrder::new(OrderSide::Buy, Price::from("98.00"), Quantity::from(300), 3),
];
let ask_orders = [
BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
4,
),
BookOrder::new(
OrderSide::Sell,
Price::from("102.00"),
Quantity::from(200),
5,
),
BookOrder::new(
OrderSide::Sell,
Price::from("103.00"),
Quantity::from(300),
6,
),
];
for (i, order) in bid_orders.iter().enumerate() {
book.add(*order, 0, i as u64, (i as u64).into());
}
for (i, order) in ask_orders.iter().enumerate() {
book.add(
*order,
0,
(i + bid_orders.len()) as u64,
UnixNanos::from((i + bid_orders.len()) as u64),
);
}
let own_bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(50),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
1.into(),
1.into(),
1.into(),
1.into(),
);
let own_ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(50),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
2.into(),
2.into(),
2.into(),
2.into(),
);
own_book.add(own_bid_order);
own_book.add(own_ask_order);
let bids_filtered = book.bids_filtered_as_map(Some(2), Some(&own_book), None, None, None);
let asks_filtered = book.asks_filtered_as_map(Some(2), Some(&own_book), None, None, None);
assert_eq!(bids_filtered.len(), 2);
assert_eq!(asks_filtered.len(), 2);
assert_eq!(bids_filtered.get(&dec!(100.00)), Some(&dec!(50))); assert_eq!(bids_filtered.get(&dec!(99.00)), Some(&dec!(200))); assert_eq!(asks_filtered.get(&dec!(101.00)), Some(&dec!(50))); assert_eq!(asks_filtered.get(&dec!(102.00)), Some(&dec!(200)));
assert!(!bids_filtered.contains_key(&dec!(98.00)));
assert!(!asks_filtered.contains_key(&dec!(103.00)));
}
#[rstest]
fn test_book_filtered_with_accepted_buffer() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask_order = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
2,
);
book.add(bid_order, 0, 1, 1.into());
book.add(ask_order, 0, 2, 2.into());
let now = UnixNanos::from(1000);
let own_bid_recent = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-RECENT"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
900.into(), 900.into(), 800.into(),
800.into(),
);
let own_bid_older = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-OLDER"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(40),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
500.into(), 500.into(), 400.into(),
400.into(),
);
let own_ask_recent = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-RECENT"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
900.into(), 900.into(), 800.into(),
800.into(),
);
let own_ask_older = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-OLDER"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from(40),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
500.into(), 500.into(), 400.into(),
400.into(),
);
own_book.add(own_bid_recent);
own_book.add(own_bid_older);
own_book.add(own_ask_recent);
own_book.add(own_ask_older);
let mut status_filter = AHashSet::new();
status_filter.insert(OrderStatus::Accepted);
let accepted_buffer = 200;
let bids_filtered = book.bids_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(accepted_buffer),
Some(now.into()),
);
let asks_filtered = book.asks_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(accepted_buffer),
Some(now.into()),
);
assert_eq!(bids_filtered.get(&dec!(100.00)), Some(&dec!(60)));
assert_eq!(asks_filtered.get(&dec!(101.00)), Some(&dec!(60)));
let short_buffer = 50;
let bids_short_buffer = book.bids_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(short_buffer),
Some(now.into()),
);
let asks_short_buffer = book.asks_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(short_buffer),
Some(now.into()),
);
assert_eq!(bids_short_buffer.get(&dec!(100.00)), Some(&dec!(30)));
assert_eq!(asks_short_buffer.get(&dec!(101.00)), Some(&dec!(30)));
let long_buffer = 600;
let bids_long_buffer = book.bids_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(long_buffer),
Some(now.into()),
);
let asks_long_buffer = book.asks_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(long_buffer),
Some(now.into()),
);
assert_eq!(bids_long_buffer.get(&dec!(100.00)), Some(&dec!(100)));
assert_eq!(asks_long_buffer.get(&dec!(101.00)), Some(&dec!(100)));
}
#[rstest]
fn test_book_filtered_with_accepted_buffer_mixed_statuses() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
book.add(bid_order, 0, 1, 1.into());
let now = UnixNanos::from(1000);
let own_bid_accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-ACCEPTED"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(20),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
500.into(), 500.into(), 400.into(),
400.into(),
);
let own_bid_submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-SUBMITTED"),
None,
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from(30),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
500.into(), 500.into(),
400.into(),
400.into(),
);
own_book.add(own_bid_accepted);
own_book.add(own_bid_submitted);
let accepted_buffer = 300;
let bids_filtered = book.bids_filtered_as_map(
None,
Some(&own_book),
None,
Some(accepted_buffer),
Some(now.into()),
);
assert_eq!(bids_filtered.get(&dec!(100.00)), Some(&dec!(50)));
let mut status_filter = AHashSet::new();
status_filter.insert(OrderStatus::Submitted);
let bids_filtered_submitted = book.bids_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter),
Some(accepted_buffer),
Some(now.into()),
);
assert_eq!(bids_filtered_submitted.get(&dec!(100.00)), Some(&dec!(70)));
let mut status_filter_both = AHashSet::new();
status_filter_both.insert(OrderStatus::Submitted);
status_filter_both.insert(OrderStatus::Accepted);
let bids_filtered_both = book.bids_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter_both),
Some(accepted_buffer),
Some(now.into()),
);
assert_eq!(bids_filtered_both.get(&dec!(100.00)), Some(&dec!(50)));
let long_buffer = 600;
let bids_filtered_long_buffer = book.bids_filtered_as_map(
None,
Some(&own_book),
Some(&status_filter_both),
Some(long_buffer),
Some(now.into()),
);
assert_eq!(
bids_filtered_long_buffer.get(&dec!(100.00)),
Some(&dec!(100))
);
}
#[rstest]
fn test_book_group_bids_filtered_empty_own_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid_order1 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let bid_order2 = BookOrder::new(OrderSide::Buy, Price::from("99.50"), Quantity::from(200), 2);
let bid_order3 = BookOrder::new(OrderSide::Buy, Price::from("99.00"), Quantity::from(300), 3);
book.add(bid_order1, 0, 1, 1.into());
book.add(bid_order2, 0, 2, 2.into());
book.add(bid_order3, 0, 3, 3.into());
let grouped_bids = book.group_bids_filtered(dec!(1.0), None, None, None, None, None);
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(100)));
assert_eq!(grouped_bids.get(&dec!(99.0)), Some(&dec!(500))); }
#[rstest]
fn test_book_group_asks_filtered_empty_own_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask_order1 = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
1,
);
let ask_order2 = BookOrder::new(
OrderSide::Sell,
Price::from("101.50"),
Quantity::from(200),
2,
);
let ask_order3 = BookOrder::new(
OrderSide::Sell,
Price::from("102.00"),
Quantity::from(300),
3,
);
book.add(ask_order1, 0, 1, 1.into());
book.add(ask_order2, 0, 2, 2.into());
book.add(ask_order3, 0, 3, 3.into());
let grouped_asks = book.group_asks_filtered(dec!(1.0), None, None, None, None, None);
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_asks.get(&dec!(101.0)), Some(&dec!(100)));
assert_eq!(grouped_asks.get(&dec!(102.0)), Some(&dec!(500)));
}
#[rstest]
fn test_book_group_bids_filtered_with_own_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order1 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let bid_order2 = BookOrder::new(OrderSide::Buy, Price::from("99.00"), Quantity::from(200), 2);
book.add(bid_order1, 0, 1, 1.into());
book.add(bid_order2, 0, 2, 2.into());
let own_bid_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("40"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let own_bid_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("99.00"),
Quantity::from("50"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
own_book.add(own_bid_order1);
own_book.add(own_bid_order2);
let grouped_bids = book.group_bids_filtered(dec!(1.0), None, Some(&own_book), None, None, None);
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(60))); assert_eq!(grouped_bids.get(&dec!(99.0)), Some(&dec!(150))); }
#[rstest]
fn test_book_group_asks_filtered_with_own_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let ask_order1 = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(100),
1,
);
let ask_order2 = BookOrder::new(
OrderSide::Sell,
Price::from("102.00"),
Quantity::from(200),
2,
);
book.add(ask_order1, 0, 1, 1.into());
book.add(ask_order2, 0, 2, 2.into());
let own_ask_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("40"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let own_ask_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("102.00"),
Quantity::from("50"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
own_book.add(own_ask_order1);
own_book.add(own_ask_order2);
let grouped_asks = book.group_asks_filtered(dec!(1.0), None, Some(&own_book), None, None, None);
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_asks.get(&dec!(101.0)), Some(&dec!(60))); assert_eq!(grouped_asks.get(&dec!(102.0)), Some(&dec!(150))); }
#[rstest]
fn test_book_group_with_status_filter() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order1 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
book.add(bid_order1, 0, 1, 1.into());
let own_accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("40"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let own_submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
own_book.add(own_accepted);
own_book.add(own_submitted);
let mut status_filter = AHashSet::new();
status_filter.insert(OrderStatus::Accepted);
let grouped_bids = book.group_bids_filtered(
dec!(1.0),
None,
Some(&own_book),
Some(&status_filter),
None,
None,
);
assert_eq!(grouped_bids.len(), 1);
assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(60))); }
#[rstest]
#[case(None)]
#[case(Some(OrderSide::NoOrderSide))]
#[case(Some(OrderSide::Buy))]
#[case(Some(OrderSide::Sell))]
fn test_book_clear_stale_levels_not_crossed(#[case] side: Option<OrderSide>) {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("99.00"),
Quantity::from("10.0"),
1,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("98.00"),
Quantity::from("20.0"),
2,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from("10.0"),
3,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("102.00"),
Quantity::from("20.0"),
4,
);
book.add(bid1, 0, 1, 100.into());
book.add(bid2, 0, 2, 200.into());
book.add(ask1, 0, 3, 300.into());
book.add(ask2, 0, 4, 400.into());
let initial_update_count = book.update_count;
let removed = book.clear_stale_levels(side);
assert!(removed.is_none());
assert_eq!(book.update_count, initial_update_count); assert_eq!(book.best_bid_price(), Some(Price::from("99.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("101.00")));
assert_eq!(book.bids(None).count(), 2);
assert_eq!(book.asks(None).count(), 2);
}
#[rstest]
#[case(None)]
#[case(Some(OrderSide::NoOrderSide))]
fn test_book_clear_stale_levels_simple_crossed(#[case] side: Option<OrderSide>) {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("105.00"),
Quantity::from("10.0"),
1,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from("20.0"),
2,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("95.00"),
Quantity::from("10.0"),
3,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("110.00"),
Quantity::from("20.0"),
4,
);
book.add(bid1, 0, 1, 100.into());
book.add(bid2, 0, 2, 200.into());
book.add(ask1, 0, 3, 300.into());
book.add(ask2, 0, 4, 400.into());
let initial_update_count = book.update_count;
let removed = book.clear_stale_levels(side);
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 3); assert_eq!(book.update_count, initial_update_count + 1); assert_eq!(book.best_bid_price(), None); assert_eq!(book.best_ask_price(), Some(Price::from("110.00"))); assert_eq!(book.bids(None).count(), 0);
assert_eq!(book.asks(None).count(), 1);
let removed2 = book.clear_stale_levels(side);
assert!(removed2.is_none());
assert_eq!(book.update_count, initial_update_count + 1);
}
#[rstest]
fn test_book_clear_stale_levels_multiple_overlapping() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("110.00"),
Quantity::from("10.0"),
1,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("108.00"),
Quantity::from("20.0"),
2,
);
let bid3 = BookOrder::new(
OrderSide::Buy,
Price::from("105.00"),
Quantity::from("30.0"),
3,
);
let bid4 = BookOrder::new(
OrderSide::Buy,
Price::from("90.00"),
Quantity::from("40.0"),
4,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("95.00"),
Quantity::from("10.0"),
5,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("100.00"),
Quantity::from("20.0"),
6,
);
let ask3 = BookOrder::new(
OrderSide::Sell,
Price::from("103.00"),
Quantity::from("30.0"),
7,
);
let ask4 = BookOrder::new(
OrderSide::Sell,
Price::from("115.00"),
Quantity::from("40.0"),
8,
);
book.add(bid1, 0, 1, 100.into());
book.add(bid2, 0, 2, 200.into());
book.add(bid3, 0, 3, 300.into());
book.add(bid4, 0, 4, 400.into());
book.add(ask1, 0, 5, 500.into());
book.add(ask2, 0, 6, 600.into());
book.add(ask3, 0, 7, 700.into());
book.add(ask4, 0, 8, 800.into());
let removed = book.clear_stale_levels(None);
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 6); assert_eq!(book.best_bid_price(), Some(Price::from("90.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("115.00")));
assert_eq!(book.bids(None).count(), 1);
assert_eq!(book.asks(None).count(), 1);
}
#[rstest]
fn test_book_clear_stale_levels_with_multiple_orders_per_level() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("105.00"),
Quantity::from("30.0"),
1,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("90.00"),
Quantity::from("20.0"),
2,
);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("95.00"),
Quantity::from("25.0"),
3,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("110.00"),
Quantity::from("20.0"),
4,
);
book.add(bid1, 0, 1, 100.into());
book.add(bid2, 0, 2, 200.into());
book.add(ask1, 0, 3, 300.into());
book.add(ask2, 0, 4, 400.into());
assert_eq!(book.best_bid_size(), Some(Quantity::from("30.0")));
let removed = book.clear_stale_levels(None);
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 2); assert_eq!(book.best_bid_price(), Some(Price::from("90.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("110.00")));
assert_eq!(book.bids(None).count(), 1);
assert_eq!(book.asks(None).count(), 1);
}
#[rstest]
fn test_book_clear_stale_levels_side_sell_clears_asks_only() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("105.00"),
Quantity::from("10.0"),
1,
),
0,
1,
100.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from("20.0"),
2,
),
0,
2,
200.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("95.00"),
Quantity::from("10.0"),
3,
),
0,
3,
300.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("110.00"),
Quantity::from("20.0"),
4,
),
0,
4,
400.into(),
);
let initial_update_count = book.update_count;
let removed = book.clear_stale_levels(Some(OrderSide::Sell));
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 1);
assert_eq!(book.update_count, initial_update_count + 1);
assert_eq!(book.best_bid_price(), Some(Price::from("105.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("110.00")));
assert_eq!(book.bids(None).count(), 2);
assert_eq!(book.asks(None).count(), 1);
}
#[rstest]
fn test_book_clear_stale_levels_side_buy_clears_bids_only() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("110.00"),
Quantity::from("10.0"),
1,
),
0,
1,
100.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("90.00"),
Quantity::from("20.0"),
2,
),
0,
2,
200.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("100.00"),
Quantity::from("10.0"),
3,
),
0,
3,
300.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("115.00"),
Quantity::from("20.0"),
4,
),
0,
4,
400.into(),
);
let initial_update_count = book.update_count;
let removed = book.clear_stale_levels(Some(OrderSide::Buy));
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 1);
assert_eq!(book.update_count, initial_update_count + 1);
assert_eq!(book.best_bid_price(), Some(Price::from("90.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("100.00")));
assert_eq!(book.bids(None).count(), 1);
assert_eq!(book.asks(None).count(), 2);
}
#[rstest]
fn test_book_clear_stale_levels_multiple_crossed_each_side() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("110.00"),
Quantity::from("10.0"),
1,
),
0,
1,
100.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("105.00"),
Quantity::from("20.0"),
2,
),
0,
2,
200.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("102.00"),
Quantity::from("30.0"),
3,
),
0,
3,
300.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("99.00"),
Quantity::from("40.0"),
4,
),
0,
4,
400.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("95.00"),
Quantity::from("50.0"),
5,
),
0,
5,
500.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("90.00"),
Quantity::from("60.0"),
6,
),
0,
6,
600.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("100.00"),
Quantity::from("15.0"),
7,
),
0,
7,
700.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("103.00"),
Quantity::from("25.0"),
8,
),
0,
8,
800.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("106.00"),
Quantity::from("35.0"),
9,
),
0,
9,
900.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("109.00"),
Quantity::from("45.0"),
10,
),
0,
10,
1000.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("112.00"),
Quantity::from("55.0"),
11,
),
0,
11,
1100.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("115.00"),
Quantity::from("65.0"),
12,
),
0,
12,
1200.into(),
);
assert_eq!(book.best_bid_price(), Some(Price::from("110.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("100.00")));
let removed = book.clear_stale_levels(None);
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 7);
assert_eq!(removed_levels[0].price.value, Price::from("110.00"));
assert_eq!(
removed_levels[0].size_decimal(),
rust_decimal::Decimal::from(10)
);
assert_eq!(removed_levels[1].price.value, Price::from("105.00"));
assert_eq!(removed_levels[2].price.value, Price::from("102.00"));
assert_eq!(removed_levels[3].price.value, Price::from("100.00"));
assert_eq!(
removed_levels[3].size_decimal(),
rust_decimal::Decimal::from(15)
);
assert_eq!(removed_levels[4].price.value, Price::from("103.00"));
assert_eq!(removed_levels[5].price.value, Price::from("106.00"));
assert_eq!(removed_levels[6].price.value, Price::from("109.00"));
assert_eq!(book.best_bid_price(), Some(Price::from("99.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("112.00")));
assert_eq!(book.bids(None).count(), 3);
assert_eq!(book.asks(None).count(), 2);
let update_count_before = book.update_count;
let removed_again = book.clear_stale_levels(None);
assert!(removed_again.is_none());
assert_eq!(book.update_count, update_count_before);
}
#[rstest]
fn test_book_clear_stale_levels_multiple_crossed_side_specific() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("110.00"),
Quantity::from("10.0"),
1,
),
0,
1,
100.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("105.00"),
Quantity::from("20.0"),
2,
),
0,
2,
200.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("102.00"),
Quantity::from("30.0"),
3,
),
0,
3,
300.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("99.00"),
Quantity::from("40.0"),
4,
),
0,
4,
400.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("95.00"),
Quantity::from("50.0"),
5,
),
0,
5,
500.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("90.00"),
Quantity::from("60.0"),
6,
),
0,
6,
600.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("100.00"),
Quantity::from("15.0"),
7,
),
0,
7,
700.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("103.00"),
Quantity::from("25.0"),
8,
),
0,
8,
800.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("106.00"),
Quantity::from("35.0"),
9,
),
0,
9,
900.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("109.00"),
Quantity::from("45.0"),
10,
),
0,
10,
1000.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("112.00"),
Quantity::from("55.0"),
11,
),
0,
11,
1100.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("115.00"),
Quantity::from("65.0"),
12,
),
0,
12,
1200.into(),
);
let removed = book.clear_stale_levels(Some(OrderSide::Buy));
assert!(removed.is_some());
let removed_levels = removed.unwrap();
assert_eq!(removed_levels.len(), 3);
assert_eq!(removed_levels[0].price.value, Price::from("110.00"));
assert_eq!(removed_levels[1].price.value, Price::from("105.00"));
assert_eq!(removed_levels[2].price.value, Price::from("102.00"));
assert_eq!(book.best_bid_price(), Some(Price::from("99.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("100.00")));
assert_eq!(book.bids(None).count(), 3);
assert_eq!(book.asks(None).count(), 6);
}
#[rstest]
fn test_book_clear_stale_levels_l1_mbp() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let initial_update_count = book.update_count;
let removed = book.clear_stale_levels(None);
assert!(removed.is_none());
assert_eq!(book.update_count, initial_update_count);
}
#[fixture]
fn own_order() -> OwnBookOrder {
let trader_id = TraderId::test_default();
let client_order_id = ClientOrderId::from("O-123456789");
let venue_order_id = None;
let side = OrderSideSpecified::Buy;
let price = Price::from("100.00");
let size = Quantity::from("10");
let order_type = OrderType::Limit;
let time_in_force = TimeInForce::Gtc;
let status = OrderStatus::Submitted;
let ts_last = UnixNanos::from(2);
let ts_accepted = UnixNanos::from(0);
let ts_submitted = UnixNanos::from(2);
let ts_init = UnixNanos::from(1);
OwnBookOrder::new(
trader_id,
client_order_id,
venue_order_id,
side,
price,
size,
order_type,
time_in_force,
status,
ts_last,
ts_accepted,
ts_submitted,
ts_init,
)
}
#[rstest]
fn test_own_order_to_book_price(own_order: OwnBookOrder) {
let book_price = own_order.to_book_price();
assert_eq!(book_price.value, Price::from("100.00"));
assert_eq!(book_price.side, OrderSideSpecified::Buy);
}
#[rstest]
fn test_own_order_exposure(own_order: OwnBookOrder) {
let exposure = own_order.exposure();
assert_eq!(exposure, 1000.0);
}
#[rstest]
fn test_own_order_signed_size(own_order: OwnBookOrder) {
let own_order_buy = own_order;
let own_order_sell = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-123456789"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Sell,
Price::from("101.0"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::from(2),
UnixNanos::from(0),
UnixNanos::from(2),
UnixNanos::from(1),
);
assert_eq!(own_order_buy.signed_size(), 10.0);
assert_eq!(own_order_sell.signed_size(), -10.0);
}
#[rstest]
fn test_own_order_debug(own_order: OwnBookOrder) {
assert_eq!(
format!("{own_order:?}"),
"OwnBookOrder(trader_id=TRADER-001, client_order_id=O-123456789, venue_order_id=None, side=BUY, price=100.00, size=10, order_type=LIMIT, time_in_force=GTC, status=SUBMITTED, ts_last=2, ts_accepted=0, ts_submitted=2, ts_init=1)"
);
}
#[rstest]
fn test_own_order_display(own_order: OwnBookOrder) {
assert_eq!(
own_order.to_string(),
"TRADER-001,O-123456789,None,BUY,100.00,10,LIMIT,GTC,SUBMITTED,2,0,2,1".to_string()
);
}
#[rstest]
fn test_client_order_ids_empty_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let book = OwnOrderBook::new(instrument_id);
let bid_ids = book.bid_client_order_ids();
let ask_ids = book.ask_client_order_ids();
assert!(bid_ids.is_empty());
assert!(ask_ids.is_empty());
let client_order_id = ClientOrderId::from("O-NONEXISTENT");
assert!(!book.is_order_in_book(&client_order_id));
}
#[rstest]
fn test_client_order_ids_with_orders() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let bid_id1 = ClientOrderId::from("O-BID-1");
let bid_id2 = ClientOrderId::from("O-BID-2");
let ask_id1 = ClientOrderId::from("O-ASK-1");
let ask_id2 = ClientOrderId::from("O-ASK-2");
let bid_order1 = OwnBookOrder::new(
TraderId::test_default(),
bid_id1,
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let bid_order2 = OwnBookOrder::new(
TraderId::test_default(),
bid_id2,
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("99.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order1 = OwnBookOrder::new(
TraderId::test_default(),
ask_id1,
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order2 = OwnBookOrder::new(
TraderId::test_default(),
ask_id2,
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("102.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(bid_order1);
book.add(bid_order2);
book.add(ask_order1);
book.add(ask_order2);
let bid_ids = book.bid_client_order_ids();
assert_eq!(bid_ids.len(), 2);
assert!(bid_ids.contains(&bid_id1));
assert!(bid_ids.contains(&bid_id2));
assert!(!bid_ids.contains(&ask_id1));
assert!(!bid_ids.contains(&ask_id2));
let ask_ids = book.ask_client_order_ids();
assert_eq!(ask_ids.len(), 2);
assert!(ask_ids.contains(&ask_id1));
assert!(ask_ids.contains(&ask_id2));
assert!(!ask_ids.contains(&bid_id1));
assert!(!ask_ids.contains(&bid_id2));
assert!(book.is_order_in_book(&bid_id1));
assert!(book.is_order_in_book(&bid_id2));
assert!(book.is_order_in_book(&ask_id1));
assert!(book.is_order_in_book(&ask_id2));
assert!(!book.is_order_in_book(&ClientOrderId::from("O-NON-EXISTENT")));
}
#[rstest]
fn test_client_order_ids_after_operations() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let client_order_id = ClientOrderId::from("O-BID-1");
let order = OwnBookOrder::new(
TraderId::test_default(),
client_order_id,
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(order);
assert!(book.is_order_in_book(&client_order_id));
assert_eq!(book.bid_client_order_ids().len(), 1);
book.delete(order).unwrap();
assert!(!book.is_order_in_book(&client_order_id));
assert!(book.bid_client_order_ids().is_empty());
}
#[rstest]
fn test_own_book_update_missing_order_errors() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let missing_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-MISSING"),
None,
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("1"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::from(1_u64),
UnixNanos::default(),
UnixNanos::from(1_u64),
UnixNanos::from(1_u64),
);
let result = book.update(missing_order);
assert!(result.is_err());
}
#[rstest]
fn test_own_book_delete_missing_order_errors() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let missing_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-MISSING"),
None,
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("1"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::from(1_u64),
UnixNanos::default(),
UnixNanos::from(1_u64),
UnixNanos::from(1_u64),
);
let result = book.delete(missing_order);
assert!(result.is_err());
}
#[rstest]
fn test_own_book_display() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OwnOrderBook::new(instrument_id);
assert_eq!(
book.to_string(),
"OwnOrderBook(instrument_id=ETHUSDT-PERP.BINANCE, orders=0, update_count=0)"
);
}
#[rstest]
fn test_own_book_pprint() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OwnOrderBook::new(instrument_id);
let order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("1.500"),
Quantity::from("2.0"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order3 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("2.000"),
Quantity::from("3.0"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order4 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-4"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("3.000"),
Quantity::from("3.0"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order5 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-5"),
Some(VenueOrderId::from("5")),
OrderSideSpecified::Sell,
Price::from("4.000"),
Quantity::from("4.0"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order6 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-6"),
Some(VenueOrderId::from("6")),
OrderSideSpecified::Sell,
Price::from("5.000"),
Quantity::from("8.0"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(order1);
book.add(order2);
book.add(order3);
book.add(order4);
book.add(order5);
book.add(order6);
let pprint_output = book.pprint(3, None);
let expected_output = "bid_levels: 3\n\
ask_levels: 3\n\
update_count: 6\n\
ts_last: 0\n\
â•───────┬───────┬───────╮\n\
│ bids │ price │ asks │\n\
├───────┼───────┼───────┤\n\
│ │ 5.000 │ [8.0] │\n\
│ │ 4.000 │ [4.0] │\n\
│ │ 3.000 │ [3.0] │\n\
│ [3.0] │ 2.000 │ │\n\
│ [2.0] │ 1.500 │ │\n\
│ [1.0] │ 1.000 │ │\n\
╰───────┴───────┴───────╯";
println!("{pprint_output}");
assert_eq!(pprint_output, expected_output);
}
#[rstest]
fn test_own_book_level_size_and_exposure() {
let mut level = OwnBookLevel::new(BookPrice::new(
Price::from("100.00"),
OrderSideSpecified::Buy,
));
let order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
level.add(order1);
level.add(order2);
assert_eq!(level.len(), 2);
assert_eq!(level.size(), 30.0);
assert_eq!(level.exposure(), 3000.0);
}
#[rstest]
fn test_own_book_level_add_update_delete() {
let mut level = OwnBookLevel::new(BookPrice::new(
Price::from("100.00"),
OrderSideSpecified::Buy,
));
let order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
level.add(order);
assert_eq!(level.len(), 1);
let updated = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
level.update(updated);
let orders = level.get_orders();
assert_eq!(orders[0].size, Quantity::from("15"));
level.delete(&ClientOrderId::from("O-1")).unwrap();
assert!(level.is_empty());
}
#[rstest]
fn test_own_book_ladder_add_update_delete() {
let mut ladder = OwnBookLadder::new(OrderSideSpecified::Buy);
let order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
ladder.add(order1);
ladder.add(order2);
assert_eq!(ladder.len(), 1);
assert_eq!(ladder.sizes(), 30.0);
let order2_updated = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("25"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
ladder.update(order2_updated).unwrap();
assert_eq!(ladder.sizes(), 35.0);
ladder.delete(order1).unwrap();
assert_eq!(ladder.sizes(), 25.0);
}
#[rstest]
fn test_own_order_book_add_update_delete_clear() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let order_buy = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order_sell = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(order_buy);
book.add(order_sell);
assert!(!book.bids.is_empty());
assert!(!book.asks.is_empty());
let order_buy_updated = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.update(order_buy_updated).unwrap();
book.delete(order_sell).unwrap();
assert_eq!(book.bids.sizes(), 15.0);
assert!(book.asks.is_empty());
book.clear();
assert!(book.bids.is_empty());
assert!(book.asks.is_empty());
}
#[rstest]
fn test_own_order_book_bids_and_asks_as_map() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(order1);
book.add(order2);
let bids_map = book.bids_as_map(None, None, None);
let asks_map = book.asks_as_map(None, None, None);
assert_eq!(bids_map.len(), 1);
let bid_price = Price::from("100.00").as_decimal();
let bid_orders = bids_map.get(&bid_price).unwrap();
assert_eq!(bid_orders.len(), 1);
assert_eq!(bid_orders[0], order1);
assert_eq!(asks_map.len(), 1);
let ask_price = Price::from("101.00").as_decimal();
let ask_orders = asks_map.get(&ask_price).unwrap();
assert_eq!(ask_orders.len(), 1);
assert_eq!(ask_orders[0], order2);
}
#[rstest]
fn test_own_order_book_quantity_empty_levels() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let book = OwnOrderBook::new(instrument_id);
let bid_quantities = book.bid_quantity(None, None, None, None, None);
let ask_quantities = book.ask_quantity(None, None, None, None, None);
assert!(bid_quantities.is_empty());
assert!(ask_quantities.is_empty());
}
#[rstest]
fn test_own_order_book_bid_ask_quantity() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let bid_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let bid_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let bid_order3 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("99.50"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-4"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("12"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-5"),
Some(VenueOrderId::from("5")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("8"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(bid_order1);
book.add(bid_order2);
book.add(bid_order3);
book.add(ask_order1);
book.add(ask_order2);
let bid_quantities = book.bid_quantity(None, None, None, None, None);
assert_eq!(bid_quantities.len(), 2);
assert_eq!(bid_quantities.get(&dec!(100.00)), Some(&dec!(25)));
assert_eq!(bid_quantities.get(&dec!(99.50)), Some(&dec!(20)));
let ask_quantities = book.ask_quantity(None, None, None, None, None);
assert_eq!(ask_quantities.len(), 1);
assert_eq!(ask_quantities.get(&dec!(101.00)), Some(&dec!(20)));
}
#[rstest]
fn test_status_filtering_bids_as_map() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
None,
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let canceled = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("99.50"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Canceled,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(submitted);
book.add(accepted);
book.add(canceled);
let all_orders = book.bids_as_map(None, None, None);
assert_eq!(all_orders.len(), 2); assert_eq!(all_orders.get(&dec!(100.00)).unwrap().len(), 2); assert_eq!(all_orders.get(&dec!(99.50)).unwrap().len(), 1);
let mut filter_submitted = AHashSet::new();
filter_submitted.insert(OrderStatus::Submitted);
let submitted_orders = book.bids_as_map(Some(&filter_submitted), None, None);
assert_eq!(submitted_orders.len(), 1); assert_eq!(submitted_orders.get(&dec!(100.00)).unwrap().len(), 1); assert_eq!(
submitted_orders.get(&dec!(100.00)).unwrap()[0].status,
OrderStatus::Submitted
);
assert!(submitted_orders.get(&dec!(99.50)).is_none());
let mut filter_accepted_canceled = AHashSet::new();
filter_accepted_canceled.insert(OrderStatus::Accepted);
filter_accepted_canceled.insert(OrderStatus::Canceled);
let accepted_canceled_orders = book.bids_as_map(Some(&filter_accepted_canceled), None, None);
assert_eq!(accepted_canceled_orders.len(), 2); assert_eq!(
accepted_canceled_orders.get(&dec!(100.00)).unwrap().len(),
1
); assert_eq!(accepted_canceled_orders.get(&dec!(99.50)).unwrap().len(), 1);
let mut filter_filled = AHashSet::new();
filter_filled.insert(OrderStatus::Filled);
let filled_orders = book.bids_as_map(Some(&filter_filled), None, None);
assert_eq!(filled_orders.len(), 0); }
#[rstest]
fn test_status_filtering_asks_as_map() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
None,
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(submitted);
book.add(accepted);
let all_orders = book.asks_as_map(None, None, None);
assert_eq!(all_orders.len(), 1); assert_eq!(all_orders.get(&dec!(101.00)).unwrap().len(), 2);
let mut filter_submitted = AHashSet::new();
filter_submitted.insert(OrderStatus::Submitted);
let submitted_orders = book.asks_as_map(Some(&filter_submitted), None, None);
assert_eq!(submitted_orders.len(), 1); assert_eq!(submitted_orders.get(&dec!(101.00)).unwrap().len(), 1); assert_eq!(
submitted_orders.get(&dec!(101.00)).unwrap()[0].status,
OrderStatus::Submitted
);
}
#[rstest]
fn test_status_filtering_bid_quantity() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
None,
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let canceled = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("99.50"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Canceled,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(submitted);
book.add(accepted);
book.add(canceled);
let all_quantities = book.bid_quantity(None, None, None, None, None);
assert_eq!(all_quantities.len(), 2); assert_eq!(all_quantities.get(&dec!(100.00)), Some(&dec!(25))); assert_eq!(all_quantities.get(&dec!(99.50)), Some(&dec!(20)));
let mut filter_submitted = AHashSet::new();
filter_submitted.insert(OrderStatus::Submitted);
let submitted_quantities = book.bid_quantity(Some(&filter_submitted), None, None, None, None);
assert_eq!(submitted_quantities.len(), 1); assert_eq!(submitted_quantities.get(&dec!(100.00)), Some(&dec!(10))); assert!(submitted_quantities.get(&dec!(99.50)).is_none());
let mut filter_accepted_canceled = AHashSet::new();
filter_accepted_canceled.insert(OrderStatus::Accepted);
filter_accepted_canceled.insert(OrderStatus::Canceled);
let accepted_canceled_quantities =
book.bid_quantity(Some(&filter_accepted_canceled), None, None, None, None);
assert_eq!(accepted_canceled_quantities.len(), 2); assert_eq!(
accepted_canceled_quantities.get(&dec!(100.00)),
Some(&dec!(15))
); assert_eq!(
accepted_canceled_quantities.get(&dec!(99.50)),
Some(&dec!(20))
); }
#[rstest]
fn test_status_filtering_ask_quantity() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let submitted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-1"),
None,
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Submitted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let accepted = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let canceled = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("O-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("102.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Canceled,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(submitted);
book.add(accepted);
book.add(canceled);
let all_quantities = book.ask_quantity(None, None, None, None, None);
assert_eq!(all_quantities.len(), 2); assert_eq!(all_quantities.get(&dec!(101.00)), Some(&dec!(25))); assert_eq!(all_quantities.get(&dec!(102.00)), Some(&dec!(20)));
let mut filter_submitted = AHashSet::new();
filter_submitted.insert(OrderStatus::Submitted);
let submitted_quantities = book.ask_quantity(Some(&filter_submitted), None, None, None, None);
assert_eq!(submitted_quantities.len(), 1); assert_eq!(submitted_quantities.get(&dec!(101.00)), Some(&dec!(10))); assert!(submitted_quantities.get(&dec!(102.00)).is_none());
let mut filter_multiple = AHashSet::new();
filter_multiple.insert(OrderStatus::Submitted);
filter_multiple.insert(OrderStatus::Canceled);
let multiple_quantities = book.ask_quantity(Some(&filter_multiple), None, None, None, None);
assert_eq!(multiple_quantities.len(), 2); assert_eq!(multiple_quantities.get(&dec!(101.00)), Some(&dec!(10))); assert_eq!(multiple_quantities.get(&dec!(102.00)), Some(&dec!(20)));
let mut filter_filled = AHashSet::new();
filter_filled.insert(OrderStatus::Filled);
let filled_quantities = book.ask_quantity(Some(&filter_filled), None, None, None, None);
assert_eq!(filled_quantities.len(), 0); }
#[rstest]
fn test_own_book_group_empty_book() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let book = OwnOrderBook::new(instrument_id);
let grouped_bids = book.bid_quantity(None, None, Some(dec!(1)), None, None);
let grouped_asks = book.ask_quantity(None, None, Some(dec!(1)), None, None);
assert!(grouped_bids.is_empty());
assert!(grouped_asks.is_empty());
}
#[rstest]
fn test_own_book_group_price_levels() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OwnOrderBook::new(instrument_id);
let bid_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("1.1"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let bid_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("1.2"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let bid_order3 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("1.8"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("2.1"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("5")),
OrderSideSpecified::Sell,
Price::from("2.2"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order3 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-3"),
Some(VenueOrderId::from("6")),
OrderSideSpecified::Sell,
Price::from("2.8"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(bid_order1);
book.add(bid_order2);
book.add(bid_order3);
book.add(ask_order1);
book.add(ask_order2);
book.add(ask_order3);
let grouped_bids = book.bid_quantity(None, None, Some(dec!(0.5)), None, None);
let grouped_asks = book.ask_quantity(None, None, Some(dec!(0.5)), None, None);
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_bids.get(&dec!(1.0)), Some(&dec!(30))); assert_eq!(grouped_bids.get(&dec!(1.5)), Some(&dec!(30)));
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_asks.get(&dec!(2.5)), Some(&dec!(30))); assert_eq!(grouped_asks.get(&dec!(3.0)), Some(&dec!(30))); }
#[rstest]
fn test_own_book_group_with_depth_limit() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OwnOrderBook::new(instrument_id);
let orders = [
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("1.0"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("2.0"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("3.0"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("4.0"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("5")),
OrderSideSpecified::Sell,
Price::from("5.0"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-3"),
Some(VenueOrderId::from("6")),
OrderSideSpecified::Sell,
Price::from("6.0"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
];
for order in &orders {
book.add(*order);
}
let grouped_bids = book.bid_quantity(None, Some(2), Some(dec!(1.0)), None, None);
let grouped_asks = book.ask_quantity(None, Some(2), Some(dec!(1.0)), None, None);
assert_eq!(grouped_bids.len(), 2); assert_eq!(grouped_bids.get(&dec!(3.0)), Some(&dec!(30))); assert_eq!(grouped_bids.get(&dec!(2.0)), Some(&dec!(20)));
assert!(grouped_bids.get(&dec!(1.0)).is_none());
assert_eq!(grouped_asks.len(), 2); assert_eq!(grouped_asks.get(&dec!(4.0)), Some(&dec!(10))); assert_eq!(grouped_asks.get(&dec!(5.0)), Some(&dec!(20)));
assert!(grouped_asks.get(&dec!(6.0)).is_none()); }
#[rstest]
fn test_own_book_group_with_multiple_orders_at_same_level() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OwnOrderBook::new(instrument_id);
let bid_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("1.0"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let bid_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("1.0"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order1 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("2.0"),
Quantity::from("15"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order2 = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("2.0"),
Quantity::from("25"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
book.add(bid_order1);
book.add(bid_order2);
book.add(ask_order1);
book.add(ask_order2);
let grouped_bids = book.bid_quantity(None, None, Some(dec!(1.0)), None, None);
let grouped_asks = book.ask_quantity(None, None, Some(dec!(1.0)), None, None);
assert_eq!(grouped_bids.len(), 1);
assert_eq!(grouped_bids.get(&dec!(1.0)), Some(&dec!(30)));
assert_eq!(grouped_asks.len(), 1);
assert_eq!(grouped_asks.get(&dec!(2.0)), Some(&dec!(40))); }
#[rstest]
fn test_own_book_group_with_larger_group_size() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OwnOrderBook::new(instrument_id);
let bid_orders = [
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("99.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("98.00"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
];
let ask_orders = [
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("5")),
OrderSideSpecified::Sell,
Price::from("102.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-3"),
Some(VenueOrderId::from("6")),
OrderSideSpecified::Sell,
Price::from("103.00"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
];
for order in &bid_orders {
book.add(*order);
}
for order in &ask_orders {
book.add(*order);
}
let grouped_bids = book.bid_quantity(None, None, Some(dec!(2)), None, None);
let grouped_asks = book.ask_quantity(None, None, Some(dec!(2)), None, None);
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(10))); assert_eq!(grouped_bids.get(&dec!(98.0)), Some(&dec!(50)));
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_asks.get(&dec!(102.0)), Some(&dec!(30))); assert_eq!(grouped_asks.get(&dec!(104.0)), Some(&dec!(30))); }
#[rstest]
fn test_own_book_group_with_fractional_group_size() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OwnOrderBook::new(instrument_id);
let bid_orders = [
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("1.23"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("1.27"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-3"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Buy,
Price::from("1.43"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
];
let ask_orders = [
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("1.53"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("5")),
OrderSideSpecified::Sell,
Price::from("1.57"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-3"),
Some(VenueOrderId::from("6")),
OrderSideSpecified::Sell,
Price::from("1.73"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
];
for order in &bid_orders {
book.add(*order);
}
for order in &ask_orders {
book.add(*order);
}
let grouped_bids = book.bid_quantity(None, None, Some(dec!(0.1)), None, None);
let grouped_asks = book.ask_quantity(None, None, Some(dec!(0.1)), None, None);
assert_eq!(grouped_bids.len(), 2);
assert_eq!(grouped_bids.get(&dec!(1.2)), Some(&dec!(30))); assert_eq!(grouped_bids.get(&dec!(1.4)), Some(&dec!(30)));
assert_eq!(grouped_asks.len(), 2);
assert_eq!(grouped_asks.get(&dec!(1.6)), Some(&dec!(30))); assert_eq!(grouped_asks.get(&dec!(1.8)), Some(&dec!(30))); }
#[rstest]
fn test_own_book_group_with_status_and_buffer() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut own_book = OwnOrderBook::new(instrument_id);
let now = 1000u64;
let own_recent = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("40"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::from(900), UnixNanos::from(900), UnixNanos::from(800),
UnixNanos::from(800),
);
let own_older = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("30"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::from(500), UnixNanos::from(500), UnixNanos::from(400),
UnixNanos::from(400),
);
own_book.add(own_recent);
own_book.add(own_older);
let mut status_filter = AHashSet::new();
status_filter.insert(OrderStatus::Accepted);
let grouped_bids = own_book.bid_quantity(
Some(&status_filter),
None,
Some(dec!(1.0)),
Some(300),
Some(now),
);
assert_eq!(grouped_bids.len(), 1);
assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(30)));
let grouped_all = own_book.bid_quantity(
Some(&status_filter),
None,
Some(dec!(1.0)),
Some(50),
Some(now),
);
assert_eq!(grouped_all.len(), 1);
assert_eq!(grouped_all.get(&dec!(100.0)), Some(&dec!(70))); }
#[rstest]
fn test_own_book_audit_open_orders_no_removals() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut own_book = OwnOrderBook::new(instrument_id);
let bid_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
let ask_order = OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
);
own_book.add(bid_order);
own_book.add(ask_order);
let mut open_order_ids = AHashSet::new();
open_order_ids.insert(ClientOrderId::from("BID-1"));
open_order_ids.insert(ClientOrderId::from("ASK-1"));
own_book.audit_open_orders(&open_order_ids);
assert!(own_book.is_order_in_book(&ClientOrderId::from("BID-1")));
assert!(own_book.is_order_in_book(&ClientOrderId::from("ASK-1")));
assert_eq!(own_book.bid_client_order_ids().len(), 1);
assert_eq!(own_book.ask_client_order_ids().len(), 1);
}
#[rstest]
fn test_own_book_audit_open_orders_with_removals() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut own_book = OwnOrderBook::new(instrument_id);
let orders = [
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-1"),
Some(VenueOrderId::from("1")),
OrderSideSpecified::Buy,
Price::from("100.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("BID-2"),
Some(VenueOrderId::from("2")),
OrderSideSpecified::Buy,
Price::from("99.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-1"),
Some(VenueOrderId::from("3")),
OrderSideSpecified::Sell,
Price::from("101.00"),
Quantity::from("10"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
OwnBookOrder::new(
TraderId::test_default(),
ClientOrderId::from("ASK-2"),
Some(VenueOrderId::from("4")),
OrderSideSpecified::Sell,
Price::from("102.00"),
Quantity::from("20"),
OrderType::Limit,
TimeInForce::Gtc,
OrderStatus::Accepted,
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
UnixNanos::default(),
),
];
for order in &orders {
own_book.add(*order);
}
assert_eq!(own_book.bid_client_order_ids().len(), 2);
assert_eq!(own_book.ask_client_order_ids().len(), 2);
let mut open_order_ids = AHashSet::new();
open_order_ids.insert(ClientOrderId::from("BID-1"));
open_order_ids.insert(ClientOrderId::from("ASK-1"));
own_book.audit_open_orders(&open_order_ids);
assert!(own_book.is_order_in_book(&ClientOrderId::from("BID-1")));
assert!(!own_book.is_order_in_book(&ClientOrderId::from("BID-2")));
assert!(own_book.is_order_in_book(&ClientOrderId::from("ASK-1")));
assert!(!own_book.is_order_in_book(&ClientOrderId::from("ASK-2")));
assert_eq!(own_book.bid_client_order_ids().len(), 1);
assert_eq!(own_book.ask_client_order_ids().len(), 1);
}
use proptest::prelude::*;
#[derive(Clone, Debug)]
enum OrderBookOperation {
Add(BookOrder, u8, u64),
Update(BookOrder, u8, u64),
Delete(BookOrder, u8, u64),
Clear(u64),
ClearBids(u64),
ClearAsks(u64),
}
fn price_strategy() -> impl Strategy<Value = Price> {
use crate::types::{fixed::FIXED_PRECISION, price::PriceRaw};
let scale_prec2 = 10i64.pow(u32::from(FIXED_PRECISION - 2)) as PriceRaw; let scale_prec8 = 10i64.pow(u32::from(FIXED_PRECISION - 8)) as PriceRaw;
prop_oneof![
(1i64..=10000i64).prop_map(move |base| Price::from_raw(base as PriceRaw * scale_prec2, 2)),
(1i64..=100i64).prop_map(move |base| Price::from_raw(base as PriceRaw * scale_prec8, 8)),
(10000i64..=1000000i64)
.prop_map(move |base| Price::from_raw(base as PriceRaw * scale_prec2, 2)),
(-10000i64..=-1i64)
.prop_map(move |base| Price::from_raw(base as PriceRaw * scale_prec2, 2)),
]
}
fn quantity_strategy() -> impl Strategy<Value = Quantity> {
use crate::types::{fixed::FIXED_PRECISION, quantity::QuantityRaw};
let scale_prec2 = 10u64.pow(u32::from(FIXED_PRECISION - 2)) as QuantityRaw; let scale_prec8 = 10u64.pow(u32::from(FIXED_PRECISION - 8)) as QuantityRaw;
prop_oneof![
(1u64..=10000u64)
.prop_map(move |base| Quantity::from_raw(base as QuantityRaw * scale_prec2, 2)),
(1u64..=100u64)
.prop_map(move |base| Quantity::from_raw(base as QuantityRaw * scale_prec8, 8)),
(10000u64..=1000000u64)
.prop_map(move |base| Quantity::from_raw(base as QuantityRaw * scale_prec2, 2)),
]
}
fn book_order_strategy() -> impl Strategy<Value = BookOrder> {
(
prop::sample::select(vec![OrderSide::Buy, OrderSide::Sell]),
price_strategy(),
quantity_strategy(),
prop::num::u64::ANY.prop_filter("non-zero order id", |&id| id > 0),
)
.prop_map(|(side, price, size, order_id)| BookOrder::new(side, price, size, order_id))
}
fn positive_book_order_strategy() -> impl Strategy<Value = BookOrder> {
(
prop::sample::select(vec![OrderSide::Buy, OrderSide::Sell]),
price_strategy(),
positive_quantity_strategy(),
prop::num::u64::ANY.prop_filter("non-zero order id", |&id| id > 0),
)
.prop_map(|(side, price, size, order_id)| BookOrder::new(side, price, size, order_id))
.prop_filter("order must have positive size and valid price", |order| {
order.size.is_positive() && order.price.raw > 0
})
}
fn positive_quantity_strategy() -> impl Strategy<Value = Quantity> {
use crate::types::{fixed::FIXED_PRECISION, quantity::QuantityRaw};
let scale_prec2 = 10u64.pow(u32::from(FIXED_PRECISION - 2)) as QuantityRaw;
let scale_prec3 = 10u64.pow(u32::from(FIXED_PRECISION - 3)) as QuantityRaw;
prop_oneof![
(1u64..=1000u64)
.prop_map(move |base| Quantity::from_raw(base as QuantityRaw * scale_prec2, 2))
.prop_filter("quantity must be positive", |q| q.is_positive()),
(1000u64..=100000u64)
.prop_map(move |base| Quantity::from_raw(base as QuantityRaw * scale_prec3, 3))
.prop_filter("quantity must be positive", |q| q.is_positive()),
(10000u64..=1000000u64)
.prop_map(move |base| Quantity::from_raw(base as QuantityRaw * scale_prec2, 2))
.prop_filter("quantity must be positive", |q| q.is_positive()),
]
}
fn orderbook_operation_strategy() -> impl Strategy<Value = OrderBookOperation> {
prop_oneof![
6 => (positive_book_order_strategy(), prop::num::u8::ANY, prop::num::u64::ANY)
.prop_map(|(order, flags, seq)| OrderBookOperation::Add(order, flags, seq)),
4 => (book_order_strategy(), prop::num::u8::ANY, prop::num::u64::ANY)
.prop_map(|(order, flags, seq)| OrderBookOperation::Update(order, flags, seq)),
3 => (book_order_strategy(), prop::num::u8::ANY, prop::num::u64::ANY)
.prop_map(|(order, flags, seq)| OrderBookOperation::Delete(order, flags, seq)),
1 => prop::num::u64::ANY.prop_map(OrderBookOperation::Clear),
1 => prop::num::u64::ANY.prop_map(OrderBookOperation::ClearBids),
1 => prop::num::u64::ANY.prop_map(OrderBookOperation::ClearAsks),
]
}
fn orderbook_test_strategy() -> impl Strategy<Value = (BookType, Vec<OrderBookOperation>)> {
(
prop::sample::select(vec![BookType::L1_MBP, BookType::L2_MBP, BookType::L3_MBO]),
prop::collection::vec(orderbook_operation_strategy(), 10..=100),
)
}
fn sanitize_operations(
operations: Vec<OrderBookOperation>,
book_type: BookType,
) -> Vec<OrderBookOperation> {
use ahash::{AHashMap, AHashSet};
let mut live_order_ids: AHashMap<OrderSide, AHashSet<u64>> = AHashMap::new();
live_order_ids.insert(OrderSide::Buy, AHashSet::new());
live_order_ids.insert(OrderSide::Sell, AHashSet::new());
let mut sanitized = Vec::new();
for operation in operations {
match operation {
OrderBookOperation::Add(order, flags, seq) => {
if order.price.raw <= 0
|| order.price.raw == crate::types::price::PRICE_UNDEF
|| order.price.raw == crate::types::price::PRICE_ERROR
|| !order.size.is_positive()
{
continue;
}
let side_set = live_order_ids.get_mut(&order.side).unwrap();
if book_type == BookType::L1_MBP {
let mut l1_order = order;
l1_order.order_id = l1_order.side as u64;
side_set.insert(l1_order.order_id);
sanitized.push(OrderBookOperation::Add(l1_order, flags, seq));
} else {
if side_set.contains(&order.order_id) {
continue; }
side_set.insert(order.order_id);
sanitized.push(OrderBookOperation::Add(order, flags, seq));
}
}
OrderBookOperation::Update(mut order, flags, seq) => {
if order.price.raw <= 0
|| order.price.raw == crate::types::price::PRICE_UNDEF
|| order.price.raw == crate::types::price::PRICE_ERROR
{
continue;
}
if book_type == BookType::L1_MBP {
order.order_id = order.side as u64;
}
let side_set = live_order_ids.get_mut(&order.side).unwrap();
if side_set.contains(&order.order_id) {
if order.size.raw == 0 {
side_set.remove(&order.order_id);
}
sanitized.push(OrderBookOperation::Update(order, flags, seq));
}
}
OrderBookOperation::Delete(mut order, flags, seq) => {
if book_type == BookType::L1_MBP {
order.order_id = order.side as u64;
}
let side_set = live_order_ids.get_mut(&order.side).unwrap();
if side_set.contains(&order.order_id) {
side_set.remove(&order.order_id);
sanitized.push(OrderBookOperation::Delete(order, flags, seq));
}
}
OrderBookOperation::Clear(seq) => {
live_order_ids.get_mut(&OrderSide::Buy).unwrap().clear();
live_order_ids.get_mut(&OrderSide::Sell).unwrap().clear();
sanitized.push(OrderBookOperation::Clear(seq));
}
OrderBookOperation::ClearBids(seq) => {
live_order_ids.get_mut(&OrderSide::Buy).unwrap().clear();
sanitized.push(OrderBookOperation::ClearBids(seq));
}
OrderBookOperation::ClearAsks(seq) => {
live_order_ids.get_mut(&OrderSide::Sell).unwrap().clear();
sanitized.push(OrderBookOperation::ClearAsks(seq));
}
}
}
sanitized
}
fn test_orderbook_with_operations(book_type: BookType, operations: Vec<OrderBookOperation>) {
let instrument_id = InstrumentId::from("TEST.VENUE");
let mut book = OrderBook::new(instrument_id, book_type);
let mut last_sequence = 0u64;
let operations = sanitize_operations(operations, book_type);
for operation in operations {
let sequence = match &operation {
OrderBookOperation::Add(_, _, seq)
| OrderBookOperation::Update(_, _, seq)
| OrderBookOperation::Delete(_, _, seq)
| OrderBookOperation::Clear(seq)
| OrderBookOperation::ClearBids(seq)
| OrderBookOperation::ClearAsks(seq) => {
last_sequence = last_sequence.max(*seq);
last_sequence
}
};
let ts_event = UnixNanos::from(sequence);
match operation {
OrderBookOperation::Add(order, flags, _) => {
book.add(order, flags, sequence, ts_event);
}
OrderBookOperation::Update(order, flags, _) => {
book.update(order, flags, sequence, ts_event);
}
OrderBookOperation::Delete(order, flags, _) => {
book.delete(order, flags, sequence, ts_event);
}
OrderBookOperation::Clear(_) => {
book.clear(sequence, ts_event);
}
OrderBookOperation::ClearBids(_) => {
book.clear_bids(sequence, ts_event);
}
OrderBookOperation::ClearAsks(_) => {
book.clear_asks(sequence, ts_event);
}
}
assert!(
book.sequence >= last_sequence,
"Sequence should be monotonic: {} >= {}",
book.sequence,
last_sequence
);
assert!(
book.update_count > 0,
"Update count should be positive after operations"
);
if let Some(best_bid) = book.best_bid_price() {
assert!(
best_bid.raw != crate::types::price::PRICE_UNDEF
&& best_bid.raw != crate::types::price::PRICE_ERROR,
"Best bid should have valid price"
);
}
if let Some(best_ask) = book.best_ask_price() {
assert!(
best_ask.raw != crate::types::price::PRICE_UNDEF
&& best_ask.raw != crate::types::price::PRICE_ERROR,
"Best ask should have valid price"
);
}
if let Some(spread) = book.spread() {
assert!(spread.is_finite(), "Spread should be finite");
}
if let (Some(bid), Some(ask)) = (book.best_bid_price(), book.best_ask_price())
&& let Some(mid) = book.midpoint()
{
assert!(mid.is_finite(), "Midpoint should be finite");
if bid <= ask {
assert!(
mid >= bid.as_f64() && mid <= ask.as_f64(),
"Midpoint {mid} should be between bid {bid} and ask {ask}"
);
}
}
let bid_prices: Vec<_> = book.bids(None).map(|level| level.price.value).collect();
for i in 1..bid_prices.len() {
assert!(
bid_prices[i - 1] >= bid_prices[i],
"Bid prices should be in descending order: {} >= {}",
bid_prices[i - 1],
bid_prices[i]
);
}
let ask_prices: Vec<_> = book.asks(None).map(|level| level.price.value).collect();
for i in 1..ask_prices.len() {
assert!(
ask_prices[i - 1] <= ask_prices[i],
"Ask prices should be in ascending order: {} <= {}",
ask_prices[i - 1],
ask_prices[i]
);
}
for level in book.bids(None) {
assert!(
level.size() > 0.0,
"Bid level should have positive size: {}",
level.size()
);
}
for level in book.asks(None) {
assert!(
level.size() > 0.0,
"Ask level should have positive size: {}",
level.size()
);
}
last_sequence = sequence;
}
}
#[rstest]
fn prop_test_orderbook_operations() {
proptest!(|(config in orderbook_test_strategy())| {
let (book_type, operations) = config;
test_orderbook_with_operations(book_type, operations);
});
}
fn test_orderbook_basic_invariants(book_type: BookType, operations: Vec<OrderBookOperation>) {
let instrument_id = InstrumentId::from("TEST.VENUE");
let mut book = OrderBook::new(instrument_id, book_type);
let mut last_sequence = 0u64;
let operations = sanitize_operations(operations, book_type);
for operation in operations {
let sequence = match &operation {
OrderBookOperation::Add(_, _, seq)
| OrderBookOperation::Update(_, _, seq)
| OrderBookOperation::Delete(_, _, seq)
| OrderBookOperation::Clear(seq)
| OrderBookOperation::ClearBids(seq)
| OrderBookOperation::ClearAsks(seq) => {
last_sequence = last_sequence.max(*seq);
last_sequence
}
};
let ts_event = UnixNanos::from(sequence);
match operation {
OrderBookOperation::Add(order, flags, _) => {
book.add(order, flags, sequence, ts_event);
}
OrderBookOperation::Update(order, flags, _) => {
book.update(order, flags, sequence, ts_event);
}
OrderBookOperation::Delete(order, flags, _) => {
book.delete(order, flags, sequence, ts_event);
}
OrderBookOperation::Clear(_) => {
book.clear(sequence, ts_event);
}
OrderBookOperation::ClearBids(_) => {
book.clear_bids(sequence, ts_event);
}
OrderBookOperation::ClearAsks(_) => {
book.clear_asks(sequence, ts_event);
}
}
assert!(
book.sequence >= last_sequence,
"Sequence should be monotonic: {} >= {}",
book.sequence,
last_sequence
);
assert!(
book.update_count > 0,
"Update count should be positive after operations"
);
if let Some(best_bid) = book.best_bid_price() {
assert!(
best_bid.raw != crate::types::price::PRICE_UNDEF
&& best_bid.raw != crate::types::price::PRICE_ERROR,
"Best bid should have valid price"
);
}
if let Some(best_ask) = book.best_ask_price() {
assert!(
best_ask.raw != crate::types::price::PRICE_UNDEF
&& best_ask.raw != crate::types::price::PRICE_ERROR,
"Best ask should have valid price"
);
}
let bid_prices: Vec<_> = book.bids(None).map(|level| level.price.value).collect();
for i in 1..bid_prices.len() {
assert!(
bid_prices[i - 1] >= bid_prices[i],
"Bid prices should be in descending order: {} >= {}",
bid_prices[i - 1],
bid_prices[i]
);
}
let ask_prices: Vec<_> = book.asks(None).map(|level| level.price.value).collect();
for i in 1..ask_prices.len() {
assert!(
ask_prices[i - 1] <= ask_prices[i],
"Ask prices should be in ascending order: {} <= {}",
ask_prices[i - 1],
ask_prices[i]
);
}
for level in book.bids(None) {
assert!(
level.size() > 0.0,
"Bid level should have positive size: {}",
level.size()
);
}
for level in book.asks(None) {
assert!(
level.size() > 0.0,
"Ask level should have positive size: {}",
level.size()
);
}
last_sequence = sequence;
}
}
#[rstest]
fn prop_test_orderbook_basic_invariants() {
proptest!(|(config in orderbook_test_strategy())| {
let (book_type, operations) = config;
test_orderbook_basic_invariants(book_type, operations);
});
}
#[rstest]
fn test_sanitize_operations_skips_duplicate_adds() {
let operations = vec![
OrderBookOperation::Add(
BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(10),
42,
),
0,
1,
),
OrderBookOperation::Add(
BookOrder::new(OrderSide::Buy, Price::from("99.00"), Quantity::from(20), 42),
0,
2,
),
OrderBookOperation::Delete(
BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(10),
42,
),
0,
3,
),
OrderBookOperation::Delete(
BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(10),
42,
),
0,
4,
),
];
let sanitized = sanitize_operations(operations, BookType::L3_MBO);
assert_eq!(sanitized.len(), 2, "Should have 1 Add + 1 Delete");
if let OrderBookOperation::Add(order, _, _) = &sanitized[0] {
assert_eq!(order.order_id, 42);
assert_eq!(order.price, Price::from("100.00"));
}
let instrument_id = InstrumentId::from("TEST.VENUE");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
for operation in sanitized {
match operation {
OrderBookOperation::Add(order, flags, seq) => {
book.add(order, flags, seq, UnixNanos::from(seq));
}
OrderBookOperation::Delete(order, flags, seq) => {
book.delete(order, flags, seq, UnixNanos::from(seq));
}
_ => {}
}
}
assert_eq!(book.bids(None).count(), 0, "Book should be empty");
}
#[rstest]
fn test_sanitize_operations_l1_id_normalization() {
let operations = vec![
OrderBookOperation::Add(
BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(10),
999,
),
0,
1,
),
OrderBookOperation::Update(
BookOrder::new(
OrderSide::Buy,
Price::from("100.50"),
Quantity::from(15),
888,
),
0,
2,
),
OrderBookOperation::Delete(
BookOrder::new(
OrderSide::Buy,
Price::from("100.50"),
Quantity::from(15),
777,
),
0,
3,
),
];
let sanitized = sanitize_operations(operations, BookType::L1_MBP);
assert_eq!(sanitized.len(), 3, "All L1 operations should be kept");
if let OrderBookOperation::Add(order, _, _) = &sanitized[0] {
assert_eq!(order.order_id, 1, "L1 Buy Add should use order_id 1");
}
if let OrderBookOperation::Update(order, _, _) = &sanitized[1] {
assert_eq!(order.order_id, 1, "L1 Buy Update should use order_id 1");
}
if let OrderBookOperation::Delete(order, _, _) = &sanitized[2] {
assert_eq!(order.order_id, 1, "L1 Buy Delete should use order_id 1");
}
let instrument_id = InstrumentId::from("TEST.VENUE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
for operation in sanitized {
match operation {
OrderBookOperation::Add(order, flags, seq) => {
book.add(order, flags, seq, UnixNanos::from(seq));
}
OrderBookOperation::Update(order, flags, seq) => {
book.update(order, flags, seq, UnixNanos::from(seq));
}
OrderBookOperation::Delete(order, flags, seq) => {
book.delete(order, flags, seq, UnixNanos::from(seq));
}
_ => {}
}
}
assert_eq!(book.bids(None).count(), 0, "L1 book should be empty");
}
#[derive(Clone, Debug)]
enum L1Operation {
QuoteUpdate(Price, Quantity, Price, Quantity),
TradeUpdate(Price, Quantity, AggressorSide),
}
fn l1_operation_strategy() -> impl Strategy<Value = L1Operation> {
use crate::types::{fixed::FIXED_PRECISION, price::PriceRaw, quantity::QuantityRaw};
let price_scale = 10i64.pow(u32::from(FIXED_PRECISION - 2)) as PriceRaw;
let qty_scale = 10u64.pow(u32::from(FIXED_PRECISION - 2)) as QuantityRaw;
prop_oneof![
7 => {
(
(1i64..=10000i64).prop_map(move |base| Price::from_raw(base as PriceRaw * price_scale, 2)),
(1u64..=10000u64).prop_map(move |base| Quantity::from_raw(base as QuantityRaw * qty_scale, 2)),
(1i64..=10000i64).prop_map(move |base| Price::from_raw(base as PriceRaw * price_scale, 2)),
(1u64..=10000u64).prop_map(move |base| Quantity::from_raw(base as QuantityRaw * qty_scale, 2)),
).prop_map(|(bid_price, bid_size, ask_price, ask_size)| {
L1Operation::QuoteUpdate(bid_price, bid_size, ask_price, ask_size)
})
},
3 => (
(1i64..=10000i64).prop_map(move |base| Price::from_raw(base as PriceRaw * price_scale, 2)),
(1u64..=10000u64).prop_map(move |base| Quantity::from_raw(base as QuantityRaw * qty_scale, 2)),
prop::sample::select(vec![AggressorSide::Buyer, AggressorSide::Seller])
).prop_map(|(price, size, aggressor)| {
L1Operation::TradeUpdate(price, size, aggressor)
}),
]
}
fn test_l1_book_with_operations(operations: Vec<L1Operation>) {
let instrument_id = InstrumentId::from("TEST.VENUE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
for operation in operations {
match operation {
L1Operation::QuoteUpdate(bid_price, bid_size, ask_price, ask_size) => {
if bid_price.raw == crate::types::price::PRICE_UNDEF
|| bid_price.raw == crate::types::price::PRICE_ERROR
|| ask_price.raw == crate::types::price::PRICE_UNDEF
|| ask_price.raw == crate::types::price::PRICE_ERROR
|| bid_size.raw == 0
|| ask_size.raw == 0
{
continue;
}
let quote = QuoteTick::new(
instrument_id,
bid_price,
ask_price,
bid_size,
ask_size,
UnixNanos::default(),
UnixNanos::default(),
);
if book.update_quote_tick("e).is_err() {
continue; }
}
L1Operation::TradeUpdate(price, size, aggressor_side) => {
if price.raw == crate::types::price::PRICE_UNDEF
|| price.raw == crate::types::price::PRICE_ERROR
|| size.raw == 0
{
continue;
}
let trade = TradeTick::new(
instrument_id,
price,
size,
aggressor_side,
TradeId::from("1"),
UnixNanos::default(),
UnixNanos::default(),
);
if book.update_trade_tick(&trade).is_err() {
continue; }
}
}
assert!(
book.bids(None).count() <= 1,
"L1 book should have at most one bid level"
);
assert!(
book.asks(None).count() <= 1,
"L1 book should have at most one ask level"
);
}
}
#[rstest]
fn prop_test_l1_book_operations() {
proptest!(|(operations in prop::collection::vec(l1_operation_strategy(), 5..=50))| {
test_l1_book_with_operations(operations);
});
}
#[rstest]
fn test_apply_deltas_single_clear_no_f_last() {
let instrument_id = InstrumentId::from("TEST.SIM");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid = BookOrder::new(
OrderSide::Buy,
Price::from("100.0"),
Quantity::from("10.0"),
1,
);
book.add(bid, 0, 0, 0.into());
assert_eq!(book.bids(None).count(), 1);
let clear_delta = OrderBookDelta::clear(instrument_id, 0, 0.into(), 0.into());
assert!(!RecordFlag::F_LAST.matches(clear_delta.flags));
assert!(RecordFlag::F_SNAPSHOT.matches(clear_delta.flags));
let deltas = OrderBookDeltas::new(instrument_id, vec![clear_delta]);
book.apply_deltas(&deltas).unwrap();
assert_eq!(book.bids(None).count(), 0);
assert_eq!(book.asks(None).count(), 0);
}
#[rstest]
fn test_apply_deltas_empty_clear_to_empty_book() {
let instrument_id = InstrumentId::from("TEST.SIM");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
assert_eq!(book.bids(None).count(), 0);
assert_eq!(book.asks(None).count(), 0);
let clear_delta = OrderBookDelta::clear(instrument_id, 0, 0.into(), 0.into());
let deltas = OrderBookDeltas::new(instrument_id, vec![clear_delta]);
book.apply_deltas(&deltas).unwrap();
assert_eq!(book.bids(None).count(), 0);
assert_eq!(book.asks(None).count(), 0);
}
#[rstest]
fn test_apply_delta_resolves_side_from_bids_cache() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order1 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from("10"),
123,
);
let delta1 = OrderBookDelta::new(
instrument_id,
BookAction::Add,
order1,
0,
1,
0.into(),
0.into(),
);
book.apply_delta(&delta1).unwrap();
let order2 = BookOrder::new(
OrderSide::NoOrderSide,
Price::from("100.00"),
Quantity::from("5"),
123,
);
let delta2 = OrderBookDelta::new(
instrument_id,
BookAction::Update,
order2,
0,
2,
0.into(),
0.into(),
);
book.apply_delta(&delta2).unwrap();
let top_bid = book.bids(Some(1)).next().unwrap();
assert_eq!(top_bid.price.value, Price::from("100.00"));
assert_eq!(top_bid.first().unwrap().size, Quantity::from("5"));
}
#[rstest]
fn test_apply_delta_resolves_side_from_asks_cache() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order1 = BookOrder::new(
OrderSide::Sell,
Price::from("100.00"),
Quantity::from("10"),
456,
);
let delta1 = OrderBookDelta::new(
instrument_id,
BookAction::Add,
order1,
0,
1,
0.into(),
0.into(),
);
book.apply_delta(&delta1).unwrap();
let order2 = BookOrder::new(
OrderSide::NoOrderSide,
Price::from("100.00"),
Quantity::from("10"),
456,
);
let delta2 = OrderBookDelta::new(
instrument_id,
BookAction::Delete,
order2,
0,
2,
0.into(),
0.into(),
);
book.apply_delta(&delta2).unwrap();
assert_eq!(book.asks(None).count(), 0);
}
#[rstest]
fn test_apply_delta_error_when_order_not_found_for_side_resolution() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order = BookOrder::new(
OrderSide::NoOrderSide,
Price::from("100.00"),
Quantity::from("10"),
999, );
let delta = OrderBookDelta::new(
instrument_id,
BookAction::Add,
order,
0,
1,
0.into(),
0.into(),
);
let result = book.apply_delta(&delta);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
BookIntegrityError::NoOrderSide
));
}
#[rstest]
fn test_apply_delta_skips_update_delete_when_order_not_found() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order = BookOrder::new(
OrderSide::NoOrderSide,
Price::from("100.00"),
Quantity::from("10"),
999, );
let delta = OrderBookDelta::new(
instrument_id,
BookAction::Update,
order,
0,
1,
0.into(),
0.into(),
);
let result = book.apply_delta(&delta);
assert!(result.is_ok());
let delta2 = OrderBookDelta::new(
instrument_id,
BookAction::Delete,
order,
0,
2,
0.into(),
0.into(),
);
let result2 = book.apply_delta(&delta2);
assert!(result2.is_ok());
}
#[rstest]
fn test_apply_delta_no_order_side_with_zero_order_id_for_clear() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
let order1 = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from("10"),
123,
);
let delta1 = OrderBookDelta::new(
instrument_id,
BookAction::Add,
order1,
0,
1,
0.into(),
0.into(),
);
book.apply_delta(&delta1).unwrap();
let delta_clear = OrderBookDelta::clear(instrument_id, 2, 0.into(), 0.into());
book.apply_delta(&delta_clear).unwrap();
assert_eq!(book.bids(None).count(), 0);
assert_eq!(book.asks(None).count(), 0);
}
#[rstest]
fn test_l1_snapshot_tardis_style_selects_best_prices() {
let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let mut deltas = Vec::new();
deltas.push(OrderBookDelta::clear(instrument_id, 0, 0.into(), 0.into()));
for price in ["99.00", "100.00", "101.00"] {
let order = BookOrder::new(
OrderSide::Buy,
Price::from(price),
Quantity::from("10"),
0, );
deltas.push(OrderBookDelta::new(
instrument_id,
BookAction::Add,
order,
RecordFlag::F_SNAPSHOT as u8,
0,
0.into(),
0.into(),
));
}
let ask_prices = ["105.00", "104.00", "103.00", "102.00"];
for (i, price) in ask_prices.iter().enumerate() {
let order = BookOrder::new(
OrderSide::Sell,
Price::from(*price),
Quantity::from("10"),
0,
);
let flags = if i == ask_prices.len() - 1 {
RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
} else {
RecordFlag::F_SNAPSHOT as u8
};
deltas.push(OrderBookDelta::new(
instrument_id,
BookAction::Add,
order,
flags,
0,
0.into(),
0.into(),
));
}
let order_book_deltas = OrderBookDeltas::new(instrument_id, deltas);
book.apply_deltas(&order_book_deltas).unwrap();
assert_eq!(
book.best_bid_price(),
Some(Price::from("101.00")),
"L1 snapshot should select best bid (101) from all bid levels"
);
assert_eq!(
book.best_ask_price(),
Some(Price::from("102.00")),
"L1 snapshot should select best ask (102) from all ask levels"
);
}
#[rstest]
fn test_l1_consecutive_snapshots_clear_between() {
let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
let mut deltas1 = Vec::new();
deltas1.push(OrderBookDelta::clear(instrument_id, 0, 0.into(), 0.into()));
for price in ["100.00", "101.00"] {
deltas1.push(OrderBookDelta::new(
instrument_id,
BookAction::Add,
BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from("10"), 0),
RecordFlag::F_SNAPSHOT as u8,
0,
0.into(),
0.into(),
));
}
for (i, price) in ["103.00", "102.00"].iter().enumerate() {
let flags = if i == 1 {
RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
} else {
RecordFlag::F_SNAPSHOT as u8
};
deltas1.push(OrderBookDelta::new(
instrument_id,
BookAction::Add,
BookOrder::new(
OrderSide::Sell,
Price::from(*price),
Quantity::from("10"),
0,
),
flags,
0,
0.into(),
0.into(),
));
}
book.apply_deltas(&OrderBookDeltas::new(instrument_id, deltas1))
.unwrap();
assert_eq!(book.best_bid_price(), Some(Price::from("101.00")));
assert_eq!(book.best_ask_price(), Some(Price::from("102.00")));
let mut deltas2 = Vec::new();
deltas2.push(OrderBookDelta::clear(instrument_id, 0, 1.into(), 1.into()));
for price in ["95.00", "96.00"] {
deltas2.push(OrderBookDelta::new(
instrument_id,
BookAction::Add,
BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from("10"), 0),
RecordFlag::F_SNAPSHOT as u8,
0,
1.into(),
1.into(),
));
}
for (i, price) in ["108.00", "107.00"].iter().enumerate() {
let flags = if i == 1 {
RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
} else {
RecordFlag::F_SNAPSHOT as u8
};
deltas2.push(OrderBookDelta::new(
instrument_id,
BookAction::Add,
BookOrder::new(
OrderSide::Sell,
Price::from(*price),
Quantity::from("10"),
0,
),
flags,
0,
1.into(),
1.into(),
));
}
book.apply_deltas(&OrderBookDeltas::new(instrument_id, deltas2))
.unwrap();
assert_eq!(
book.best_bid_price(),
Some(Price::from("96.00")),
"Second snapshot should clear first, best bid is 96"
);
assert_eq!(
book.best_ask_price(),
Some(Price::from("107.00")),
"Second snapshot should clear first, best ask is 107"
);
}
#[rstest]
#[case::buy_crosses_all_asks(
OrderSide::Buy,
"2.020", // price above all asks
vec![("2.000", 1.0), ("2.010", 2.0), ("2.011", 3.0)], )]
#[case::buy_crosses_some_asks(
OrderSide::Buy,
"2.010", // price at middle ask
vec![("2.000", 1.0), ("2.010", 2.0)], )]
#[case::buy_crosses_one_ask(
OrderSide::Buy,
"2.005", // price between first and second ask
vec![("2.000", 1.0)], )]
#[case::buy_crosses_no_asks(
OrderSide::Buy,
"1.999", // price below all asks
vec![], // expected: no levels
)]
#[case::sell_crosses_all_bids(
OrderSide::Sell,
"0.980", // price below all bids
vec![("1.000", 1.0), ("0.990", 2.0), ("0.989", 3.0)], )]
#[case::sell_crosses_some_bids(
OrderSide::Sell,
"0.990", // price at middle bid
vec![("1.000", 1.0), ("0.990", 2.0)], )]
#[case::sell_crosses_one_bid(
OrderSide::Sell,
"0.995", // price between first and second bid
vec![("1.000", 1.0)], )]
#[case::sell_crosses_no_bids(
OrderSide::Sell,
"1.001", // price above all bids
vec![], // expected: no levels
)]
fn test_get_all_crossed_levels(
#[case] order_side: OrderSide,
#[case] price_str: &str,
#[case] expected: Vec<(&str, f64)>,
) {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("2.011"),
Quantity::from("3.0"),
0,
),
0,
0,
1.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("2.010"),
Quantity::from("2.0"),
0,
),
0,
1,
2.into(),
);
book.add(
BookOrder::new(
OrderSide::Sell,
Price::from("2.000"),
Quantity::from("1.0"),
0,
),
0,
2,
3.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("1.000"),
Quantity::from("1.0"),
0,
),
0,
3,
4.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("0.990"),
Quantity::from("2.0"),
0,
),
0,
4,
5.into(),
);
book.add(
BookOrder::new(
OrderSide::Buy,
Price::from("0.989"),
Quantity::from("3.0"),
0,
),
0,
5,
6.into(),
);
let price = Price::from(price_str);
let size_precision = 1;
let levels = book.get_all_crossed_levels(order_side, price, size_precision);
assert_eq!(
levels.len(),
expected.len(),
"Expected {} levels, was {}",
expected.len(),
levels.len()
);
for (i, (exp_price, exp_size)) in expected.iter().enumerate() {
assert_eq!(
levels[i].0,
Price::from(*exp_price),
"Level {i} price mismatch"
);
assert_eq!(levels[i].1.as_f64(), *exp_size, "Level {i} size mismatch");
}
}
#[rstest]
fn test_to_deltas_empty_book_has_f_last_on_clear() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let deltas = book.to_deltas(0.into(), 0.into());
assert_eq!(deltas.deltas.len(), 1);
assert_eq!(deltas.deltas[0].action, BookAction::Clear);
assert!(RecordFlag::F_LAST.matches(deltas.deltas[0].flags));
assert!(RecordFlag::F_SNAPSHOT.matches(deltas.deltas[0].flags));
}
#[rstest]
fn test_to_deltas_non_empty_book_has_f_last_on_last_order() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid = BookOrder::new(OrderSide::Buy, Price::from("100.00"), Quantity::from(10), 1);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from(20),
2,
);
book.add(bid, 0, 1, 1.into());
book.add(ask, 0, 2, 2.into());
let deltas = book.to_deltas(0.into(), 0.into());
assert_eq!(deltas.deltas.len(), 3);
assert_eq!(deltas.deltas[0].action, BookAction::Clear);
assert!(!RecordFlag::F_LAST.matches(deltas.deltas[0].flags));
assert_eq!(deltas.deltas[1].action, BookAction::Add);
assert!(!RecordFlag::F_LAST.matches(deltas.deltas[1].flags));
assert!(RecordFlag::F_SNAPSHOT.matches(deltas.deltas[1].flags));
assert_eq!(deltas.deltas[2].action, BookAction::Add);
assert!(RecordFlag::F_LAST.matches(deltas.deltas[2].flags));
assert!(RecordFlag::F_SNAPSHOT.matches(deltas.deltas[2].flags));
}
fn make_delta(
instrument_id: InstrumentId,
action: BookAction,
side: OrderSide,
price: &str,
size: &str,
order_id: u64,
ts: u64,
) -> OrderBookDelta {
OrderBookDelta::new(
instrument_id,
action,
BookOrder::new(side, Price::from(price), Quantity::from(size), order_id),
0,
0,
UnixNanos::from(ts),
UnixNanos::from(ts),
)
}
#[rstest]
#[should_panic(expected = "must not be empty")]
fn test_deltas_to_quotes_panics_on_empty() {
OrderBook::deltas_to_quotes(BookType::L3_MBO, &[]);
}
#[rstest]
fn test_deltas_to_quotes_no_quotes_from_single_side() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![make_delta(
id,
BookAction::Add,
OrderSide::Buy,
"100.00",
"10",
1,
1000,
)];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert!(quotes.is_empty());
}
#[rstest]
fn test_deltas_to_quotes_emits_on_two_sided_book() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
2000,
),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 1);
assert_eq!(quotes[0].bid_price, Price::from("99.00"));
assert_eq!(quotes[0].ask_price, Price::from("101.00"));
assert_eq!(quotes[0].ts_event, UnixNanos::from(2000u64));
}
#[rstest]
fn test_deltas_to_quotes_suppresses_duplicate_bbo() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
2000,
),
make_delta(id, BookAction::Add, OrderSide::Buy, "98.00", "5", 3, 3000),
make_delta(id, BookAction::Add, OrderSide::Sell, "102.00", "5", 4, 4000),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 1, "Non-BBO changes should not produce quotes");
}
#[rstest]
fn test_deltas_to_quotes_emits_on_bid_improve() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
2000,
),
make_delta(id, BookAction::Add, OrderSide::Buy, "100.00", "5", 3, 3000),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 2);
assert_eq!(quotes[0].bid_price, Price::from("99.00"));
assert_eq!(quotes[1].bid_price, Price::from("100.00"));
assert_eq!(quotes[1].ask_price, Price::from("101.00"));
}
#[rstest]
fn test_deltas_to_quotes_emits_on_ask_improve() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
2000,
),
make_delta(id, BookAction::Add, OrderSide::Sell, "100.50", "5", 3, 3000),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 2);
assert_eq!(quotes[1].ask_price, Price::from("100.50"));
}
#[rstest]
fn test_deltas_to_quotes_emits_on_cancel_changes_bbo() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(id, BookAction::Add, OrderSide::Buy, "98.00", "10", 2, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
3,
2000,
),
make_delta(
id,
BookAction::Delete,
OrderSide::Buy,
"99.00",
"0",
1,
3000,
),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 2);
assert_eq!(quotes[0].bid_price, Price::from("99.00"));
assert_eq!(quotes[1].bid_price, Price::from("98.00"));
assert_eq!(quotes[1].ask_price, Price::from("101.00"));
}
#[rstest]
fn test_deltas_to_quotes_preserves_timestamps() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(
id,
BookAction::Add,
OrderSide::Buy,
"99.00",
"10",
1,
100_000,
),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
200_000,
),
make_delta(
id,
BookAction::Add,
OrderSide::Buy,
"100.00",
"5",
3,
300_000,
),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes[0].ts_event, UnixNanos::from(200_000u64));
assert_eq!(quotes[0].ts_init, UnixNanos::from(200_000u64));
assert_eq!(quotes[1].ts_event, UnixNanos::from(300_000u64));
assert_eq!(quotes[1].ts_init, UnixNanos::from(300_000u64));
}
#[rstest]
fn test_deltas_to_quotes_preserves_instrument_id() {
let id = InstrumentId::from("AAPL.XNAS");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "150.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"151.00",
"10",
2,
2000,
),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes[0].instrument_id, id);
}
#[rstest]
fn test_deltas_to_quotes_works_with_l2_book() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 0, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
0,
2000,
),
make_delta(id, BookAction::Add, OrderSide::Buy, "100.00", "5", 0, 3000),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L2_MBP, &deltas);
assert_eq!(quotes.len(), 2);
assert_eq!(quotes[1].bid_price, Price::from("100.00"));
}
#[rstest]
fn test_deltas_to_quotes_multiple_bbo_changes() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
2000,
),
make_delta(id, BookAction::Add, OrderSide::Buy, "99.50", "5", 3, 3000),
make_delta(id, BookAction::Add, OrderSide::Sell, "100.50", "5", 4, 4000),
make_delta(
id,
BookAction::Delete,
OrderSide::Buy,
"99.50",
"0",
3,
5000,
),
make_delta(
id,
BookAction::Delete,
OrderSide::Sell,
"100.50",
"0",
4,
6000,
),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 5);
assert_eq!(quotes[0].bid_price, Price::from("99.00"));
assert_eq!(quotes[0].ask_price, Price::from("101.00"));
assert_eq!(quotes[1].bid_price, Price::from("99.50"));
assert_eq!(quotes[1].ask_price, Price::from("101.00"));
assert_eq!(quotes[2].bid_price, Price::from("99.50"));
assert_eq!(quotes[2].ask_price, Price::from("100.50"));
assert_eq!(quotes[3].bid_price, Price::from("99.00"));
assert_eq!(quotes[3].ask_price, Price::from("100.50"));
assert_eq!(quotes[4].bid_price, Price::from("99.00"));
assert_eq!(quotes[4].ask_price, Price::from("101.00"));
}
#[rstest]
fn test_deltas_to_quotes_emits_after_clear_with_same_prices() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
2,
2000,
),
OrderBookDelta::clear(id, 0, 3000.into(), 3000.into()),
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 3, 4000),
make_delta(
id,
BookAction::Add,
OrderSide::Sell,
"101.00",
"10",
4,
5000,
),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 2);
assert_eq!(quotes[0].ts_event, UnixNanos::from(2000));
assert_eq!(quotes[1].ts_event, UnixNanos::from(5000));
}
#[rstest]
fn test_deltas_to_quotes_aggregates_level_sizes_for_l3() {
let id = InstrumentId::from("TEST.VENUE");
let deltas = vec![
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "10", 1, 1000),
make_delta(id, BookAction::Add, OrderSide::Buy, "99.00", "20", 2, 2000),
make_delta(id, BookAction::Add, OrderSide::Sell, "101.00", "5", 3, 3000),
];
let quotes = OrderBook::deltas_to_quotes(BookType::L3_MBO, &deltas);
assert_eq!(quotes.len(), 1);
assert_eq!(quotes[0].bid_size, Quantity::from("30"));
assert_eq!(quotes[0].ask_size, Quantity::from("5"));
}
fn create_book_with_levels(bids: &[(&str, i64, u64)], asks: &[(&str, i64, u64)]) -> OrderBook {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
for &(price, size, id) in bids {
let order = BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id);
book.add(order, 0, id, id.into());
}
for &(price, size, id) in asks {
let order = BookOrder::new(
OrderSide::Sell,
Price::from(price),
Quantity::from(size),
id,
);
book.add(order, 0, id, id.into());
}
book
}
#[rstest]
fn test_bids_range_down_to_returns_levels_at_or_above_price() {
let book = create_book_with_levels(
&[
("100.00", 10, 1),
("99.00", 20, 2),
("98.00", 30, 3),
("97.00", 40, 4),
],
&[],
);
let bound = BookPrice::new(Price::from("98.00"), OrderSideSpecified::Buy);
assert_eq!(book.bids.levels.range(..=bound).count(), 3);
}
#[rstest]
fn test_asks_range_up_to_returns_levels_at_or_below_price() {
let book = create_book_with_levels(
&[],
&[
("101.00", 10, 1),
("102.00", 20, 2),
("103.00", 30, 3),
("104.00", 40, 4),
],
);
let bound = BookPrice::new(Price::from("103.00"), OrderSideSpecified::Sell);
assert_eq!(book.asks.levels.range(..=bound).count(), 3);
}
#[rstest]
fn test_bids_range_down_to_empty_when_price_above_all_bids() {
let book = create_book_with_levels(&[("100.00", 10, 1), ("99.00", 20, 2)], &[]);
let bound = BookPrice::new(Price::from("101.00"), OrderSideSpecified::Buy);
assert_eq!(book.bids.levels.range(..=bound).count(), 0);
}
#[rstest]
fn test_asks_range_up_to_empty_when_price_below_all_asks() {
let book = create_book_with_levels(&[], &[("101.00", 10, 1), ("102.00", 20, 2)]);
let bound = BookPrice::new(Price::from("100.00"), OrderSideSpecified::Sell);
assert_eq!(book.asks.levels.range(..=bound).count(), 0);
}
#[rstest]
fn test_bids_range_down_to_returns_all_at_lowest_bid() {
let book = create_book_with_levels(
&[("100.00", 10, 1), ("99.00", 20, 2), ("98.00", 30, 3)],
&[],
);
let bound = BookPrice::new(Price::from("98.00"), OrderSideSpecified::Buy);
assert_eq!(book.bids.levels.range(..=bound).count(), 3);
}
#[rstest]
fn test_asks_range_up_to_returns_all_at_highest_ask() {
let book = create_book_with_levels(
&[],
&[("101.00", 10, 1), ("102.00", 20, 2), ("103.00", 30, 3)],
);
let bound = BookPrice::new(Price::from("103.00"), OrderSideSpecified::Sell);
assert_eq!(book.asks.levels.range(..=bound).count(), 3);
}
#[rstest]
fn test_bids_range_down_to_single_exact_top() {
let book = create_book_with_levels(
&[("100.00", 10, 1), ("99.00", 20, 2), ("98.00", 30, 3)],
&[],
);
let bound = BookPrice::new(Price::from("100.00"), OrderSideSpecified::Buy);
let levels: Vec<_> = book.bids.levels.range(..=bound).collect();
assert_eq!(levels.len(), 1);
assert_eq!(levels[0].0.value, Price::from("100.00"));
}
#[rstest]
fn test_asks_range_up_to_single_exact_bottom() {
let book = create_book_with_levels(
&[],
&[("101.00", 10, 1), ("102.00", 20, 2), ("103.00", 30, 3)],
);
let bound = BookPrice::new(Price::from("101.00"), OrderSideSpecified::Sell);
let levels: Vec<_> = book.asks.levels.range(..=bound).collect();
assert_eq!(levels.len(), 1);
assert_eq!(levels[0].0.value, Price::from("101.00"));
}
#[rstest]
fn test_book_get_levels_for_price_buy_crosses_two_levels() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("1.001"),
Quantity::from("10.0"),
0,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("1.002"),
Quantity::from("20.0"),
0,
);
let ask3 = BookOrder::new(
OrderSide::Sell,
Price::from("1.003"),
Quantity::from("30.0"),
0,
);
book.add(ask1, 0, 1, 1.into());
book.add(ask2, 0, 2, 2.into());
book.add(ask3, 0, 3, 3.into());
let result = book.get_all_crossed_levels(OrderSide::Buy, Price::from("1.002"), 1);
assert_eq!(
result,
vec![
(Price::from("1.001"), Quantity::from("10.0")),
(Price::from("1.002"), Quantity::from("20.0")),
]
);
}
#[rstest]
fn test_book_get_levels_for_price_sell_crosses_two_levels() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid1 = BookOrder::new(
OrderSide::Buy,
Price::from("1.003"),
Quantity::from("10.0"),
0,
);
let bid2 = BookOrder::new(
OrderSide::Buy,
Price::from("1.002"),
Quantity::from("20.0"),
0,
);
let bid3 = BookOrder::new(
OrderSide::Buy,
Price::from("1.001"),
Quantity::from("30.0"),
0,
);
book.add(bid1, 0, 1, 1.into());
book.add(bid2, 0, 2, 2.into());
book.add(bid3, 0, 3, 3.into());
let result = book.get_all_crossed_levels(OrderSide::Sell, Price::from("1.002"), 1);
assert_eq!(
result,
vec![
(Price::from("1.003"), Quantity::from("10.0")),
(Price::from("1.002"), Quantity::from("20.0")),
]
);
}
#[rstest]
fn test_book_get_levels_for_price_no_levels_crossed() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("1.001"),
Quantity::from("10.0"),
0,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("1.002"),
Quantity::from("20.0"),
0,
);
book.add(ask1, 0, 1, 1.into());
book.add(ask2, 0, 2, 2.into());
let result = book.get_all_crossed_levels(OrderSide::Buy, Price::from("0.999"), 1);
assert!(result.is_empty());
}
#[rstest]
fn test_book_get_levels_for_price_all_levels_crossed() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let ask1 = BookOrder::new(
OrderSide::Sell,
Price::from("1.001"),
Quantity::from("10.0"),
0,
);
let ask2 = BookOrder::new(
OrderSide::Sell,
Price::from("1.002"),
Quantity::from("20.0"),
0,
);
let ask3 = BookOrder::new(
OrderSide::Sell,
Price::from("1.003"),
Quantity::from("30.0"),
0,
);
book.add(ask1, 0, 1, 1.into());
book.add(ask2, 0, 2, 2.into());
book.add(ask3, 0, 3, 3.into());
let result = book.get_all_crossed_levels(OrderSide::Buy, Price::from("2.000"), 1);
assert_eq!(
result,
vec![
(Price::from("1.001"), Quantity::from("10.0")),
(Price::from("1.002"), Quantity::from("20.0")),
(Price::from("1.003"), Quantity::from("30.0")),
]
);
}
#[rstest]
fn test_book_get_levels_for_price_empty_book() {
let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
let book = OrderBook::new(instrument_id, BookType::L2_MBP);
let result = book.get_all_crossed_levels(OrderSide::Buy, Price::from("1.000"), 1);
assert!(result.is_empty());
}
#[rstest]
fn test_book_integrity_locked_market_is_valid() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
let bid = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from(100),
1,
);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("100.00"),
Quantity::from(100),
2,
);
book.add(bid, 0, 1, 1.into());
book.add(ask, 0, 2, 2.into());
assert_eq!(book_check_integrity(&book), Ok(()));
}