nautechsystems/nautilus_trader · error

position {position_id} is not cached

Error message

position {position_id} is not cached

What it means

During fill-void processing the engine takes exclusive ownership of the position (position_owned) from the cache to mutate its fill-void records. If the position identified by position_id is not present in the cache, the engine cannot apply the void and returns this error. The library expects every position referenced by a void event to have been created and cached by prior fill events.

Source

Thrown at crates/execution/src/engine/mod.rs:3984

                "position fragments do not cover voided commission for fill {}",
                event.trade_id
            );
        }

        let mut corrected_positions = Vec::new();

        for (position_id, (voided_qty, commission_voided)) in allocations {
            if voided_qty.is_zero() {
                anyhow::bail!(
                    "commission-only position correction requires authoritative reconciliation for fill {}",
                    event.trade_id
                );
            }
            let mut position = self
                .cache
                .borrow()
                .position_owned(&position_id)
                .ok_or_else(|| anyhow::anyhow!("position {position_id} is not cached"))?;
            let previous = position
                .fill_voids
                .iter()
                .rev()
                .find(|record| {
                    record.event.client_order_id == event.client_order_id
                        && record.event.trade_id == event.trade_id
                })
                .map(|record| (record.voided_qty, record.commission_voided));
            if previous == Some((voided_qty, commission_voided)) {
                continue;
            }
            let corrected_qty = previous.map_or(voided_qty, |(prior_qty, _)| {
                voided_qty.saturating_sub(prior_qty)
            });

            // `events` holds the fills since the position was last flat, because `apply_fill`
            // clears it when reopening from flat. A NETTING flip splits one fill across the

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the cache is loaded from the same snapshot/session that contains the position before applying void events.
  2. Replay fills and voids together in order so the position exists in cache when the void is processed.
  3. Skip or drop void events whose position_id is unknown only if your recovery policy explicitly tolerates them — otherwise rehydrate state.
  4. Log position_id and inspect cache contents (cache.position(&position_id)) before dispatching the void to confirm presence.

Example fix

// before: applying a void with an empty cache
let event = FillVoided { position_id: Some(pid), .. };
engine.apply_fill_void(event)?; // 'position ... is not cached'
// after: rehydrate cache first
assert!(cache.position(&pid).is_some(), "rehydrate cache snapshot before voids");
engine.apply_fill_void(event)?;
Defensive patterns

Strategy: validation

Validate before calling

// confirm the position exists and is retrievable before applying any void
if cache.position(&position_id).is_none() {
    return Err(anyhow::anyhow!(
        "refusing to void fill {}: position {position_id} missing; rehydrate cache first",
        event.trade_id
    ));
}

Type guard

fn position_cached(cache: &Cache, position_id: &PositionId) -> bool {
    cache.position(position_id).is_some()
}

Try / catch

match engine.apply_fill_void(event) {
    Err(e) if e.to_string().contains("is not cached") => {
        log::warn!("skipping void for uncached position: {e}");
        // rehydrate cache from snapshot, then retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: A fill-void event references a position_id that was never cached (fills applied before cache persistence, cache flushed/reset mid-session), or the position was previously removed/archived before the void arrived.

Common situations: Running with a fresh cache while replaying historical events; cache snapshot written before the position was opened; multiple processes sharing a cache where one deleted the position; adapter (e.g. Betfair) emitting voids for positions from a prior session.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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