nautechsystems/nautilus_trader · error

stale position fill void for {}

Error message

stale position fill void for {}

What it means

Position::apply_fill_void rejects a void event whose voided_qty is smaller than a previously recorded void for the same client_order_id/trade_id pair. Fill voids must be monotonically non-decreasing (they represent cumulative voided quantity), so an out-of-order or stale void event would rewind state. This protects against replaying old events after newer ones.

Source

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

        commission_voided: Option<Money>,
    ) -> anyhow::Result<Option<Money>> {
        let fragment_qty = self
            .fill_fragments(event.client_order_id, event.trade_id)
            .iter()
            .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,
        });

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Deliver fill-void events in the original venue/event ordering (sort by event timestamp/sequence) before applying.
  2. Check the existing fill_voids records for the same client_order_id/trade_id and skip events with voided_qty less than the latest recorded value.
  3. Rebuild the position from scratch rather than mixing a stale snapshot with newer live void events.

Example fix

// before: replaying old events onto a live position
for ev in stale_history { position.apply_fill_void(ev)?; }
// after: skip stale voids
let last = position.fill_voids.iter().rev().find(|r|
    r.event.client_order_id == ev.client_order_id && r.event.trade_id == ev.trade_id);
if let Some(prev) = last {
    if ev.voided_qty < prev.voided_qty { continue; } // stale, ignore
}
position.apply_fill_void(ev)?;
Defensive patterns

Strategy: validation

Validate before calling

if let Some(prev) = position.fill_voids.iter().rev().find(|r| {
    r.event.client_order_id == ev.client_order_id && r.event.trade_id == ev.trade_id
}) {
    if ev.voided_qty < prev.voided_qty { return Ok(()); } // stale, skip
}

Prevention

When it happens

Trigger: Applying the same void event stream out of order, e.g. replaying historical events where an older cumulative-void record arrives after a larger one for the same trade_id/client_order_id; re-processing a backfill snapshot after live voids were already applied.

Common situations: Backtesting with a replay engine that does not preserve event ordering; loading a stale cached position snapshot and re-applying old void events; duplicated data feeds delivering events with delayed timestamps.

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