{"record":{"id":"b53950ef6f63ad98","repo":"nautechsystems/nautilus_trader","slug":"position-fragments-do-not-cover-voided-quantity-fo","errorCode":null,"errorMessage":"position fragments do not cover voided quantity for fill {}","messagePattern":"position fragments do not cover voided quantity for fill (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/execution/src/engine/mod.rs","lineNumber":3928,"sourceCode":"            \"no position fragments found for fill {}\",\n            event.trade_id\n        );\n        fragments.sort_by_key(|(_, split_rank, _, _)| *split_rank);\n\n        let mut allocations = IndexMap::<PositionId, (Quantity, Option<Money>)>::new();\n        let mut remaining_qty = event.voided_qty;\n        for (position_id, _, quantity, _) in fragments.iter().rev() {\n            if remaining_qty.is_zero() {\n                break;\n            }\n            let removed = remaining_qty.min(*quantity);\n            allocations\n                .entry(*position_id)\n                .and_modify(|allocation| allocation.0 = allocation.0 + removed)\n                .or_insert((removed, None));\n            remaining_qty = remaining_qty - removed;\n        }\n        anyhow::ensure!(\n            remaining_qty.is_zero(),\n            \"position fragments do not cover voided quantity for fill {}\",\n            event.trade_id\n        );\n\n        if let Some(mut remaining_commission) = event.commission_voided {\n            for (position_id, _, _, commission) in fragments.iter().rev() {\n                if remaining_commission.is_zero() {\n                    break;\n                }\n                let Some(commission) = commission else {\n                    continue;\n                };\n                anyhow::ensure!(\n                    commission.currency == remaining_commission.currency,\n                    \"position commission currency differs for fill {}\",\n                    event.trade_id\n                );","sourceCodeStart":3910,"sourceCodeEnd":3946,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/execution/src/engine/mod.rs#L3910-L3946","documentation":"During fill-void (cancel/adjust) processing in the execution engine, the engine walks cached positions that were touched by the fill and subtracts each position's fragment of the voided quantity. If after consuming all matching fragments the remaining quantity is not zero, the position bookkeeping is inconsistent with the fill being voided, so the engine aborts the void with this error to preserve exact quantity arithmetic.","triggerScenarios":"Calling the fill-void handling (e.g. applying a FillVoided/FillCancellation event) where the sum of quantity removed per matched position_id is less than event.voided_qty — e.g. one of the positions was already voided, adjusted, or the void event quantity disagrees with the originally recorded fills.","commonSituations":"Replaying out-of-order or duplicated fill/void events after a restart; manually editing or restoring cache state from a partial snapshot; adapter bugs (e.g. Betfair fill tracker) emitting voided quantities that do not match fills previously applied to positions.","solutions":["Verify the voided event's trade_id/voided_qty matches the original fill quantities that were applied to the positions.","Check for duplicated or replayed void events and make void application idempotent before dispatching to the engine.","Rebuild the cache/position state from a consistent snapshot so position quantities reflect the fills being voided.","Inspect the position_id->allocation loop in crates/execution/src/engine/mod.rs (~line 3920) and log allocations to find which fragment is missing or double-counted."],"exampleFix":"// before: voiding with a quantity that doesn't match applied fills\nlet event = FillVoided { voided_qty: Quantity::from(10), .. };\nengine.apply_fill_void(event)?; // panics/errors: fragments cover only 6\n// after: reconcile voided_qty against the recorded fills\nlet voided_qty = recorded_fills.iter().map(|f| f.qty).sum();\nlet event = FillVoided { voided_qty, .. };\nengine.apply_fill_void(event)?;","handlingStrategy":"validation","validationCode":"// verify voided qty matches the fills recorded for this trade before applying\nlet applied: Quantity = positions_affected_by_fill(event.trade_id)\n    .iter().map(|p| p.fill_qty_for(event.trade_id)).sum();\nassert_eq!(applied, event.voided_qty, \"voided quantity mismatch for fill\");","typeGuard":null,"tryCatchPattern":"match engine.apply_fill_void(event) {\n    Err(e) if e.to_string().contains(\"do not cover voided quantity\") => {\n        log::error!(\"inconsistent void state for {}: {e}\", event.trade_id);\n        // halt event replay; snapshot state for diagnosis\n    }\n    other => other?,\n}","preventionTips":["Replay fill and void events strictly in original order and never skip fills.","Make void application idempotent (track processed trade_ids).","Restore cache from a snapshot taken at a consistent event boundary."],"tags":["execution-engine","position-accounting","fill-void","invariant-violation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}