nautechsystems/nautilus_trader · error

Lighter GTD expire_time must be at least 5 minutes from now

Error message

Lighter GTD expire_time must be at least 5 minutes from now (plus 1 second transport margin)

What it means

Lighter requires a GTD order's expiry to be at least ORDER_EXPIRY_MIN_GTD_MS (5 minutes plus a 1s transport margin) from submission time; expire_time closer than that is rejected with this error. This prevents orders that would instantly expire or race the venue clock.

Source

Thrown at crates/adapters/lighter/src/websocket/dispatch.rs:2068

/// - `Gtc` / `Day` / `Gtd` without expiry: `now_ms + ORDER_EXPIRY_DEFAULT_GTC_MS`.
///   The venue rejects `-1` for these TIFs with `21711 invalid expiry`.
pub(crate) fn order_expiry_for(
    order_type: OrderType,
    tif: &TimeInForce,
    expire_time: Option<UnixNanos>,
    now_ms: i64,
) -> anyhow::Result<i64> {
    if order_type == OrderType::Market {
        return Ok(ORDER_EXPIRY_IOC);
    }

    if matches!(tif, TimeInForce::Gtd)
        && let Some(ts) = expire_time
    {
        let expiry_ms = (ts.as_u64() / 1_000_000) as i64;
        let min_expiry_ms = now_ms.saturating_add(ORDER_EXPIRY_MIN_GTD_MS);
        let max_expiry_ms = now_ms.saturating_add(ORDER_EXPIRY_MAX_GTD_MS);
        anyhow::ensure!(
            expiry_ms >= min_expiry_ms,
            "Lighter GTD expire_time must be at least 5 minutes from now (plus 1 second transport margin)",
        );
        anyhow::ensure!(
            expiry_ms <= max_expiry_ms,
            "Lighter GTD expire_time must be no more than 30 days from now",
        );
        return Ok(expiry_ms);
    }

    if is_conditional_order(order_type) && matches!(tif, TimeInForce::Ioc) {
        return Ok(now_ms.saturating_add(ORDER_EXPIRY_DEFAULT_GTC_MS));
    }

    if matches!(tif, TimeInForce::Ioc | TimeInForce::Fok) {
        return Ok(ORDER_EXPIRY_IOC);
    }

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure expire_time is nanoseconds since epoch and set GTD expiries at least ~5 minutes in the future.
  2. Use TimeInForce::Gtc (or Ioc/Fok) instead of Gtd for orders intended to live shorter than 5 minutes.
  3. Check unit conversion: ts.as_u64() / 1_000_000 must yield milliseconds; verify the caller isn't passing seconds.

Example fix

// before: short-lived GTD rejected
let expire_ns = now_ns + 30_000_000_000; // 30s
// after
let expire_ns = now_ns + 300_000_000_000; // >= 5 minutes, or use TimeInForce::Ioc
Defensive patterns

Strategy: validation

Validate before calling

let expiry_ms = (expire_ts.as_u64() / 1_000_000) as i64;
let now_ms = current_unix_ms();
if expiry_ms < now_ms + 5 * 60_000 + 1_000 {
    // widen expiry or switch to GTC/Ioc before submitting
}

Prevention

When it happens

Trigger: Submitting a GTD order whose expire_time (in Unix nanoseconds) converts to a millisecond timestamp earlier than now_ms + ORDER_EXPIRY_MIN_GTD_MS — e.g. expire_time only seconds away, already past, or computed in the wrong unit.

Common situations: Passing seconds instead of nanoseconds in expire_time; GTD times computed for short-lived orders (seconds-level TTL); clock skew between the client and venue making a nominally valid expiry too close.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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