nautechsystems/nautilus_trader · error
cannot decrease an empty bet position
Error message
cannot decrease an empty bet position
What it means
`decreased_state` computes the resulting state when a bet position is reduced, but it can only do so when the current position has a side (long/short). If the position is empty (no side, exposure effectively flat), there is nothing to decrease, so the library raises this error instead of returning a meaningless state.
Source
Thrown at crates/model/src/data/bet.rs:468
let total_stake = checked_add(checked_div(abs_self_exposure, self.price)?, bet.stake)?;
checked_div(
checked_add(abs_self_exposure, abs_bet_exposure)?,
total_stake,
)?
} else {
self.price
};
Ok((
price,
checked_add(self.exposure, bet_exposure)?,
self.realized_pnl,
))
}
fn decreased_state(&self, bet: &Bet) -> anyhow::Result<(Decimal, Decimal, Decimal)> {
let current_side = self
.side()
.ok_or_else(|| anyhow::anyhow!("cannot decrease an empty bet position"))?;
let bet_exposure = bet.exposure_checked()?;
let abs_bet_exposure = bet_exposure.abs();
let abs_self_exposure = self.exposure.abs();
match abs_bet_exposure.cmp(&abs_self_exposure) {
std::cmp::Ordering::Less => {
check_nonzero_denominator(self.price, "price")?;
let decreasing_volume = checked_div(abs_bet_exposure, self.price)?;
let decreasing_bet = Bet::new(self.price, decreasing_volume, current_side);
let pnl = calc_bets_pnl_checked(&[bet.clone(), decreasing_bet])?;
Ok((
self.price,
checked_add(self.exposure, bet_exposure)?,
checked_add(self.realized_pnl, pnl)?,
))
}
std::cmp::Ordering::Greater => Ok((
bet.price,View on GitHub (pinned to 18893faf8b)
Solutions
- Before applying a decrease, check `position.side().is_some()` and skip or no-op when the position is empty.
- Reconcile your event stream: ensure the decrease event actually corresponds to an open position (correct bet_id/order).
- If events may arrive out of order, buffer decreases until the matching increase is applied.
- Wrap the call in error handling and treat 'cannot decrease an empty bet position' as an idempotency signal rather than a crash.
Example fix
// before
position.add_bet_checked(&bet)?;
// after
if position.side().is_some() {
position.add_bet_checked(&bet)?;
} else {
// position already flat; ignore stale decrease
} Defensive patterns
Strategy: try-catch
Validate before calling
if position.side().is_none() {
// skip decrease on flat position
return Ok(());
} Type guard
fn is_open(position: &BetPosition) -> bool { position.side().is_some() } Try / catch
match position.add_bet_checked(&bet) {
Ok(()) => {},
Err(e) if e.to_string().contains("empty bet position") => { /* idempotent no-op */ },
Err(e) => return Err(e),
} Prevention
- Check position state before applying decrease/offset events.
- Make event processing idempotent so duplicate closes are harmless.
- Track bet lifecycle explicitly so stale cancellations are filtered.
When it happens
Trigger: Calling `add_bet_checked` (or otherwise driving `decreased_state`) with a decrease/offsetting bet while the current `BetPosition` has `side() == None` — i.e. the position is empty or already flat.
Common situations: Receiving a bet cancellation or hedge fill that arrives after the position was already closed; replaying events out of order so a decrease is applied to a flat position; double-processing a close in a betting-market integration.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- {e}
- default client already registered
- Already running
- Cannot set cache database while node is running, set it befo
- Cannot add actor while node is running, add actors before ru
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/833d9525ec97be76.
Report an issue: GitHub.