{"record":{"id":"d47b5ee8484f8d91","repo":"nautechsystems/nautilus_trader","slug":"negative-tick-count-ticks-for-quantity","errorCode":null,"errorMessage":"negative tick count {ticks} for Quantity","messagePattern":"negative tick count (.+?) for Quantity","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/lighter/src/common/parse.rs","lineNumber":78,"sourceCode":"/// Order sizes on the wire are signed `i64` multiples of `10^-decimals`\n/// base-asset units. Nautilus [`Quantity`] is non-negative, so a negative\n/// `ticks` value is rejected: callers (e.g. position parsers) extract the\n/// sign separately before invoking this parser.\n///\n/// Conversion routes through [`Decimal`] and [`Quantity::from_decimal_dp`]\n/// so out-of-range tick counts return an error rather than panicking inside\n/// the unchecked mantissa-exponent constructor.\n///\n/// # Errors\n///\n/// Returns an error if `decimals` exceeds [`MAX_DECIMALS`], if `ticks` is\n/// negative, or if the resulting value exceeds the [`Quantity`] range.\npub fn parse_quantity_from_ticks(ticks: i64, decimals: u8) -> anyhow::Result<Quantity> {\n    anyhow::ensure!(\n        decimals <= MAX_DECIMALS,\n        \"size decimals {decimals} exceeds maximum {MAX_DECIMALS}\",\n    );\n    anyhow::ensure!(ticks >= 0, \"negative tick count {ticks} for Quantity\");\n    let decimal = Decimal::new(ticks, u32::from(decimals));\n    Quantity::from_decimal_dp(decimal, decimals).map_err(|e| {\n        anyhow::anyhow!(\"Quantity overflow for ticks={ticks}, decimals={decimals}: {e}\")\n    })\n}\n\n/// Converts a decimal string into a Nautilus [`Price`] at the requested precision.\n///\n/// # Errors\n///\n/// Returns an error if the string is not a decimal, if `precision` exceeds\n/// [`MAX_DECIMALS`], or if the resulting value is out of range.\npub fn parse_price(value: &str, precision: u8) -> anyhow::Result<Price> {\n    anyhow::ensure!(\n        precision <= MAX_DECIMALS,\n        \"price precision {precision} exceeds maximum {MAX_DECIMALS}\",\n    );\n    let decimal =","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/lighter/src/common/parse.rs#L60-L96","documentation":"parse_quantity_from_ticks converts a signed i64 tick count (a mantissa in units of 10^-decimals base-asset units) into a Nautilus Quantity, which is non-negative. The library rejects any negative tick count because Quantity cannot represent signed values; callers that deal with signed amounts (e.g. position/delta parsers) are expected to extract the sign before calling this parser. This guards against silently wrapping or panicking in the fixed-precision constructor.","triggerScenarios":"Calling parse_quantity_from_ticks with a negative i64 ticks value — e.g. passing a signed Lighter base-amount field (order delta of -5, negative position change) directly without taking its absolute value first.","commonSituations":"Parsing WebSocket order-fill or position-update payloads where Lighter sends signed base_amount values; applying a decrease/delta directly instead of its magnitude; copy-pasting a signed field into a size parser.","solutions":["Extract the sign before parsing: call parse_quantity_from_ticks(ticks.abs(), decimals) and handle the sign separately (e.g. as an order side or position direction).","Check the sign at the call site and route negative values to whatever decrement/removal logic the caller has, mirroring how position parsers in this adapter do it.","If a negative value is truly unexpected for your payload type, log the raw ticks value and treat the message as malformed rather than coercing it.","Audit which wire fields are signed (i64) vs unsigned (u32) in the Lighter API docs and use parse_price_from_ticks (u32) for unsigned fields so the type system prevents this."],"exampleFix":"// before\nlet qty = parse_quantity_from_ticks(base_amount, size_decimals)?;\n// after\nlet qty = parse_quantity_from_ticks(base_amount.abs(), size_decimals)?;\nlet side = if base_amount < 0 { OrderSide::Sell } else { OrderSide::Buy };","handlingStrategy":"validation","validationCode":"fn ensure_non_negative_ticks(ticks: i64) -> anyhow::Result<()> {\n    anyhow::ensure!(ticks >= 0, \"negative tick count {ticks}; extract sign before parsing\");\n    Ok(())\n}","typeGuard":"fn is_non_negative(ticks: i64) -> bool { ticks >= 0 }","tryCatchPattern":"match parse_quantity_from_ticks(ticks, decimals) {\n    Ok(q) => /* ... */,\n    Err(e) if e.to_string().contains(\"negative tick count\") => {\n        tracing::warn!(\"signed amount {ticks}; handle sign separately\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Take .abs() of signed i64 wire fields before size parsing and carry the sign as side/direction metadata","Use u32-based parse_price_from_ticks for unsigned wire fields so signedness errors surface at compile time","Match Lighter's field conventions: base_amount on deltas/fills is signed, sizes on orders are not","Add a unit test with a negative input for every signed-field parser"],"tags":["rust","parsing","validation","quantity"],"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"}