nautechsystems/nautilus_trader · warning

Duplicate fill

Error message

Duplicate fill

What it means

The engine validates fills before applying them: if the order reports the fill's trade_id as a duplicate (order.is_duplicate_fill), the fill is skipped and this error is raised instead of re-applying state. Duplicate fills would otherwise double-count fills/positions.

Source

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

        if self.config.debug {
            log::debug!("Generated {} for {}", position_id, fill.client_order_id());
        }
        position_id
    }

    fn determine_netting_position_id(&self, fill: &OrderFilled) -> PositionId {
        PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id))
    }

    fn validate_fill_for_order(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
        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(

View on GitHub (pinned to 18893faf8b)

Solutions

  1. This is a guard: treat it as 'already processed' and skip — deduplicate upstream by tracking processed trade_ids.
  2. If your code propagates the error, catch it and continue when the fill is a benign duplicate.
  3. Fix duplicate event delivery: check for double subscription or re-delivery after WebSocket reconnect.
  4. If reconciliation is the source, let it skip already-applied fills instead of re-submitting them to the fill path.

Example fix

// before
engine.apply_fill(&order, &fill)?; // bails on replayed trade_id
// after
match engine.apply_fill(&order, &fill) {
    Err(e) if e.to_string().contains("Duplicate fill") => log::debug!("skipped duplicate {}", fill.trade_id),
    other => other?,
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check before applying
if order.is_duplicate_fill(&fill) { return Ok(()); }

Try / catch

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

Prevention

When it happens

Trigger: Calling the engine's fill application path (e.g. update_order_filled / process_fill) with an OrderFilled event whose trade_id was already applied to the order — typically a replayed or duplicated venue event.

Common situations: Reconciliation re-delivering a fill that was already applied; venue/WebSocket message replay after reconnect; processing the same fill from both the execution report stream and reconciliation reports.

Related errors


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