nautechsystems/nautilus_trader · error

provider order expiration seconds {provider_expire_seconds:?

Error message

provider order expiration seconds {provider_expire_seconds:?} do not match cached order expiration seconds {cached_expire_seconds:?}

What it means

For GTD (good-tilled-date) orders, reconciliation compares the provider-reported expiration seconds with the cached order's expire_time. This error means they differ, so the venue row cannot be safely matched to the cached GTD order. The adapter only enforces this for TimeInForce::Gtd because other TIFs have no meaningful expiration.

Source

Thrown at crates/adapters/polymarket/src/execution/reconciliation.rs:515

        cached_order.time_in_force(),
    );
    validate_client_bound_order_quantity(provider_order, expected_quantity)?;
    let cached_price = cached_order
        .price()
        .context("cached Limit order is missing price")?;
    anyhow::ensure!(
        cached_price.as_decimal() == provider_order.price,
        "provider order price {} does not match cached order price {cached_price}",
        provider_order.price,
    );

    let provider_expire_seconds = provider_expire_time.map(|value| value.as_seconds());
    let cached_expire_seconds = cached_order
        .expire_time()
        .filter(|value| !value.is_zero())
        .map(|value| value.as_seconds());
    if cached_order.time_in_force() == TimeInForce::Gtd {
        anyhow::ensure!(
            cached_expire_seconds == provider_expire_seconds,
            "provider order expiration seconds {provider_expire_seconds:?} do not match cached order expiration seconds {cached_expire_seconds:?}",
        );
    }

    Ok(())
}

struct OrderRowResult {
    report: Option<OrderStatusReport>,
    counted_filtered: bool,
}

#[derive(Clone, Copy)]
pub(crate) struct TargetOrderReportScope<'a> {
    instrument_id: InstrumentId,
    venue_order_id: VenueOrderId,
    client_order_id: Option<ClientOrderId>,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check the cached order's expire_time against the venue's expiration and refresh/rebuild the cache entry for that venue_order_id.
  2. Verify expire_time is stored as Unix seconds consistently (not millis/nanos) across persistence and reconstruction.
  3. If the order expired and was re-placed, reconcile the new venue order id rather than reusing the old one.
  4. Cancel the GTD order and resubmit with the intended expiry, then re-run reconciliation.

Example fix

// before: reusing a cached GTD row with a stale expire_time
let cached = cache.order(&client_order_id)?; // expire 1700000000
// after: drop stale GTD rows before reconciliation
if cached.time_in_force() == TimeInForce::Gtd
    && cached.expire_time().as_seconds() < now_seconds()
{
    cache.rebuild_from_venue(&venue_order_id).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

if cached.time_in_force() == TimeInForce::Gtd {
    let venue = client.get_order(&venue_order_id).await?;
    let cached_secs = cached.expire_time().as_seconds();
    let venue_secs = venue.expire_time.map(|t| t.as_seconds());
    if cached_secs != venue_secs {
        return Err(anyhow!("GTD expire drift for {venue_order_id}: {cached_secs} vs {venue_secs:?}"));
    }
}

Type guard

fn gtd_expiry_matches(cached: &OrderAny, provider_secs: Option<i64>) -> bool {
    cached.time_in_force() != TimeInForce::Gtd
        || cached.expire_time().filter(|t| !t.is_zero()).map(|t| t.as_seconds()) == provider_secs
}

Try / catch

match result {
    Err(e) if e.to_string().contains("expiration seconds") => {
        log::warn!("GTD expiry drift; refreshing cache from venue");
        rebuild_order_from_venue(&venue_order_id).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Reconciling a cached GTD order where `provider_expire_seconds` (from the venue's open-order data, zero/none values filtered out of the cached side) is not exactly equal to the cached `expire_time().as_seconds()`. Occurs when the cached GTD order expired/was replaced, when expire_time was serialized with different units or truncated (nanos->seconds), or when the venue row belongs to a different order generation.

Common situations: Order placed with a GTD timeout that was later canceled and re-placed with a new expiry while the cache kept the old row; timezone/epoch-unit mismatch when persisting expire_time; adapter version change altering how expiration is parsed from the Polymarket API; clock skew at order creation.

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/3e3d94795c1b43b2. Report an issue: GitHub.