nautechsystems/nautilus_trader · error

canonical projection contains an unencoded floating-point va

Error message

canonical projection contains an unencoded floating-point value

What it means

Raised by `stringify_numbers` during canonical-value preparation. Any `Value::Number` that is neither i64 nor u64 is a float, which would leak nondeterministic `f64` formatting into the canonical projection; the code requires all numerics to be integral before encoding them as strings.

Source

Thrown at crates/backtest/src/result.rs:993

        "venue_order_ids" => Some(IdentityClass::VenueOrder),
        _ => None,
    }
}

fn stringify_numbers(value: &mut Value) -> anyhow::Result<()> {
    match value {
        Value::Array(values) => {
            for value in values {
                stringify_numbers(value)?;
            }
        }
        Value::Object(object) => {
            for value in object.values_mut() {
                stringify_numbers(value)?;
            }
        }
        Value::Number(number) => {
            anyhow::ensure!(
                number.is_i64() || number.is_u64(),
                "canonical projection contains an unencoded floating-point value"
            );
            *value = Value::String(number.to_string());
        }
        _ => {}
    }
    Ok(())
}

fn first_divergence(
    expected: &Value,
    actual: &Value,
    path: String,
) -> Option<CanonicalResultDivergence> {
    match (expected, actual) {
        (Value::Object(expected), Value::Object(actual)) => {
            let keys = expected

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Replace raw `f64` fields with domain types (`Price`, `Quantity`, `Money`) or `Decimal` so they serialize as strings.
  2. Use serde stringification (e.g. `#[serde(with = ...)]` or `as_raw().to_string()`) for any numeric that must survive canonicalization.
  3. Reproduce by running the failing backtest and dumping the canonical JSON to find the offending key.
  4. Add/refresh unit tests asserting no floats appear in canonical output.

Example fix

// before
pub quantity: f64,
// after
pub quantity: Quantity,  // serializes as string "100"
Defensive patterns

Strategy: validation

Validate before calling

let v = serde_json::to_value(&output)?;
fn has_floats(v: &serde_json::Value) -> bool {
    match v {
        Value::Number(n) => !(n.is_i64() || n.is_u64()),
        Value::Array(a) => a.iter().any(has_floats),
        Value::Object(o) => o.values().any(has_floats),
        _ => false,
    }
}

Prevention

When it happens

Trigger: A serialized order/position/account/event value contains an `f64`-backed JSON number — i.e. some field (price, quantity, money) was serialized from `f64` instead of a Decimal/int, or serde emitted a float during canonicalization.

Common situations: Using plain `f64` fields instead of the project's `Price`/`Quantity`/`Money` types in a struct that flows into backtest result output; calling `serde_json::to_value` on types that don't implement decimal-as-string serialization.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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