nautechsystems/nautilus_trader · warning

Duplicate position fill

Error message

Duplicate position fill

What it means

Beyond the order-level duplicate check, the engine verifies that a fill's trade_id has not already been applied to the target position (position_contains_trade_id). If it has, the fill is skipped and this error is raised to prevent double-counting position quantities.

Source

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

        if order.is_duplicate_fill(fill) {
            log::warn!(
                "Duplicate fill: {} trade_id={} already applied, skipping",
                order.client_order_id(),
                fill.trade_id
            );
            anyhow::bail!("Duplicate fill");
        }

        if let Some(position_id) = fill.position_id
            && self.position_contains_trade_id(position_id, fill.trade_id)
        {
            log::warn!(
                "Duplicate fill: {} trade_id={} already applied to position {}, skipping",
                order.client_order_id(),
                fill.trade_id,
                position_id
            );
            anyhow::bail!("Duplicate position fill");
        }

        self.check_overfill(order, fill)
    }

    fn validate_fill_for_order_projection(
        &self,
        order: &OrderAny,
        fill: &OrderFilled,
    ) -> anyhow::Result<()> {
        if order.is_duplicate_fill(fill) {
            anyhow::bail!("Duplicate fill");
        }

        self.check_overfill(order, fill)
    }

    fn position_contains_trade_id(&self, position_id: PositionId, trade_id: TradeId) -> bool {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Treat as an idempotency guard: skip the fill if the position already contains the trade_id.
  2. Deduplicate reconciliation data before applying: reconcile position reports against the engine's already-known trade IDs.
  3. Check for double-delivery of fills from both the live stream and reconciliation, and prefer a single source of truth per position.
  4. If the trade_id is genuinely different but colliding, verify the venue's trade ID semantics/normalization in the adapter.

Example fix

// before
engine.apply_fill(&order, &recon_fill)?; // trade already on position
// after
if !engine.position_contains_trade_id(position_id, recon_fill.trade_id) {
    engine.apply_fill(&order, &recon_fill)?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check before applying
if engine.position_contains_trade_id(position_id, fill.trade_id) { return Ok(()); }

Try / catch

match engine.apply_fill(order, &fill) {
    Err(e) if e.to_string().contains("Duplicate position fill") => log::debug!("trade {} already on position", fill.trade_id),
    other => other?,
}

Prevention

When it happens

Trigger: Applying a fill whose trade_id is already recorded in the position's trade IDs even though the order-level duplicate check passed — e.g. the same trade_id applied to the same position from a second event path (reconciliation vs live stream).

Common situations: Reconciliation importing position reports that contain trades already applied via the live fill stream; hedge-position setups where one trade is reported against the same position twice; venue re-sending an execution report after reconnect.

Related errors


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