nautechsystems/nautilus_trader · error · anyhow::Error

fill {} without a venue position ID would cross position {po

Error message

fill {} without a venue position ID would cross position {position_id}

What it means

When a fill has no venue_position_id, the manager infers its position from the order. This check rejects an inferred fill whose side is opposite the position's side with quantity exceeding the position's open quantity - meaning the fill would flip/cross the position rather than reduce it, which the no-position-ID inference path does not allow.

Source

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

        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();
        let Some((first, remaining)) = events.split_first() else {
            return Ok(false);
        };
        let mut projected = OrderAny::from_events(vec![(*first).clone()]).map_err(|e| {
            anyhow::anyhow!(

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Have the adapter populate venue_position_id on fills so attribution is explicit instead of inferred
  2. Ensure cached position quantity is accurate via reconciliation before sending oversized closing orders
  3. Size closing orders to the actual open position quantity, or split orders so no fill crosses flat
  4. Configure hedging mode if the strategy intentionally holds opposite-side exposure

Example fix

// before (oversized close, no position ID)
let qty = Quantity::from(2 * position.quantity);
// after
let qty = position.quantity; // close exactly, avoid crossing
Defensive patterns

Strategy: validation

Validate before calling

if report.venue_position_id.is_none() {
    if let Some(pos) = cache.position(&position_id) {
        let would_cross = pos.is_opposite_side(report.order_side) && report.last_qty > pos.quantity;
        assert!(!would_cross);
    }
}

Prevention

When it happens

Trigger: report.venue_position_id is None, the order maps to position_id, the fill side is opposite to position.side, and report.last_qty > position.quantity. Happens when a closing order over-fills past flat (venue supports position flip) but the adapter provides no position ID, or when the cached position quantity is stale/undersized.

Common situations: Trading venues that allow flipping a position in one fill while the adapter doesn't report position IDs; stale cached position quantity after missed fills; submitting a larger closing order than the open position without letting the adapter attribute the resulting fill.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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