nautechsystems/nautilus_trader · error

failed to construct initial margin: {e}

Error message

failed to construct initial margin: {e}

What it means

margin_balance_from_user_stats_with_currency derives the initial margin as collateral minus available balance (floored at zero) and wraps it in a Money value in the settlement currency. Money::from_decimal rejects decimals whose precision exceeds the currency's precision, so if the Lighter user-stats values carry more decimal places than the settlement currency supports (e.g. more than 6 dp for USDC), the conversion fails and this error is raised.

Source

Thrown at crates/adapters/lighter/src/websocket/parse.rs:1143

/// # Errors
///
/// Returns an error if either `Money::from_decimal` call rejects the value.
pub fn margin_balance_from_user_stats(stats: &LighterUserStats) -> anyhow::Result<MarginBalance> {
    margin_balance_from_user_stats_with_currency(stats, Currency::get_or_create_crypto("USDC"))
}

/// Builds the cross-margin [`MarginBalance`] in the supplied settlement currency.
///
/// # Errors
///
/// Returns an error if either `Money::from_decimal` call rejects the value.
pub fn margin_balance_from_user_stats_with_currency(
    stats: &LighterUserStats,
    settlement_currency: Currency,
) -> anyhow::Result<MarginBalance> {
    let initial_dec = (stats.collateral - stats.available_balance).max(Decimal::ZERO);
    let initial = Money::from_decimal(initial_dec, settlement_currency)
        .map_err(|e| anyhow::anyhow!("failed to construct initial margin: {e}"))?;
    let maintenance = Money::from_decimal(Decimal::ZERO, settlement_currency)
        .map_err(|e| anyhow::anyhow!("failed to construct maintenance margin: {e}"))?;
    Ok(MarginBalance::new(initial, maintenance, None))
}

/// Assembles the unified [`AccountState`] from already-parsed components.
///
/// The reconciler in the `websocket::account_state` module owns the latest
/// snapshot of each input stream and calls this once per emission.
/// `AccountType::Margin` is invariant for Lighter; `base_currency` is `None`
/// because the account holds multiple spot currencies.
#[must_use]
pub fn build_unified_account_state(
    balances: Vec<AccountBalance>,
    margin: Option<MarginBalance>,
    account_id: AccountId,
    ts_event: UnixNanos,
    ts_init: UnixNanos,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Round the computed decimal to the settlement currency's precision before conversion: initial_dec = initial_dec.round_dp(settlement_currency.precision).
  2. Verify the settlement Currency instance used for the account matches Lighter's actual asset precision (e.g. USDC at 6 dp).
  3. Log stats.collateral, stats.available_balance and the currency precision when this fires to confirm which side has excess precision.
  4. If the currency is newly listed, register it with the correct precision instead of reusing a generic Currency::USD placeholder.

Example fix

// before
let initial_dec = (stats.collateral - stats.available_balance).max(Decimal::ZERO);
let initial = Money::from_decimal(initial_dec, settlement_currency)
    .map_err(|e| anyhow::anyhow!("failed to construct initial margin: {e}"))?;

// after
let initial_dec = ((stats.collateral - stats.available_balance).max(Decimal::ZERO))
    .round_dp(settlement_currency.precision);
let initial = Money::from_decimal(initial_dec, settlement_currency)
    .map_err(|e| anyhow::anyhow!("failed to construct initial margin: {e}"))?;
Defensive patterns

Strategy: validation

Validate before calling

if initial_dec.scale() > settlement_currency.precision as u32 {
    initial_dec = initial_dec.round_dp(settlement_currency.precision as u32);
}

Try / catch

match margin_balance_from_user_stats_with_currency(&stats, currency) {
    Ok(mb) => apply(mb),
    Err(e) => log::error!("margin conversion failed, check currency precision: {e}"),
}

Prevention

When it happens

Trigger: A Lighter account-stats websocket snapshot has collateral or available_balance with a decimal precision greater than the settlement currency's configured precision, making Money::from_decimal(initial_dec, settlement_currency) fail.

Common situations: Using a settlement Currency whose precision was configured too low (e.g. precision 2) while Lighter reports 6+ decimal places; a new settlement asset added on Lighter with a precision not matching the local Currency catalog; raw exchange values with unexpected extra decimals after a API change.

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/687e2811cc2337ac. Report an issue: GitHub.