orderbook-rs 0.10.0

A high-performance, lock-free price level implementation for limit order books in Rust. This library provides the building blocks for creating efficient trading systems with support for multiple order types and concurrent access patterns.
Documentation
//! Integration tests for the operational kill switch on `OrderBook<T>`.
//!
//! The kill switch halts new flow (`submit_*`, `add_order`, non-cancel
//! `update_order`) without dropping the book, while keeping cancel and
//! mass-cancel paths open so operators can drain the resting book.

#[cfg(test)]
mod tests_kill_switch {
    use orderbook_rs::orderbook::order_state::{OrderStateTracker, OrderStatus};
    use orderbook_rs::{OrderBook, OrderBookError, RejectReason};
    use pricelevel::{Hash32, Id, OrderUpdate, Price, Quantity, Side, TimeInForce};

    fn new_book() -> OrderBook<()> {
        OrderBook::new("TEST")
    }

    fn book_with_tracker() -> OrderBook<()> {
        let mut book = OrderBook::<()>::new("TEST");
        book.set_order_state_tracker(OrderStateTracker::new());
        book
    }

    // ───────────────────────────────────────────────────────────────────
    // Submit / add gates
    // ───────────────────────────────────────────────────────────────────

    #[test]
    fn submit_market_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        // Seed liquidity so the only failure mode is the kill switch.
        book.add_limit_order(Id::new_uuid(), 100, 10, Side::Sell, TimeInForce::Gtc, None)
            .expect("seed resting ask");

        book.engage_kill_switch();

        let result = book.submit_market_order(Id::new_uuid(), 5, Side::Buy);
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
    }

    #[test]
    fn submit_market_with_user_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        book.add_limit_order(Id::new_uuid(), 100, 10, Side::Sell, TimeInForce::Gtc, None)
            .expect("seed resting ask");

        book.engage_kill_switch();

        let result = book.submit_market_order_with_user(
            Id::new_uuid(),
            5,
            Side::Buy,
            Hash32::new([1u8; 32]),
        );
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
    }

    #[test]
    fn add_order_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        book.engage_kill_switch();

        let result =
            book.add_limit_order(Id::new_uuid(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
        assert_eq!(book.best_bid(), None, "rejected order must not rest");
    }

    // ───────────────────────────────────────────────────────────────────
    // update_order — non-cancel variants are gated, Cancel passes
    // ───────────────────────────────────────────────────────────────────

    #[test]
    fn update_order_price_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        let order_id = Id::new_uuid();
        book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        book.engage_kill_switch();

        let result = book.update_order(OrderUpdate::UpdatePrice {
            order_id,
            new_price: Price::new(101),
        });
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
        // Original order untouched.
        assert!(book.get_order(order_id).is_some());
    }

    #[test]
    fn update_order_quantity_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        let order_id = Id::new_uuid();
        book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        book.engage_kill_switch();

        let result = book.update_order(OrderUpdate::UpdateQuantity {
            order_id,
            new_quantity: Quantity::new(20),
        });
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
    }

    #[test]
    fn update_order_price_and_quantity_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        let order_id = Id::new_uuid();
        book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        book.engage_kill_switch();

        let result = book.update_order(OrderUpdate::UpdatePriceAndQuantity {
            order_id,
            new_price: Price::new(99),
            new_quantity: Quantity::new(7),
        });
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
    }

    #[test]
    fn update_order_replace_under_kill_switch_returns_kill_switch_active() {
        let book = new_book();
        let order_id = Id::new_uuid();
        book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        book.engage_kill_switch();

        let result = book.update_order(OrderUpdate::Replace {
            order_id,
            price: Price::new(99),
            quantity: Quantity::new(7),
            side: Side::Buy,
        });
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "expected KillSwitchActive, got {result:?}"
        );
    }

    #[test]
    fn update_order_cancel_under_kill_switch_succeeds() {
        let book = new_book();
        let order_id = Id::new_uuid();
        book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        book.engage_kill_switch();

        // Cancel must pass through — operators rely on this to drain.
        let result = book.update_order(OrderUpdate::Cancel { order_id });
        assert!(
            result.is_ok(),
            "Cancel under kill switch must succeed; got {result:?}"
        );
        assert!(
            book.get_order(order_id).is_none(),
            "order must be cancelled"
        );
    }

    // ───────────────────────────────────────────────────────────────────
    // Cancel paths are NOT gated
    // ───────────────────────────────────────────────────────────────────

    #[test]
    fn cancel_order_under_kill_switch_succeeds() {
        let book = new_book();
        let order_id = Id::new_uuid();
        book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        book.engage_kill_switch();

        let cancelled = book
            .cancel_order(order_id)
            .expect("cancel under kill switch");
        assert!(
            cancelled.is_some(),
            "cancel should return the cancelled order"
        );
        assert!(book.get_order(order_id).is_none());
    }

    #[test]
    fn mass_cancel_by_side_under_kill_switch_succeeds() {
        let book = new_book();
        for price in [99, 100, 101] {
            book.add_limit_order(Id::new_uuid(), price, 5, Side::Buy, TimeInForce::Gtc, None)
                .expect("seed bid");
        }
        book.add_limit_order(Id::new_uuid(), 110, 5, Side::Sell, TimeInForce::Gtc, None)
            .expect("seed ask");

        book.engage_kill_switch();

        let result = book.cancel_orders_by_side(Side::Buy);
        assert_eq!(result.cancelled_count(), 3, "all bids must be cancelled");
        assert_eq!(book.best_bid(), None);
        assert_eq!(book.best_ask(), Some(110), "ask side untouched");
    }

    #[test]
    fn cancel_all_under_kill_switch_succeeds() {
        let book = new_book();
        for price in [99, 100] {
            book.add_limit_order(Id::new_uuid(), price, 5, Side::Buy, TimeInForce::Gtc, None)
                .expect("seed bid");
        }
        for price in [110, 111] {
            book.add_limit_order(Id::new_uuid(), price, 5, Side::Sell, TimeInForce::Gtc, None)
                .expect("seed ask");
        }

        book.engage_kill_switch();

        let result = book.cancel_all_orders();
        assert_eq!(result.cancelled_count(), 4);
        assert_eq!(book.best_bid(), None);
        assert_eq!(book.best_ask(), None);
    }

    // ───────────────────────────────────────────────────────────────────
    // Release resumes new flow; tracker integration
    // ───────────────────────────────────────────────────────────────────

    #[test]
    fn release_then_submit_succeeds() {
        let book = new_book();
        book.add_limit_order(Id::new_uuid(), 100, 10, Side::Sell, TimeInForce::Gtc, None)
            .expect("seed resting ask");

        book.engage_kill_switch();
        let blocked = book.submit_market_order(Id::new_uuid(), 5, Side::Buy);
        assert!(matches!(blocked, Err(OrderBookError::KillSwitchActive)));

        book.release_kill_switch();
        let resumed = book.submit_market_order(Id::new_uuid(), 5, Side::Buy);
        assert!(
            resumed.is_ok(),
            "submit must succeed after release; got {resumed:?}"
        );
    }

    #[test]
    fn tracker_records_rejected_on_kill_switch() {
        let book = book_with_tracker();
        book.engage_kill_switch();

        let order_id = Id::new_uuid();
        let result = book.add_limit_order(order_id, 100, 10, Side::Buy, TimeInForce::Gtc, None);
        assert!(matches!(result, Err(OrderBookError::KillSwitchActive)));

        let status = book.order_status(order_id);
        match status {
            Some(OrderStatus::Rejected { reason }) => {
                assert_eq!(reason, RejectReason::KillSwitchActive);
            }
            other => panic!("expected Rejected with kill switch reason, got {other:?}"),
        }
    }

    // ───────────────────────────────────────────────────────────────────
    // Snapshot round-trip
    // ───────────────────────────────────────────────────────────────────

    #[test]
    fn kill_switch_state_round_trips_through_snapshot() {
        let original = OrderBook::<()>::new("TEST");
        original
            .add_limit_order(Id::new_uuid(), 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");
        original.engage_kill_switch();
        assert!(original.is_kill_switch_engaged());

        // Serialize to JSON, deserialize, restore — the canonical disaster
        // recovery path.
        let json = original
            .snapshot_to_json(10)
            .expect("serialize snapshot to json");

        let mut restored = OrderBook::<()>::new("TEST");
        restored
            .restore_from_snapshot_json(&json)
            .expect("restore from json");

        assert!(
            restored.is_kill_switch_engaged(),
            "kill switch state must round-trip through snapshot/restore"
        );

        // And: the restored book actually enforces the gate.
        let result =
            restored.add_limit_order(Id::new_uuid(), 99, 5, Side::Buy, TimeInForce::Gtc, None);
        assert!(
            matches!(result, Err(OrderBookError::KillSwitchActive)),
            "restored book with engaged kill switch must reject new flow; got {result:?}"
        );
    }

    #[test]
    fn legacy_v2_snapshot_without_kill_switch_field_defaults_to_false() {
        // Build a fresh package and drop the `kill_switch_engaged` key
        // from the JSON to simulate a v2 payload written before this
        // field existed. `#[serde(default)]` should fill it back in
        // with `false` on deserialization.
        let book = OrderBook::<()>::new("TEST");
        book.add_limit_order(Id::new_uuid(), 100, 10, Side::Buy, TimeInForce::Gtc, None)
            .expect("seed bid");

        let json = book.snapshot_to_json(10).expect("serialize");
        let mut value: serde_json::Value = serde_json::from_str(&json).expect("parse json");
        let obj = value.as_object_mut().expect("top level object");
        obj.remove("kill_switch_engaged");
        let stripped = serde_json::to_string(&value).expect("re-serialize");
        assert!(
            !stripped.contains("kill_switch_engaged"),
            "preflight: stripped JSON must not carry the new field"
        );

        let mut restored = OrderBook::<()>::new("TEST");
        restored
            .restore_from_snapshot_json(&stripped)
            .expect("restore stripped json");

        assert!(
            !restored.is_kill_switch_engaged(),
            "missing field must default to false"
        );
    }
}