betex 0.35.0

Betfair / Prediction Market Exchange
Documentation
use betex::{
    book::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce},
    book::protocol::reject::RejectReason,
    book::{Book, BookEvent},
    types::{AccountId, CorrelationId, MarketId, Money, OddsX10000, RunnerId},
};

fn place_order_cmd(
    market_id: MarketId,
    correlation_id: u64,
    account_id: u64,
    runner_id: u32,
) -> Command {
    Command {
        correlation_id: Some(CorrelationId(correlation_id.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(runner_id),
            account_id: AccountId::from(account_id),
            client_order_id: None,
            side: Side::Yes,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn batch_cancel_cmd(market_id: MarketId, metadata: Option<serde_json::Value>) -> Command {
    Command {
        correlation_id: Some(CorrelationId(9001.to_string())),
        metadata,
        market_id,
        kind: CommandKind::BatchCancelOrders {
            from_created_at_inclusive: None,
            to_created_at_inclusive: None,
            account_id: None,
            runner_id: None,
            reason: "BET_CANCEL".to_string(),
        },
    }
}

fn continue_batch_cancel_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(9002.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ContinueBatchProcess,
    }
}

#[test]
fn batch_cancel_no_orders_still_emits_final_event() {
    let market_id = MarketId(101);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    let (events, _) = book
        .handle(&batch_cancel_cmd(market_id, None))
        .expect("batch cancel should be accepted");
    assert_eq!(events.len(), 2);
    assert!(matches!(
        events[0].event,
        BookEvent::BatchProcessStarted { .. }
    ));
    assert!(matches!(
        events[1].event,
        BookEvent::BatchProcessCompleted { .. }
    ));
}

#[test]
fn batch_cancel_metadata_attaches_only_to_first_event() {
    let market_id = MarketId(102);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    for i in 0..3 {
        let (events, _) = book
            .handle(&place_order_cmd(market_id, i + 1, 10_000 + i, 1))
            .expect("place should succeed");
        book.apply_all_events(&events);
    }

    let metadata = serde_json::json!({"source":"bet-cancel","trace":"t-1"});
    book.set_close_batch_max_events(2);
    let (start_events, _) = book
        .handle(&batch_cancel_cmd(market_id, Some(metadata.clone())))
        .expect("batch start should succeed");
    assert_eq!(start_events.len(), 2);
    assert!(matches!(
        start_events[0].event,
        BookEvent::BatchProcessStarted { .. }
    ));
    match &start_events[1].event {
        BookEvent::OrderCancelledBatched { .. } => {}
        _ => panic!("expected OrderCancelledBatched"),
    }
    assert_eq!(start_events[0].metadata, Some(metadata));
    assert!(start_events[1].metadata.is_none());
    book.apply_all_events(&start_events);

    loop {
        let (events, _) = book
            .handle(&continue_batch_cancel_cmd(market_id))
            .expect("continue should succeed");
        assert!(events.len() == 1 || events.len() == 2);
        assert!(events.iter().all(|event| event.metadata.is_none()));
        match (
            events.first().map(|e| &e.event),
            events.get(1).map(|e| &e.event),
        ) {
            (
                Some(BookEvent::OrderCancelledBatched { .. }),
                Some(BookEvent::BatchProcessCompleted { .. }),
            ) => break,
            (Some(BookEvent::OrderCancelledBatched { .. }), _) => {}
            _ => panic!("expected OrderCancelledBatched"),
        }
        book.apply_all_events(&events);
    }
}

#[test]
fn batch_cancel_blocks_manual_cancel_while_processing() {
    let market_id = MarketId(103);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    for correlation_id in [1_u64, 2] {
        let (events, _) = book
            .handle(&place_order_cmd(
                market_id,
                correlation_id,
                99 + correlation_id,
                1,
            ))
            .expect("place should succeed");
        book.apply_all_events(&events);
    }

    let (events, _) = book
        .handle(&place_order_cmd(market_id, 3, 102, 1))
        .expect("place should succeed");
    let oid = match events[0].event {
        BookEvent::OrderAccepted { order_id, .. } => order_id,
        _ => panic!("expected OrderAccepted"),
    };
    book.apply_all_events(&events);

    book.set_close_batch_max_events(2);
    let (start_events, _) = book
        .handle(&batch_cancel_cmd(market_id, None))
        .expect("batch start should succeed");
    book.apply_all_events(&start_events);

    let err = book
        .handle(&Command {
            correlation_id: Some(CorrelationId(77.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CancelOrder {
                account_id: AccountId::from(102),
                order_id: oid,
            },
        })
        .expect_err("manual cancel should be blocked while batch-cancel is in progress");
    assert_eq!(err.reason, RejectReason::MarketBatchCancelling);
}

#[test]
fn batch_cancel_is_allowed_when_market_halted() {
    let market_id = MarketId(104);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    let (events, _) = book
        .handle(&Command {
            correlation_id: Some(CorrelationId(55.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::HaltMarket {
                reason: "TEST_HALT".to_string(),
            },
        })
        .expect("halt should succeed");
    book.apply_all_events(&events);

    let (events, _) = book
        .handle(&batch_cancel_cmd(market_id, None))
        .expect("batch-cancel should be accepted while halted");
    assert_eq!(events.len(), 2);
    assert!(matches!(
        events[0].event,
        BookEvent::BatchProcessStarted { .. }
    ));
    assert!(matches!(
        events[1].event,
        BookEvent::BatchProcessCompleted { .. }
    ));
}