nautechsystems/nautilus_trader · error

duplicate position fill void for {}

Error message

duplicate position fill void for {}

What it means

Position::apply_fill_void rejects a void event identical to the most recent one already applied for the same client_order_id/trade_id (same voided_qty and commission_voided). Applying the identical cumulative void twice would double-count it, so exact duplicates are treated as invalid idempotency violations rather than no-ops.

Source

Thrown at crates/model/src/position.rs:763

            .fold(Quantity::zero(self.size_precision), |total, fill| {
                total + fill.last_qty
            });
        anyhow::ensure!(
            !voided_qty.is_zero() && voided_qty <= fragment_qty,
            "position fill void exceeds known fragments for {}",
            event.trade_id,
        );

        if let Some(previous) = self.fill_voids.iter().rev().find(|record| {
            record.event.client_order_id == event.client_order_id
                && record.event.trade_id == event.trade_id
        }) {
            anyhow::ensure!(
                voided_qty >= previous.voided_qty,
                "stale position fill void for {}",
                event.trade_id,
            );
            anyhow::ensure!(
                voided_qty != previous.voided_qty
                    || commission_voided != previous.commission_voided,
                "duplicate position fill void for {}",
                event.trade_id,
            );
        }

        self.fill_voids.push(PositionFillVoid {
            event,
            voided_qty,
            commission_voided,
        });

        Ok(self.rebuild_from_replay())
    }

    /// Returns durable fill fragments matching an order trade in local application order.
    #[must_use]

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Track applied void events by (client_order_id, trade_id, voided_qty, commission_voided) and skip exact duplicates before calling apply_fill_void.
  2. Deduplicate the event stream upstream (e.g. by event id/sequence) before feeding position mutations.
  3. If a duplicate should be idempotent in your flow, detect it via the position's fill_voids and treat it as a no-op instead of an error path.

Example fix

// before
position.apply_fill_void(ev)?; // errors on replayed duplicate
// after: idempotent skip
let dup = position.fill_voids.iter().rev().any(|r| {
    r.event.client_order_id == ev.client_order_id
        && r.event.trade_id == ev.trade_id
        && r.voided_qty == ev.voided_qty
        && r.commission_voided == ev.commission_voided
});
if !dup { position.apply_fill_void(ev)?; }
Defensive patterns

Strategy: validation

Validate before calling

let is_dup = position.fill_voids.iter().rev().any(|r| {
    r.event.client_order_id == ev.client_order_id
        && r.event.trade_id == ev.trade_id
        && r.voided_qty == ev.voided_qty
        && r.commission_voided == ev.commission_voided
});
if is_dup { return Ok(()); } // idempotent no-op

Prevention

When it happens

Trigger: Re-applying the exact same void event twice — e.g. an event bus delivering a duplicate message, a replay engine feeding the same historical trade-adjustment twice, or retrying apply_fill_void after an ambiguous failure where the first call actually succeeded.

Common situations: Historical data containing duplicate trade-adjustment records; at-least-once event delivery from a message broker; manually re-running a replay script that already mutated the position.

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/b933849aeb027691. Report an issue: GitHub.