nautechsystems/nautilus_trader · error · anyhow::Error

Binance Futures goodTillDate requires whole-second precision

Error message

Binance Futures goodTillDate requires whole-second precision

What it means

Binance's goodTillDate is expressed in milliseconds with second granularity, so the client requires the order's expire_time (UnixNanos) to be a whole number of seconds — expire_ns must be a multiple of 1_000_000_000. Any sub-second component (nanoseconds remainder) makes the request unrepresentable and the lifetime resolution fails fast rather than silently truncating the expiry.

Source

Thrown at crates/adapters/binance/src/futures/execution.rs:1188

    }

    anyhow::ensure!(
        matches!(
            order_type,
            OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
        ),
        "Binance Futures does not support GTD for order type {order_type:?}"
    );
    anyhow::ensure!(!post_only, "Binance Futures GTD cannot be post-only");

    anyhow::ensure!(
        product_type == BinanceProductType::UsdM,
        "Binance {product_type:?} Futures does not support native GTD"
    );

    let expire_time = expire_time.context("Binance Futures GTD requires an expire_time")?;
    let expire_ns = expire_time.as_u64();
    anyhow::ensure!(
        expire_ns.is_multiple_of(NANOSECONDS_IN_SECOND),
        "Binance Futures goodTillDate requires whole-second precision"
    );

    let minimum_ns = ts_now
        .as_u64()
        .checked_add(BINANCE_GTD_MIN_LEAD_SECS * NANOSECONDS_IN_SECOND)
        .context("Binance Futures GTD minimum timestamp overflow")?;
    anyhow::ensure!(
        expire_ns > minimum_ns,
        "Binance Futures goodTillDate must be strictly greater than current time plus {BINANCE_GTD_MIN_LEAD_SECS} seconds"
    );

    let good_till_date = expire_ns / NANOSECONDS_IN_MILLISECOND;
    anyhow::ensure!(
        good_till_date < BINANCE_GTD_MAX_MILLIS,
        "Binance Futures goodTillDate must be smaller than {BINANCE_GTD_MAX_MILLIS}"
    );

View on GitHub (pinned to a4b06ed870)

Solutions

  1. Round the expire_time down (or up, mind the minimum lead) to whole seconds before submitting
  2. Build expiries as seconds-precision datetimes (e.g. UnixNanos from a second-aligned timestamp)
  3. Check the remainder: expire_ns % 1_000_000_000 == 0

Example fix

// before
let expire = UnixNanos::now().checked_add(15 * 60 * 1_000_000_000)?; // may carry ns remainder

// after
const NS_PER_SEC: u64 = 1_000_000_000;
let expire = UnixNanos::from((UnixNanos::now().as_u64() / NS_PER_SEC + 15 * 60) * NS_PER_SEC);
Defensive patterns

Strategy: validation

Validate before calling

const NS_PER_SEC: u64 = 1_000_000_000;
let ns = expire_time.as_u64();
anyhow::ensure!(ns.is_multiple_of(NS_PER_SEC), "expire_time must be whole seconds");
// or normalize first: expire = (ns / NS_PER_SEC) * NS_PER_SEC;

Type guard

fn is_whole_second_expiry(ns: u64) -> bool {
    ns.is_multiple_of(1_000_000_000)
}

Prevention

When it happens

Trigger: Setting an expire_time derived from a sub-second-precise timestamp, e.g. now + 15*60*1e9 + 123ms of drift from a nanosecond clock, or a datetime parsed with millisecond/microsecond fractions.

Common situations: Expiries computed from UnixNanos::now() plus an offset without rounding; expiry datetimes from configs or external systems that carry fractional seconds.

Related errors


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