nautechsystems/nautilus_trader · error · anyhow::Error

fill {} maps to non-open position {position_id}

Error message

fill {} maps to non-open position {position_id}

What it means

The fill was mapped to a cached position that is already closed (is_open() is false). Applying a fill to a closed position would resurrect inconsistent state, so the manager fails the report preparation.

Source

Thrown at crates/live/src/execution/manager.rs:2898

        }

        let Some(position_id) = mapped_position_id else {
            return Ok(PositionFillReportPreparation::Unattributed);
        };
        let position = cache.position(&position_id).ok_or_else(|| {
            anyhow::anyhow!(
                "fill {} maps to position {position_id}, which is not cached",
                report.trade_id,
            )
        })?;

        anyhow::ensure!(
            position.account_id == report.account_id
                && position.instrument_id == report.instrument_id,
            "fill {} maps to position {position_id} with a different account or instrument",
            report.trade_id,
        );
        anyhow::ensure!(
            position.is_open(),
            "fill {} maps to non-open position {position_id}",
            report.trade_id,
        );
        anyhow::ensure!(
            !position.is_opposite_side(report.order_side) || report.last_qty <= position.quantity,
            "fill {} without a venue position ID would cross position {position_id}",
            report.trade_id,
        );

        report.venue_position_id = Some(position_id);
        Ok(PositionFillReportPreparation::Ready)
    }

    #[cfg(feature = "node")]
    fn has_active_inferred_fill(order: &OrderAny) -> anyhow::Result<bool> {
        let events = order.events();
        let trade_ids = order.trade_ids();

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Deduplicate trade reports by trade_id before processing so replayed fills are ignored
  2. Configure reconciliation/idempotency so late duplicate fills are dropped rather than applied
  3. Check venue netting behavior and configure the node's account type (netting vs hedging) to match
  4. Review adapter report replay logic to not re-emit already-processed fills

Example fix

// before (no dedup on replay)
for report in reports { self.handle_report(report); }
// after
for report in reports {
    if self.processed_trade_ids.insert(report.trade_id) {
        self.handle_report(report);
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if let Some(pos) = cache.position(&position_id) {
    if !pos.is_open() {
        // drop or dedupe this late fill instead of applying it
    }
}

Try / catch

match result {
    Err(e) if e.to_string().contains("non-open position") => {
        // treat as duplicate/late fill: log and discard
    }
    other => other?,
}

Prevention

When it happens

Trigger: A fill arrives referencing a position_id whose cached Position already emitted a Closed event - e.g. a late/duplicate venue fill after the position was flattened, or a duplicate report delivered after the closing fill was processed.

Common situations: Late fills from the venue arriving after an offsetting order closed the position; duplicate trade reports on reconnect/replay; venues emitting fills for positions the adapter already considered closed due to netting.

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


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