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
- Track applied void events by (client_order_id, trade_id, voided_qty, commission_voided) and skip exact duplicates before calling apply_fill_void.
- Deduplicate the event stream upstream (e.g. by event id/sequence) before feeding position mutations.
- 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
- Deduplicate event streams by id/sequence before applying position mutations.
- Make void application idempotent at the call site by checking fill_voids first.
- Avoid re-running replay scripts against already-mutated positions.
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
- Duplicate position fill
- Cannot calculate inverse points: open price is not positive
- Cannot calculate inverse points: close price is not positive
- position fill void exceeds known fragments for {}
- stale position fill void for {}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/b933849aeb027691.
Report an issue: GitHub.