nautechsystems/nautilus_trader · error

negative tick count {ticks} for Quantity

Error message

negative tick count {ticks} for Quantity

What it means

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.

Source

Thrown at crates/adapters/lighter/src/common/parse.rs:78

/// Order sizes on the wire are signed `i64` multiples of `10^-decimals`
/// base-asset units. Nautilus [`Quantity`] is non-negative, so a negative
/// `ticks` value is rejected: callers (e.g. position parsers) extract the
/// sign separately before invoking this parser.
///
/// Conversion routes through [`Decimal`] and [`Quantity::from_decimal_dp`]
/// so out-of-range tick counts return an error rather than panicking inside
/// the unchecked mantissa-exponent constructor.
///
/// # Errors
///
/// Returns an error if `decimals` exceeds [`MAX_DECIMALS`], if `ticks` is
/// negative, or if the resulting value exceeds the [`Quantity`] range.
pub fn parse_quantity_from_ticks(ticks: i64, decimals: u8) -> anyhow::Result<Quantity> {
    anyhow::ensure!(
        decimals <= MAX_DECIMALS,
        "size decimals {decimals} exceeds maximum {MAX_DECIMALS}",
    );
    anyhow::ensure!(ticks >= 0, "negative tick count {ticks} for Quantity");
    let decimal = Decimal::new(ticks, u32::from(decimals));
    Quantity::from_decimal_dp(decimal, decimals).map_err(|e| {
        anyhow::anyhow!("Quantity overflow for ticks={ticks}, decimals={decimals}: {e}")
    })
}

/// Converts a decimal string into a Nautilus [`Price`] at the requested precision.
///
/// # Errors
///
/// Returns an error if the string is not a decimal, if `precision` exceeds
/// [`MAX_DECIMALS`], or if the resulting value is out of range.
pub fn parse_price(value: &str, precision: u8) -> anyhow::Result<Price> {
    anyhow::ensure!(
        precision <= MAX_DECIMALS,
        "price precision {precision} exceeds maximum {MAX_DECIMALS}",
    );
    let decimal =

View on GitHub (pinned to 18893faf8b)

Solutions

  1. 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).
  2. 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.
  3. 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.
  4. 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.

Example fix

// before
let qty = parse_quantity_from_ticks(base_amount, size_decimals)?;
// after
let qty = parse_quantity_from_ticks(base_amount.abs(), size_decimals)?;
let side = if base_amount < 0 { OrderSide::Sell } else { OrderSide::Buy };
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_non_negative_ticks(ticks: i64) -> anyhow::Result<()> {
    anyhow::ensure!(ticks >= 0, "negative tick count {ticks}; extract sign before parsing");
    Ok(())
}

Type guard

fn is_non_negative(ticks: i64) -> bool { ticks >= 0 }

Try / catch

match parse_quantity_from_ticks(ticks, decimals) {
    Ok(q) => /* ... */,
    Err(e) if e.to_string().contains("negative tick count") => {
        tracing::warn!("signed amount {ticks}; handle sign separately");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/d47b5ee8484f8d91. Report an issue: GitHub.