use orderbook_rs::orderbook::trade::{TradeListener, TradeResult};
use orderbook_rs::{OrderBook, OrderBookError, ReferencePriceSource, RiskConfig};
use pricelevel::{Hash32, Id, OrderUpdate, Price, Quantity, Side, TimeInForce};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
fn account(byte: u8) -> Hash32 {
Hash32::new([byte; 32])
}
fn book_with_trade_counter() -> (OrderBook<()>, Arc<AtomicU64>) {
let count = Arc::new(AtomicU64::new(0));
let listener_count = Arc::clone(&count);
let listener: TradeListener = Arc::new(move |_: &TradeResult| {
listener_count.fetch_add(1, Ordering::Relaxed);
});
let book = OrderBook::<()>::with_trade_listener("TEST", listener);
(book, count)
}
#[test]
fn post_only_modify_that_would_cross_leaves_original_untouched_and_emits_no_trade() {
let (book, trade_count) = book_with_trade_counter();
book.add_limit_order(Id::new_uuid(), 110, 10, Side::Sell, TimeInForce::Gtc, None)
.expect("resting ask admitted");
let buy_id = Id::new_uuid();
book.add_post_only_order(buy_id, 100, 5, Side::Buy, TimeInForce::Gtc, None)
.expect("post-only buy admitted");
assert_eq!(trade_count.load(Ordering::Relaxed), 0);
let result = book.update_order(OrderUpdate::UpdatePrice {
order_id: buy_id,
new_price: Price::new(115),
});
match result {
Err(OrderBookError::PriceCrossing { price, side, .. }) => {
assert_eq!(price, 115);
assert_eq!(side, Side::Buy);
}
other => panic!("expected PriceCrossing, got {other:?}"),
}
let survivor = book
.get_order(buy_id)
.expect("original order still resting");
assert_eq!(survivor.id(), buy_id);
assert_eq!(survivor.price().as_u128(), 100, "price unchanged");
assert_eq!(
survivor.visible_quantity(),
Quantity::new(5),
"quantity unchanged"
);
assert_eq!(survivor.side(), Side::Buy);
assert_eq!(book.best_bid(), Some(100));
assert_eq!(
trade_count.load(Ordering::Relaxed),
0,
"a rejected modify must not emit any trade"
);
}
#[test]
fn post_only_replace_that_would_cross_leaves_original_untouched() {
let book: OrderBook<()> = OrderBook::new("TEST");
book.add_limit_order(Id::new_uuid(), 110, 10, Side::Sell, TimeInForce::Gtc, None)
.expect("resting ask admitted");
let buy_id = Id::new_uuid();
book.add_post_only_order(buy_id, 100, 5, Side::Buy, TimeInForce::Gtc, None)
.expect("post-only buy admitted");
let result = book.update_order(OrderUpdate::Replace {
order_id: buy_id,
price: Price::new(120),
quantity: Quantity::new(7),
side: Side::Buy,
});
assert!(
matches!(result, Err(OrderBookError::PriceCrossing { .. })),
"expected PriceCrossing, got {result:?}"
);
let survivor = book
.get_order(buy_id)
.expect("original survives the rejected replace");
assert_eq!(survivor.price().as_u128(), 100);
assert_eq!(survivor.visible_quantity(), Quantity::new(5));
}
#[test]
fn modify_outside_price_band_leaves_original_untouched() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_risk_config(RiskConfig::new().with_price_band_bps(100, ReferencePriceSource::Mid));
let acct = account(31);
book.add_limit_order_with_user(
Id::new_uuid(),
90,
10,
Side::Buy,
TimeInForce::Gtc,
acct,
None,
)
.expect("bid admitted");
book.add_limit_order_with_user(
Id::new_uuid(),
110,
10,
Side::Sell,
TimeInForce::Gtc,
acct,
None,
)
.expect("ask admitted");
let order_id = Id::new_uuid();
book.add_limit_order_with_user(order_id, 99, 5, Side::Buy, TimeInForce::Gtc, acct, None)
.expect("order admitted within band");
let result = book.update_order(OrderUpdate::UpdatePrice {
order_id,
new_price: Price::new(200),
});
assert!(
matches!(result, Err(OrderBookError::RiskPriceBand { .. })),
"expected RiskPriceBand, got {result:?}"
);
let survivor = book.get_order(order_id).expect("original survives");
assert_eq!(survivor.price().as_u128(), 99);
assert_eq!(survivor.visible_quantity(), Quantity::new(5));
}
#[test]
fn modify_over_max_notional_leaves_original_untouched() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_risk_config(RiskConfig::new().with_max_notional_per_account(1_000));
let acct = account(32);
let order_id = Id::new_uuid();
book.add_limit_order_with_user(order_id, 100, 5, Side::Buy, TimeInForce::Gtc, acct, None)
.expect("order admitted within notional");
let result = book.update_order(OrderUpdate::UpdatePriceAndQuantity {
order_id,
new_price: Price::new(100),
new_quantity: Quantity::new(20),
});
match result {
Err(OrderBookError::RiskMaxNotional {
account: a,
attempted,
limit,
..
}) => {
assert_eq!(a, acct);
assert_eq!(attempted, 2_000);
assert_eq!(limit, 1_000);
}
other => panic!("expected RiskMaxNotional, got {other:?}"),
}
let survivor = book.get_order(order_id).expect("original survives");
assert_eq!(survivor.price().as_u128(), 100);
assert_eq!(survivor.visible_quantity(), Quantity::new(5));
}
#[test]
fn modify_at_max_open_orders_succeeds() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_risk_config(RiskConfig::new().with_max_open_orders_per_account(2));
let acct = account(33);
let target_id = Id::new_uuid();
book.add_limit_order_with_user(target_id, 100, 3, Side::Buy, TimeInForce::Gtc, acct, None)
.expect("first order admitted");
book.add_limit_order_with_user(
Id::new_uuid(),
101,
3,
Side::Buy,
TimeInForce::Gtc,
acct,
None,
)
.expect("second order admitted (now at limit)");
let result = book.update_order(OrderUpdate::UpdatePrice {
order_id: target_id,
new_price: Price::new(95),
});
assert!(
result.is_ok(),
"modify at max_open_orders must succeed, got {result:?}"
);
let modified = book.get_order(target_id).expect("modified order resting");
assert_eq!(modified.price().as_u128(), 95, "price was updated");
assert_eq!(modified.visible_quantity(), Quantity::new(3));
}