nautechsystems/nautilus_trader · error · anyhow::Error

AX requires whole contract quantities, was {}

Error message

AX requires whole contract quantities, was {}

What it means

quantity_to_contracts converts a NautilusTrader fixed-precision Quantity to AX's whole-contract count. ArchitectX deals in integral contracts only, so the raw fixed-point value must be an exact multiple of the FIXED_PRECISION scale (i.e. a whole number); fractional sizes such as 0.5 or 1.25 are rejected with the offending decimal shown. Note this fires even for values < 1 (e.g. 0.5 is fractional, not zero-contract).

Source

Thrown at crates/adapters/architect_ax/src/common/parse.rs:196

}

/// Converts a [`Quantity`] to an i64 contract count for AX orders.
///
/// AX uses integer contracts only. Uses integer arithmetic to avoid
/// floating-point precision issues.
///
/// # Errors
///
/// Returns an error if:
/// - The quantity represents a fractional number of contracts.
/// - The quantity is zero.
pub fn quantity_to_contracts(quantity: Quantity) -> anyhow::Result<u64> {
    let raw = quantity.raw;
    let scale = 10_u64.pow(FIXED_PRECISION as u32) as QuantityRaw;

    // AX requires whole contract quantities
    if !raw.is_multiple_of(scale) {
        anyhow::bail!(
            "AX requires whole contract quantities, was {}",
            quantity.as_f64()
        );
    }

    // QuantityRaw is u128 under the `high-precision` feature and u64 otherwise,
    // so the narrowing cast is conditional on the active feature set.
    #[allow(clippy::unnecessary_cast)]
    let contracts = (raw / scale) as u64;
    if contracts == 0 {
        anyhow::bail!("Order quantity must be at least 1 contract");
    }
    Ok(contracts)
}

/// Converts a [`ClientOrderId`] to a deterministic AX `cid` in the non-negative `int64` range.
///
/// Inbound WebSocket `cid` values remain `u64` because venue messages can exceed `int64`.

View on GitHub (pinned to a4b06ed870)

Solutions

  1. Round your order size to a whole number of contracts before submitting: qty = float(qty).__floor__() or an explicit round strategy
  2. Apply risk sizing in contract units rather than fractional notional
  3. Validate size against the instrument's lot_size before order creation

Example fix

# before (Python strategy)
order = self.factory.market(instrument_id, quantity=Quantity.from_str('2.7'))
# -> AX requires whole contract quantities, was 2.7

# after
order = self.factory.market(instrument_id, quantity=Quantity.from_int(2))
Defensive patterns

Strategy: validation

Validate before calling

qty = self._compute_position_size(...)  # float contracts
whole = int(qty)  # or use your rounding policy
if whole < 1:
    self.log.info(f'Skipping order: size {qty} < 1 contract')
    return
order = self.factory.market(instrument_id, quantity=Quantity.from_int(whole))

Type guard

from nautilus_trader.model.objects import Quantity

def is_whole_contracts(qty: Quantity) -> bool:
    return qty.as_double() == int(qty.as_double())

Try / catch

# Prefer pre-submission validation; adapter errors arrive async via order rejected events.
# Python strategy guard:
if qty.as_double() % 1 != 0:
    qty = Quantity.from_int(int(qty.as_double()))

Prevention

When it happens

Trigger: Submitting an order to the architect_ax adapter with a fractional quantity — e.g. Quantity.from_str('0.5') on a futures contract — when the adapter converts the size for the venue; commonly a strategy tuned for crypto venues running against AX.

Common situations: Porting crypto strategies (where fractional BTC/ETH sizes are normal) to Architect futures; position sizing math (e.g. risk-based sizing) producing 2.7 contracts; symbol metadata missing so no lot-size rounding occurs upstream.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@a4b06ed870 (2026-08-16). Data as JSON: /api/errors/8a55bcbfa42e4644. Report an issue: GitHub.