nautechsystems/nautilus_trader · error · anyhow::Error

AX {field} scale must not exceed 26 for exact percent conver

Error message

AX {field} scale must not exceed 26 for exact percent conversion, was {scale}

What it means

margin_percent_to_rate converts a percent to a rate by shifting the decimal scale up by 2 (equivalent to dividing by 100); rust_decimal supports at most 28 fractional digits, so the source percent must have scale <= 26. A margin percent serialized with 27+ decimal places fails this check before the conversion would overflow.

Source

Thrown at crates/adapters/architect_ax/src/http/parse.rs:406

    anyhow::ensure!(
        maintenance_margin_pct > Decimal::ZERO,
        "AX maintenance_margin_pct must be positive, was {maintenance_margin_pct}"
    );
    anyhow::ensure!(
        maintenance_margin_pct <= initial_margin_pct,
        "AX maintenance_margin_pct {maintenance_margin_pct} exceeds initial_margin_pct {initial_margin_pct}"
    );

    Ok((
        margin_percent_to_rate(initial_margin_pct, "initial_margin_pct")?,
        margin_percent_to_rate(maintenance_margin_pct, "maintenance_margin_pct")?,
    ))
}

fn margin_percent_to_rate(value: Decimal, field: &str) -> anyhow::Result<Decimal> {
    let normalized = value.normalize();
    let scale = normalized.scale();
    anyhow::ensure!(
        scale <= 26,
        "AX {field} scale must not exceed 26 for exact percent conversion, was {scale}"
    );
    Decimal::try_from_i128_with_scale(normalized.mantissa(), scale + 2)
        .with_context(|| format!("Failed to convert AX {field} percentage to a rate"))
}

/// Parses an Ax balances response into a Nautilus [`AccountState`].
///
/// Ax provides a simple balance structure with symbol and amount.
/// The amount is treated as both total and free balance (no locked funds tracking).
///
/// # Errors
///
/// Returns an error if balance amount parsing fails.
pub fn parse_account_state(
    response: &AxBalancesResponse,
    account_id: AccountId,

View on GitHub (pinned to a4b06ed870)

Solutions

  1. Round or normalize the margin percent values in AX venue config to a sane precision (a few decimal places is plenty for percents)
  2. Fix the upstream serializer so it emits fixed-precision decimals instead of full float expansions
  3. If high precision is genuinely required, quantize in the adapter before conversion (e.g. rescale to <= 26) via a PR with tests

Example fix

// before (AX contract metadata)
"initial_margin_pct": "1.0000000000000000000000000001"
// after
"initial_margin_pct": "1.0"
Defensive patterns

Strategy: validation

Validate before calling

// Check decimal scale before percent conversion (mirror of the adapter's guard)
fn margin_scale_ok(v: Decimal) -> bool {
    v.normalize().scale() <= 26
}

if !margin_scale_ok(initial_margin_pct) || !margin_scale_ok(maintenance_margin_pct) {
    anyhow::bail!("margin percent scale exceeds 26 digits; round before submitting");
}

Try / catch

match parse_instrument(&definition, ts_event, ts_init) {
    Ok(instrument) => Ok(instrument),
    Err(e) if e.to_string().contains("scale must not exceed 26") => {
        log::error!("margin percent serialized with >26 fractional digits; fix upstream precision");
        Err(e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: A margin percent value like 1.0000000000000000000000000001 (scale 27) reaches parse_margin_rates — typically from a float round-trip that emits full f64 precision, or an upstream system generating unnormalized decimals.

Common situations: Integrations that build definitions from f64 and serialize without rounding; JSON numbers with absurd precision from spreadsheets or ETL pipelines; normalizing a value like 1E-27 leaves a high scale even though the mantissa is tiny.

Related errors


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