nautechsystems/nautilus_trader · error · anyhow::Error

AX maintenance_margin_pct {maintenance_margin_pct} exceeds i

Error message

AX maintenance_margin_pct {maintenance_margin_pct} exceeds initial_margin_pct {initial_margin_pct}

What it means

parse_margin_rates enforces maintenance_margin_pct <= initial_margin_pct; an inverted pair (maintenance exceeding initial) is nonsensical for risk modeling and fails instrument parsing. Both values may individually be positive — only their ordering is wrong.

Source

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

        ts_init,
    );

    Ok(InstrumentAny::PerpetualContract(instrument))
}

fn parse_margin_rates(
    initial_margin_pct: Decimal,
    maintenance_margin_pct: Decimal,
) -> anyhow::Result<(Decimal, Decimal)> {
    anyhow::ensure!(
        initial_margin_pct > Decimal::ZERO,
        "AX initial_margin_pct must be positive, was {initial_margin_pct}"
    );
    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)

View on GitHub (pinned to a4b06ed870)

Solutions

  1. Inspect both margin fields for the symbol in AX and swap them so maintenance <= initial
  2. Audit the integration/tool that writes venue config for swapped field mapping
  3. Reload instruments and confirm the symbol parses
  4. Add a config lint (maintenance <= initial) before publishing contract metadata

Example fix

// before (AX contract metadata)
{"initial_margin_pct": "0.5", "maintenance_margin_pct": "1.5"}
// after
{"initial_margin_pct": "1.5", "maintenance_margin_pct": "0.5"}
Defensive patterns

Strategy: try-catch

Validate before calling

// Lint margin ordering before definitions reach the parser
fn margin_order_ok(initial: Decimal, maintenance: Decimal) -> bool {
    initial > Decimal::ZERO && maintenance > Decimal::ZERO && maintenance <= initial
}

if !margin_order_ok(definition.initial_margin_pct, definition.maintenance_margin_pct) {
    anyhow::bail!("symbol {} margin percents inverted or invalid", definition.symbol);
}

Try / catch

match parse_instrument(&definition, ts_event, ts_init) {
    Ok(instrument) => Ok(instrument),
    Err(e) if e.to_string().contains("exceeds initial_margin_pct") => {
        log::error!("symbol {} has maintenance > initial margin; swap values in venue config", definition.symbol);
        Err(e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Venue config for a symbol swaps the two fields (maintenance 5, initial 2); an integration or spreadsheet import maps the API fields backwards; a fat-fingered template applied to new listings.

Common situations: Manual contract configuration with swapped margin columns; upstream API field-order changes misinterpreted by a sync tool; inherited misconfigurations surfacing when the symbol is first loaded.

Related errors


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