betex 0.5.3

Betfair / Prediction Market Exchange
Documentation
//! Market state machine validation.
//!
//! Provides helpers for validating market state transitions and checking
//! if a market can accept trading commands.

use super::types::BookMarketState;
use crate::book::protocol::reject::RejectReason;
use crate::types::MarketKind;

/// Validate that a state transition is allowed.
///
/// # Arguments
/// * `from` - Current market state
/// * `to` - Target market state
/// * `market_kind` - Type of market (affects allowed transitions)
///
/// # Errors
/// Returns an error if:
/// - PreEventOnly markets attempt to go to TurnInPlayEnabled
/// - Terminal states attempt invalid transitions
#[allow(dead_code)]
pub fn validate_transition(
    from: BookMarketState,
    to: BookMarketState,
    market_kind: MarketKind,
) -> Result<(), RejectReason> {
    // PreEventOnly markets cannot go to TurnInPlayEnabled
    if market_kind == MarketKind::PreEventOnly && to == BookMarketState::TurnInPlayEnabled {
        return Err(RejectReason::MarketInPlayNotSupported);
    }

    // Terminal states reject most transitions
    if from.is_terminal() && !matches!(to, BookMarketState::Settled | BookMarketState::Voided) {
        return Err(RejectReason::MarketTerminal);
    }

    Ok(())
}

/// Check if market can accept trading commands.
///
/// # Errors
/// Returns `MarketNotOpen` if the market is not in a matchable state.
#[allow(dead_code)]
pub fn can_trade(state: BookMarketState) -> Result<(), RejectReason> {
    if !state.is_matchable() {
        return Err(RejectReason::MarketNotOpen);
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pre_event_only_cannot_go_in_play() {
        let result = validate_transition(
            BookMarketState::Open,
            BookMarketState::TurnInPlayEnabled,
            MarketKind::PreEventOnly,
        );
        assert_eq!(result, Err(RejectReason::MarketInPlayNotSupported));
    }

    #[test]
    fn in_play_capable_can_go_in_play() {
        let result = validate_transition(
            BookMarketState::Open,
            BookMarketState::TurnInPlayEnabled,
            MarketKind::InPlayCapable,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn terminal_state_rejects_open() {
        let result = validate_transition(
            BookMarketState::Settled,
            BookMarketState::Open,
            MarketKind::InPlayCapable,
        );
        assert_eq!(result, Err(RejectReason::MarketTerminal));
    }

    #[test]
    fn can_trade_when_open() {
        assert!(can_trade(BookMarketState::Open).is_ok());
        assert!(can_trade(BookMarketState::TurnInPlayEnabled).is_ok());
    }

    #[test]
    fn cannot_trade_when_closed() {
        assert_eq!(
            can_trade(BookMarketState::Closed),
            Err(RejectReason::MarketNotOpen)
        );
        assert_eq!(
            can_trade(BookMarketState::Suspended),
            Err(RejectReason::MarketNotOpen)
        );
    }
}