nautechsystems/nautilus_trader · warning
Duplicate position fill
Error message
Duplicate position fill
What it means
Beyond the order-level duplicate check, the engine verifies that a fill's trade_id has not already been applied to the target position (position_contains_trade_id). If it has, the fill is skipped and this error is raised to prevent double-counting position quantities.
Source
Thrown at crates/execution/src/engine/mod.rs:3498
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(
&self,
order: &OrderAny,
fill: &OrderFilled,
) -> anyhow::Result<()> {
if order.is_duplicate_fill(fill) {
anyhow::bail!("Duplicate fill");
}
self.check_overfill(order, fill)
}
fn position_contains_trade_id(&self, position_id: PositionId, trade_id: TradeId) -> bool {View on GitHub (pinned to 18893faf8b)
Solutions
- Treat as an idempotency guard: skip the fill if the position already contains the trade_id.
- Deduplicate reconciliation data before applying: reconcile position reports against the engine's already-known trade IDs.
- Check for double-delivery of fills from both the live stream and reconciliation, and prefer a single source of truth per position.
- If the trade_id is genuinely different but colliding, verify the venue's trade ID semantics/normalization in the adapter.
Example fix
// before
engine.apply_fill(&order, &recon_fill)?; // trade already on position
// after
if !engine.position_contains_trade_id(position_id, recon_fill.trade_id) {
engine.apply_fill(&order, &recon_fill)?;
} Defensive patterns
Strategy: try-catch
Validate before calling
// check before applying
if engine.position_contains_trade_id(position_id, fill.trade_id) { return Ok(()); } Try / catch
match engine.apply_fill(order, &fill) {
Err(e) if e.to_string().contains("Duplicate position fill") => log::debug!("trade {} already on position", fill.trade_id),
other => other?,
} Prevention
- Use a single source of truth per position when combining reconciliation and live fills.
- Filter reconciliation trades already present in the position before applying.
- Verify venue trade_id normalization in the adapter to avoid false collisions.
When it happens
Trigger: Applying a fill whose trade_id is already recorded in the position's trade IDs even though the order-level duplicate check passed — e.g. the same trade_id applied to the same position from a second event path (reconciliation vs live stream).
Common situations: Reconciliation importing position reports that contain trades already applied via the live fill stream; hedge-position setups where one trade is reported against the same position twice; venue re-sending an execution report after reconnect.
Related errors
- Duplicate fill
- duplicate position fill void for {}
- Failed to reserve execution intent for signer {} on chain {}
- Failed to commit replacement transaction: {e}
- Order for {} not found to determine position ID
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/9e995b7a860fe1a6.
Report an issue: GitHub.