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

  1. 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())
  2. Only call update_balances in response to fill/position events where a position definitely exists
  3. Verify the instrument_id and account_id arguments match the filled instrument
  4. 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

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


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/bb6fe81372783127. Report an issue: GitHub.