betex 0.35.0

Betfair / Prediction Market Exchange
Documentation
//! Shared deterministic formulas used across book/engine paths.
//!
//! Rounding policy:
//! - back odds -> implied lay odds: `ceil_tick`
//! - lay odds -> implied back odds: `floor_tick`
//! - maker stake -> taker capacity: imply first, then round down to cents
//! - taker stake -> maker stake: round up to cents

use crate::{
    book::protocol::command::Side,
    types::{Money, OddsX10000},
};

#[inline]
fn clamp_i128_to_i64(v: i128) -> i64 {
    v.clamp(i64::MIN as i128, i64::MAX as i128) as i64
}

#[inline]
fn floor_to_step(v: i128, step: i128) -> i128 {
    if v <= 0 || step <= 0 {
        0
    } else {
        (v / step) * step
    }
}

#[inline]
fn ceil_div(v: i128, denom: i128) -> i128 {
    if v <= 0 || denom <= 0 {
        0
    } else {
        (v + denom - 1) / denom
    }
}

#[inline]
fn cent_step() -> i128 {
    Money::MONEY_SCALE_PER_CENT as i128
}

#[inline]
pub fn floor_to_cent(stake: Money) -> Money {
    Money(clamp_i128_to_i64(floor_to_step(
        stake.0 as i128,
        cent_step(),
    )))
}

#[inline]
pub fn ceil_to_cent(stake: Money) -> Money {
    if stake.0 <= 0 {
        Money::zero()
    } else {
        Money(clamp_i128_to_i64(
            ceil_div(stake.0 as i128, cent_step()) * cent_step(),
        ))
    }
}

#[inline]
fn derived_odds_raw(odds: OddsX10000) -> OddsX10000 {
    let o = odds.0 as u64;
    if o <= 10_000 {
        return OddsX10000(OddsX10000::MAX);
    }
    let derived = (o * 10_000) / (o - 10_000);
    OddsX10000(derived.min(u32::MAX as u64) as u32)
}

/// Profit (excluding returned stake) for BACK/YES at decimal odds.
///
/// Formula: `stake * (odds - 1)`, with fixed-point odds (`x10000`).
#[inline]
pub fn backer_profit(stake: Money, odds: OddsX10000) -> Money {
    let stake_i = stake.0 as i128;
    let odds_i = odds.0 as i128;
    let profit = (stake_i * (odds_i - 10_000)) / 10_000;
    Money(clamp_i128_to_i64(profit))
}

/// Required resting reserve/liability for an exchange-odds order.
#[inline]
pub fn exchange_order_reserve(side: Side, odds: OddsX10000, stake: Money) -> Money {
    let stake = stake.clamp_non_negative();
    match side {
        Side::Yes => stake,
        Side::No => backer_profit(stake, odds).clamp_non_negative(),
    }
}

/// Required resting reserve for a binary YES order.
#[inline]
pub fn binary_order_reserve(
    max_price_ticks: u16,
    side: Side,
    price_ticks: u16,
    qty_shares: u64,
) -> u128 {
    let per_share = match side {
        Side::Yes => u128::from(price_ticks),
        Side::No => u128::from(max_price_ticks.saturating_sub(price_ticks)),
    };
    per_share.saturating_mul(u128::from(qty_shares))
}

/// Convert BACK odds on one runner into implied LAY odds on the opposite runner.
///
/// Formula: `o' = o / (o - 1)`, then ceil to the nearest valid ladder tick.
#[inline]
pub fn back_to_lay_odds(back_odds: OddsX10000) -> OddsX10000 {
    let raw = derived_odds_raw(back_odds);
    raw.ceil_tick().unwrap_or(OddsX10000(OddsX10000::MAX))
}

/// Convert LAY odds on one runner into implied BACK odds on the opposite runner.
///
/// Formula: `o' = o / (o - 1)`, then floor to the nearest valid ladder tick.
#[inline]
pub fn lay_to_back_odds(lay_odds: OddsX10000) -> OddsX10000 {
    let raw = derived_odds_raw(lay_odds);
    raw.floor_tick().unwrap_or(OddsX10000(OddsX10000::MAX))
}

/// Largest opposite-runner BACK odds that can still imply at least `target_back_odds`.
///
/// This is used when a taker BACK order checks whether opposite-runner BACK orders
/// remain matchable after `back -> lay` uses `ceil_tick`.
#[inline]
pub fn max_opposite_back_odds_for_taker_back(target_back_odds: OddsX10000) -> OddsX10000 {
    if target_back_odds.0 == OddsX10000::MIN {
        return OddsX10000(OddsX10000::MAX);
    }
    let inclusive_floor = target_back_odds.tick_down().unwrap_or(target_back_odds);
    lay_to_back_odds(inclusive_floor)
}

/// Smallest opposite-runner LAY odds that can still imply at most `target_lay_odds`.
///
/// This is used when a taker LAY order checks whether opposite-runner LAY orders
/// remain matchable after `lay -> back` uses `floor_tick`.
#[inline]
pub fn min_opposite_lay_odds_for_taker_lay(target_lay_odds: OddsX10000) -> OddsX10000 {
    let inclusive_ceil = target_lay_odds.tick_up().unwrap_or(target_lay_odds);
    back_to_lay_odds(inclusive_ceil)
}

/// Convert implied taker stake into consumed maker stake at `trade_price`.
///
/// Formula: `s_m = ceil_to_cent(s_t * (a - 1))`.
#[inline]
pub fn implied_maker_stake_from_taker(taker_stake: Money, trade_price: OddsX10000) -> Money {
    let denom = trade_price.0 as i128 - 10_000;
    if taker_stake.0 <= 0 || denom <= 0 {
        return Money::zero();
    }
    let numerator = taker_stake.0 as i128 * denom;
    ceil_to_cent(Money(clamp_i128_to_i64(numerator / 10_000)))
}

/// Convert maker remaining stake into max implied taker capacity at `trade_price`.
///
/// Formula: `s_t_max = floor_to_cent(s_m * 10000 / (a - 1))`.
#[inline]
pub fn implied_taker_capacity_from_maker(maker_stake: Money, trade_price: OddsX10000) -> Money {
    let denom = trade_price.0 as i128 - 10_000;
    if maker_stake.0 <= 0 || denom <= 0 {
        return Money::zero();
    }
    let numerator = maker_stake.0 as i128 * 10_000;
    floor_to_cent(Money(clamp_i128_to_i64(numerator / denom)))
}