{"record":{"id":"2a8c5eed7113e035","repo":"nautechsystems/nautilus_trader","slug":"fill-group-quantity-is-not-positive","errorCode":null,"errorMessage":"fill group quantity is not positive","messagePattern":"fill group quantity is not positive","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/live/src/execution/manager.rs","lineNumber":1328,"sourceCode":"                let quantity = quantity.checked_add(fill_quantity).ok_or_else(|| {\n                    anyhow::anyhow!(\"fill quantity overflow while aggregating fill group\")\n                })?;\n\n                let fill_notional = fill_quantity\n                    .checked_mul(fill.last_px.as_decimal())\n                    .ok_or_else(|| {\n                        anyhow::anyhow!(\"fill notional overflow while aggregating fill group\")\n                    })?;\n\n                let notional = notional.checked_add(fill_notional).ok_or_else(|| {\n                    anyhow::anyhow!(\"fill notional overflow while aggregating fill group\")\n                })?;\n\n                Ok::<_, anyhow::Error>((quantity, notional))\n            },\n        )?;\n\n        anyhow::ensure!(\n            quantity > Decimal::ZERO,\n            \"fill group quantity is not positive\"\n        );\n\n        let order_qty = Quantity::from_decimal_dp(quantity, instrument.size_precision())?;\n        let avg_px = notional\n            .checked_div(quantity)\n            .ok_or_else(|| anyhow::anyhow!(\"fill group average price is not representable\"))?;\n\n        let ts_accepted = fills\n            .iter()\n            .map(|fill| fill.ts_event)\n            .min()\n            .expect(\"non-empty fill group\");\n\n        let ts_last = fills\n            .iter()\n            .map(|fill| fill.ts_event)","sourceCodeStart":1310,"sourceCodeEnd":1346,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/live/src/execution/manager.rs#L1310-L1346","documentation":"After aggregating a fill group, the manager asserts via `anyhow::ensure!` that the summed fill quantity is strictly positive (`quantity > Decimal::ZERO`). This guards the invariant that a fill group representing an executed order must have positive total size; it is thrown when the sum is zero or negative. Zero-size or negative-quantity fills indicate malformed fill data, cancellation-as-fill handling bugs, or wrong-side quantity signs from the venue adapter.","triggerScenarios":"Processing a fill group where every fill has `last_qty == 0`, or where venue-reported quantities carry negative signs (some venues report position reductions as negative quantities) so the aggregate lands at or below zero; also possible when the fills list contains cancel/reject/expire records misrouted into the fill aggregation path.","commonSituations":"Exchange adapter not converting venue-side signed quantities to positive `last_qty` with a separate order-side; adapter emitting zero-quantity fill events for expired/canceled orders; replaying corrupted or truncated fill logs; reconciliation logic including reject/cancel events as fills.","solutions":["Inspect the fills in the group and their `last_qty` signs and values","Fix the adapter to report fill quantities as positive values regardless of reduce/long/short direction","Filter out zero-quantity or non-fill (cancel/expire/reject) events before the fill-group aggregation path","Verify the instrument's quantity handling matches venue semantics (e.g. contracts vs base units)","Enable debug logging of raw venue fill messages and report the adapter bug if quantities look valid"],"exampleFix":"// before: venue reports reduction fills with negative qty\nlet qty = raw_qty;\n// after: normalize to positive; side is carried by the order\nlet qty = raw_qty.abs();","handlingStrategy":"validation","validationCode":"fn fill_group_has_positive_quantity(fills: &[Fill]) -> bool {\n    !fills.is_empty()\n        && fills.iter().fold(Decimal::ZERO, |acc, f| acc + f.last_qty.as_decimal())\n            > Decimal::ZERO\n}\n","typeGuard":"fn is_positive_fill(f: &Fill) -> bool {\n    f.last_qty.as_decimal() > Decimal::ZERO\n}\n","tryCatchPattern":"match manager.aggregate_fill_group(&fills) {\n    Ok(r) => handle(r),\n    Err(e) if e.to_string().contains(\"quantity is not positive\") => {\n        log::error!(\"zero/negative aggregate fill quantity: {e}\");\n        discard_or_reconcile_group(&fills);\n    }\n    Err(e) => return Err(e),\n}\n","preventionTips":["Filter zero-quantity and non-fill events (cancels, expires, rejects) before aggregation","Normalize venue signed quantities to positive last_qty with side carried separately","Test the adapter against reduce/close order fills to confirm positive quantity reporting","Assert every fill in the group has positive last_qty in pre-aggregation validation"],"tags":["validation","execution","fills","invariant"],"backgroundTag":"invalid-argument-value","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}