betex 0.35.0

Betfair / Prediction Market Exchange
Documentation
//! Cross-matching configuration types.

use crate::types::{AccountId, Money};
use rust_decimal::Decimal;

/// Risk tolerance settings for cross-matching.
///
/// Controls how much loss the exchange is willing to accept per cross-match
/// and in aggregate across all unresolved positions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RiskTolerance {
    /// Maximum loss as percentage of user's stake.
    /// - 0.0 = risk-free only
    /// - 0.05 = allow up to 5% loss relative to stake
    pub max_loss_pct: Decimal,

    /// Absolute cap on loss per cross-match (regardless of percentage).
    /// Prevents runaway losses on large bets.
    pub max_loss_absolute: Money,

    /// Maximum aggregate exposure across all unresolved cross-matches.
    /// Circuit breaker - reject new cross-matches if breached.
    pub max_total_exposure: Money,
}

impl Default for RiskTolerance {
    /// Default is completely risk-free (no tolerance).
    fn default() -> Self {
        Self {
            max_loss_pct: Decimal::ZERO,
            max_loss_absolute: Money(0),
            max_total_exposure: Money(0),
        }
    }
}

impl RiskTolerance {
    /// Create a risk-free configuration (P&L >= 0 in all outcomes).
    pub fn risk_free() -> Self {
        Self::default()
    }

    /// Conservative preset: tiny tolerance for rounding only.
    ///
    /// - 0.01% of stake
    /// - Max $1 per match
    /// - Max $100 aggregate
    pub fn conservative() -> Self {
        Self {
            max_loss_pct: Decimal::new(1, 4),              // 0.0001 = 0.01%
            max_loss_absolute: Money::from_cents(100),     // $1
            max_total_exposure: Money::from_cents(10_000), // $100
        }
    }

    /// Moderate preset: small tolerance to improve fill rates.
    ///
    /// - 1% of stake
    /// - Max $100 per match
    /// - Max $1,000 aggregate
    pub fn moderate() -> Self {
        Self {
            max_loss_pct: Decimal::new(1, 2),               // 0.01 = 1%
            max_loss_absolute: Money::from_cents(10_000),   // $100
            max_total_exposure: Money::from_cents(100_000), // $1,000
        }
    }

    /// Calculate effective tolerance for a given stake.
    ///
    /// Returns the minimum of (stake * pct) and absolute cap.
    pub fn effective_tolerance(&self, stake: Money) -> Money {
        let pct_tolerance = self.max_loss_pct * Decimal::from(stake.0);
        let pct_money = Money(pct_tolerance.try_into().unwrap_or(i64::MAX));
        Money(pct_money.0.min(self.max_loss_absolute.0))
    }
}

/// Configuration for the cross-matching engine.
#[derive(Debug, Clone)]
pub struct CrossMatchConfig {
    /// Maximum runners supported for cross-matching.
    /// v1 only supports 3-runner markets (e.g., soccer 1X2).
    pub max_runners: usize,

    /// House account used for placing hedge legs.
    /// This account takes the opposite side of user orders and places hedges.
    pub house_account: AccountId,

    /// Risk tolerance settings.
    pub risk: RiskTolerance,

    /// Whether to attempt cross-match when an order partially fills
    /// against direct liquidity and has remaining size.
    pub cross_match_remainder: bool,
}

impl Default for CrossMatchConfig {
    fn default() -> Self {
        Self {
            max_runners: 3,
            house_account: AccountId::from(uuid::Uuid::nil()), // Reserved for house
            risk: RiskTolerance::default(),
            cross_match_remainder: true,
        }
    }
}