nautechsystems/nautilus_trader · error

{e}

Error message

{e}

What it means

This panic fires inside `duration_days`, which converts a day count into a `SignedDuration` for time-bar aggregation. The underlying `try_duration_days` fails in two ways: `days * 24` overflows i64 ('days overflow i64 hours'), or the hour count is outside the representable duration range ('days exceed signed duration range'). It is reached from `BarSpecification` step handling for Day/Week/Month/Year aggregations (bar.rs:180-190 and 577-587), so the step in the bar type is far too large.

Source

Thrown at crates/model/src/data/bar.rs:358

    // Floor to start of day
    let day_start = Offset::UTC
        .to_timestamp(Offset::UTC.to_datetime(now).date().at(0, 0, 0, 0))
        .expect("valid UTC day start");
    let base_time = day_start + daily_time_origin;

    let time_difference = base_time.duration_until(now);
    let period_ns = period.as_nanos();
    debug_assert_ne!(period_ns, 0, "bar period must be non-zero");

    // Use div_euclid for floor division (rounds toward -inf, not zero)
    // so negative deltas (now before origin) yield the previous period boundary
    let num_periods = time_difference.as_nanos().div_euclid(period_ns);

    base_time + SignedDuration::from_nanos_i128(num_periods * period_ns)
}

fn duration_days(days: i64) -> SignedDuration {
    try_duration_days(days).unwrap_or_else(|e| panic!("{e}"))
}

fn try_duration_days(days: i64) -> anyhow::Result<SignedDuration> {
    let hours = days
        .checked_mul(24)
        .ok_or_else(|| anyhow::anyhow!("days overflow i64 hours"))?;
    SignedDuration::try_from_hours(hours)
        .ok_or_else(|| anyhow::anyhow!("days exceed signed duration range"))
}

fn try_time_interval(step: usize, aggregation: BarAggregation) -> anyhow::Result<SignedDuration> {
    let step_i64 = i64::try_from(step)
        .map_err(|_| invalid_interval_step(step, aggregation, "step exceeds i64 range"))?;

    let duration = match aggregation {
        BarAggregation::Millisecond => SignedDuration::from_millis(step_i64),
        BarAggregation::Second => SignedDuration::from_secs(step_i64),
        BarAggregation::Minute => SignedDuration::try_from_mins(step_i64).ok_or_else(|| {

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Reduce the BarSpecification step so step x multiplier (1/7/30/365) days stays within a few thousand years at most.
  2. Check the unit of the step value: for Day/Week/Month/Year aggregations it is a count of those units, not seconds or milliseconds.
  3. If the step comes from external input, validate its magnitude before constructing the BarType; the validated path reports 'Invalid step in bar_type.spec.step' with the aggregation named instead of panicking.
  4. Find the offending BarType string in your config/logs and fix it at the source.

Example fix

// before: aggregation YEAR with an astronomical step count
let spec = BarSpecification::new(300_000_000, BarAggregation::Year, PriceType::Last);
let interval = spec.timedelta(); // panics: 'days exceed signed duration range'

// after: bound calendar-aggregation steps before constructing the bar type
fn step_ok(step: usize, agg: BarAggregation) -> bool {
    let days = step.saturating_mul(match agg {
        BarAggregation::Day => 1,
        BarAggregation::Week => 7,
        BarAggregation::Month => 30,
        BarAggregation::Year => 365,
        _ => return true,
    });
    days.checked_mul(24).map(|h| h <= 2_500_000_000_000_000).unwrap_or(false)
}
Defensive patterns

Strategy: validation

Validate before calling

fn calendar_step_ok(step: usize, agg: BarAggregation) -> bool {
    let days = step.saturating_mul(match agg {
        BarAggregation::Day => 1,
        BarAggregation::Week => 7,
        BarAggregation::Month => 30,
        BarAggregation::Year => 365,
        _ => return true,
    });
    days.checked_mul(24).map(|h| h <= 2_500_000_000_000_000).unwrap_or(false)
}
// call before constructing the BarType

Try / catch

Prefer validated construction (the builder reports 'Invalid step in bar_type.spec.step' by name). As a last-resort boundary: std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| spec.timedelta())) and treat Err(payload) as 'reject this bar type'. From Python, panics surface as pyo3_runtime.PanicException — catch it and drop the invalid spec.

Prevention

When it happens

Trigger: Calling interval/timedelta handling on a bar specification whose aggregation is Day, Week, Month, or Year with an extreme step: e.g. Year where step*365 days overflows the hours-to-duration conversion, or Day with step > i64::MAX/24 overflowing the `checked_mul(24)`. Also reachable via `find_closest_smaller_time` with such a period.

Common situations: A step typo with extra zeros, a step generated from config with confused units (seconds or milliseconds supplied where a day/week/month/year count is expected), or BarType strings assembled from external data. The magnitudes needed are astronomical, so in practice this means a units or data-quality bug, not a legitimate interval.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/34125e12926bd427. Report an issue: GitHub.