nautechsystems/nautilus_trader · error
List of Positions is empty
Error message
List of Positions is empty
What it means
PortfolioManager::update_balances looks up the open positions for an account (filtered by instrument_id/instrument_ids arguments) and takes the first position to derive the position id for balance recalculation. If the cache reports positions that then produce an empty list, it panics with 'List of Positions is empty'. The error indicates an internal inconsistency: the cache said there were positions, but none were retrievable.
Source
Thrown at crates/portfolio/src/manager.rs:95
// Snapshot only what the balance update can mutate: cloning the account would
// deep-copy its event log, which grows by one entry per fill.
let base = base_account(&account);
let original_balances = base.balances.clone();
let original_commissions = base.commissions.clone();
let position_id = if let Some(position_id) = fill.position_id {
position_id
} else {
let cache = self.cache.borrow();
let positions_open = cache.positions_open(
None,
Some(&fill.instrument_id),
None,
Some(&fill.account_id),
None,
);
positions_open
.first()
.unwrap_or_else(|| panic!("List of Positions is empty"))
.id
};
let position = self
.cache
.borrow()
.position(&position_id)
.map(|position| position.clone_without_events());
let pnls = match account.calculate_pnls(instrument, fill, position) {
Ok(pnls) => pnls,
Err(e) => {
log::error!(
"Cannot update balances for fill {}: failed to calculate PnL: {e}",
fill.trade_id
);
let state = self.generate_account_state(&account, fill.ts_event);
return (account, state);View on GitHub (pinned to 18893faf8b)
Solutions
- Check that at least one open position exists for the account/instrument before calling update_balances (e.g. cache.positions_open(&venue, Some(&instrument_id)).is_some())
- Only call update_balances in response to fill/position events where a position definitely exists
- Verify the instrument_id and account_id arguments match the filled instrument
- If the mismatch persists, inspect cache position indexing for a reconciliation bug
Example fix
// before
portfolio.update_balances(None, Some(&account_id), None, Some(&instrument_id));
// after
if !cache.borrow().positions_open(Some(&venue), Some(&instrument_id)).is_empty() {
portfolio.update_balances(None, Some(&account_id), None, Some(&instrument_id));
} Defensive patterns
Strategy: type-guard
Validate before calling
let has_position = !cache.borrow()
.positions_open(Some(&venue), instrument_id.as_ref())
.is_empty(); Type guard
fn has_open_position(cache: &Cache, venue: &Venue, instrument_id: Option<&InstrumentId>) -> bool {
!cache.positions_open(Some(venue), instrument_id).is_empty()
} Prevention
- Only call update_balances after a fill/position event confirmed a position exists
- Check positions_open for the specific instrument before the call
- Ensure account_id and instrument_id filters actually match existing positions
- Investigate cache/position index desynchronization after reconciliation
When it happens
Trigger: Calling portfolio.update_balances(...) for an account that actually has no open positions (or for an instrument_id filter that matches none) so positions_open.first() is None.
Common situations: Calling update_balances at startup before any fills created positions; passing an instrument_id that has no position while the account does elsewhere; a cache/position index out of sync after reconciliation or order-event race.
Understand the failure class
Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.
Related errors
- Failed to convert Decimal to f64
- invalid notional value
- Strategy not registered: Portfolio not initialized
- in-flight mutex poisoned
- wallet balance mutex poisoned
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/bb6fe81372783127.
Report an issue: GitHub.