nautechsystems/nautilus_trader · error

{e}

Error message

{e}

What it means

`Bet::from_stake_or_liability` builds a Back bet directly from a stake, but for a Lay bet it derives the stake from a liability via `from_liability_checked`. The panicking wrapper surfaces that checked failure when `price` (decimal odds) is not greater than 1.0 ('Price must be greater than 1.0 for lay liability calculation, was {price}') or when `liability / (price - 1)` overflows. The Back branch can never panic.

Source

Thrown at crates/model/src/data/bet.rs:75

    }

    /// Returns the bet's side.
    #[must_use]
    pub fn side(&self) -> BetSide {
        self.side
    }

    /// Creates a bet from a stake or liability depending on the bet side.
    ///
    /// For `BetSide::Back` this calls [`Self::from_stake`] and for
    /// `BetSide::Lay` it calls [`Self::from_liability`].
    ///
    /// # Panics
    ///
    /// Panics if `side` is [`BetSide::Lay`] and [`Self::from_liability`] panics.
    #[must_use]
    pub fn from_stake_or_liability(price: Decimal, volume: Decimal, side: BetSide) -> Self {
        Self::from_stake_or_liability_checked(price, volume, side).unwrap_or_else(|e| panic!("{e}"))
    }

    /// Creates a bet from a stake or liability depending on the bet side.
    ///
    /// # Errors
    ///
    /// Returns an error if `side` is [`BetSide::Lay`] and [`Self::from_liability_checked`] fails.
    pub fn from_stake_or_liability_checked(
        price: Decimal,
        volume: Decimal,
        side: BetSide,
    ) -> anyhow::Result<Self> {
        match side {
            BetSide::Back => Ok(Self::from_stake(price, volume, side)),
            BetSide::Lay => Self::from_liability_checked(price, volume, side),
        }
    }

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Pass decimal odds strictly greater than 1.0 as price on the Lay path, converting probabilities with 1/p first.
  2. Switch to `from_stake_or_liability_checked` and handle the Result instead of the panicking wrapper.
  3. Reject or quarantine market data with price <= 1.0 before constructing bets.
  4. If the message mentions Decimal overflow, check for corrupted or mis-scaled volume fields.

Example fix

// before: probability used directly as odds on the lay path
let bet = Bet::from_stake_or_liability(prob, volume, BetSide::Lay); // panics: price must be > 1.0

// after: convert probability to decimal odds, use the checked API
let odds = Decimal::ONE.checked_div(prob).context('zero probability')?;
let bet = Bet::from_stake_or_liability_checked(odds, volume, BetSide::Lay)?;
Defensive patterns

Strategy: validation

Validate before calling

fn can_build_from_volume(price: Decimal, side: BetSide) -> bool {
    match side {
        BetSide::Back => true,
        BetSide::Lay => price > Decimal::ONE,
    }
}

Try / catch

let bet = Bet::from_stake_or_liability_checked(odds, volume, side)
    .with_context(|| format('cannot build {side} bet at {odds} from volume {volume}'))?;

Prevention

When it happens

Trigger: Calling `from_stake_or_liability(price, volume, BetSide::Lay)` with price <= 1.0 — price exactly 1.0 (the divisor price-1 would be zero) or a probability like 0.35 passed where decimal odds are expected — or with liability magnitudes that overflow Decimal in the division.

Common situations: Betting/prediction-market adapters converting exchange messages: probability (0..1) fed instead of decimal odds, feed prices of exactly 1.0 for near-certain outcomes, placeholder values in unit tests, or mis-scaled volumes (raw token base units).

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/5cf8bbfa96fbb54a. Report an issue: GitHub.