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
- Deliver fill-void events in the original venue/event ordering (sort by event timestamp/sequence) before applying.
- 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.
- 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
- Preserve event ordering when replaying or backfilling void events.
- Never mix a stale position snapshot with newer live void events.
- Sort void events by timestamp/sequence before application.
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
- 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 {}
- duplicate position fill void for {}
- inverse position {} has no base currency
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/b0fd00fd609e48e7.
Report an issue: GitHub.