{"record":{"id":"8a990c18e541dbd4","repo":"nautechsystems/nautilus_trader","slug":"negative-quantity-value","errorCode":null,"errorMessage":"negative quantity `{value}`","messagePattern":"negative quantity `(.+?)`","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/lighter/src/common/parse.rs","lineNumber":119,"sourceCode":"\n/// Converts a decimal string into a non-negative Nautilus [`Quantity`].\n///\n/// Zero is allowed because Lighter sends zero-size book levels to delete\n/// existing orders.\n///\n/// # Errors\n///\n/// Returns an error if the string is not a decimal, if `precision` exceeds\n/// [`MAX_DECIMALS`], if the value is negative, or if the resulting quantity\n/// is out of range.\npub fn parse_quantity(value: &str, precision: u8) -> anyhow::Result<Quantity> {\n    anyhow::ensure!(\n        precision <= MAX_DECIMALS,\n        \"size precision {precision} exceeds maximum {MAX_DECIMALS}\",\n    );\n    let decimal =\n        Decimal::from_str(value).map_err(|e| anyhow::anyhow!(\"invalid quantity `{value}`: {e}\"))?;\n    anyhow::ensure!(decimal.is_sign_positive(), \"negative quantity `{value}`\");\n    Quantity::from_decimal_dp(decimal, precision)\n        .map_err(|e| anyhow::anyhow!(\"invalid quantity `{value}` at precision {precision}: {e}\"))\n}\n\n/// Converts a [`Decimal`] into a Nautilus [`Price`] at the requested precision.\n///\n/// Use this when the wire value has already been deserialized as a [`Decimal`]\n/// (the standard pattern for model fields tagged with `deserialize_decimal`).\n///\n/// # Errors\n///\n/// Returns an error if `precision` exceeds [`MAX_DECIMALS`] or if the value\n/// is out of [`Price`] range.\npub fn price_from_decimal(value: Decimal, precision: u8) -> anyhow::Result<Price> {\n    anyhow::ensure!(\n        precision <= MAX_DECIMALS,\n        \"price precision {precision} exceeds maximum {MAX_DECIMALS}\",\n    );","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/lighter/src/common/parse.rs#L101-L137","documentation":"Nautilus's Quantity type is strictly non-negative, so parse_quantity rejects any decimal string whose value is negative (checked via Decimal::is_sign_positive). Signed amounts must have their sign extracted by the caller (e.g. as a side/direction) before parsing the magnitude. Note that zero is intentionally allowed because Lighter sends zero-size book levels to delete existing orders.","triggerScenarios":"Calling parse_quantity with a string like \"-0.5\" — e.g. a signed base_amount from a fill/delta/position payload passed directly, or a sign forgotten when reformatting a value.","commonSituations":"Order-delta processing where decreases arrive as negative sizes; position-change events with signed amounts; sign lost/added during manual string formatting; mixing bid-side and ask-side signed conventions.","solutions":["Take the magnitude before parsing: parse_quantity(&value.trim_start_matches('-'), precision) or better, parse to Decimal first, take .abs(), and use the sign for side/direction decisions.","Route signed deltas through parse_quantity_from_ticks after .abs() and handle the sign explicitly (sell/buy, long/short).","Verify the payload field is genuinely meant to be unsigned; if the source can be negative, this is the wrong parser.","Treat unexpected negatives as malformed data: log the raw value and drop the message rather than coercing."],"exampleFix":"// before\nlet qty = parse_quantity(delta_str, precision)?;\n// after\nlet magnitude = delta_str.trim_start_matches('-');\nlet qty = parse_quantity(magnitude, precision)?;\nlet side = if delta_str.starts_with('-') { OrderSide::Sell } else { OrderSide::Buy };","handlingStrategy":"validation","validationCode":"fn is_non_negative_decimal_str(s: &str) -> bool {\n    s.trim().parse::<rust_decimal::Decimal>()\n        .map(|d| d.is_sign_positive())\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"match parse_quantity(raw, precision) {\n    Ok(q) => q,\n    Err(e) if e.to_string().starts_with(\"negative quantity\") => {\n        tracing::warn!(raw = %raw, \"signed size; extract sign and parse magnitude\");\n        return Ok(None);\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Extract the sign into side/direction before parsing the magnitude of any signed wire field","Route signed deltas through parse_quantity_from_ticks(ticks.abs(), decimals)","Confirm the field is genuinely unsigned before using parse_quantity on it","Log and drop unexpected negatives rather than coercing to zero"],"tags":["rust","parsing","validation","quantity","sign"],"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-14T00:17:10.932Z"}