nautechsystems/nautilus_trader · error · anyhow::Error

start ({s}) must be before end ({e})

Error message

start ({s}) must be before end ({e})

What it means

request_trade_ticks validates that when both start and end UnixNanos bounds are supplied, start must be strictly less than end. anyhow::ensure! aborts the request with this message when start >= end, since dYdX would return an error or empty result for a reversed/empty interval.

Source

Thrown at crates/adapters/dydx/src/http/client.rs:1222

    /// Returns an error if the HTTP request fails, response cannot be parsed,
    /// or the instrument is not found in the cache.
    ///
    /// # Panics
    ///
    /// This function will panic if the API returns a non-empty trades response
    /// but `last()` on the trades vector returns `None` (should never happen).
    pub async fn request_trade_ticks(
        &self,
        instrument_id: InstrumentId,
        start: Option<Timestamp>,
        end: Option<Timestamp>,
        limit: Option<u32>,
    ) -> anyhow::Result<Vec<TradeTick>> {
        const DYDX_MAX_TRADES_PER_REQUEST: u32 = 1_000;

        // Validation
        if let (Some(s), Some(e)) = (start, end) {
            anyhow::ensure!(s < e, "start ({s}) must be before end ({e})");
        }

        let instrument = self
            .get_instrument(&instrument_id)
            .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;

        let ticker = extract_raw_symbol(instrument_id.symbol.as_str());
        let price_precision = instrument.price_precision();
        let size_precision = instrument.size_precision();
        let ts_init = self.generate_ts_init();

        // We always start pagination from the chain head (cursor = None). An earlier
        // version used `DEFAULT_BLOCK_TIME_SECS` with `get_height()` to skip directly
        // to an estimated target block, but any hardcoded block-time estimate that
        // underestimates the true average lands the cursor BEFORE the real `end`
        // block and silently drops the trades in the skipped window. Walking back
        // from head costs a few extra round-trips for stale `end` times but is
        // always correct. Per-call trades above `end` are filtered inside the loop.

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Before calling, check start < end and skip/adjust the request when the range is empty
  2. Make the end bound exclusive in your caller logic (e.g. last_ts + 1)
  3. Only pass bounds when both are present and ordered; otherwise pass None

Example fix

// before
client.request_trade_ticks(instrument_id, Some(start), Some(last_ts), None).await?;
// after
if start < last_ts {
    client.request_trade_ticks(instrument_id, Some(start), Some(last_ts), None).await?;
} // else: empty range, nothing to fetch
Defensive patterns

Strategy: validation

Validate before calling

if let (Some(s), Some(e)) = (start, end) {
    anyhow::ensure!(s < e, "trade tick range empty: start={s} end={e}");
}

Prevention

When it happens

Trigger: Calling request_trade_ticks(instrument_id, Some(start), Some(end), ...) with start == end or start > end, e.g. when end is computed as "last processed timestamp" and start equals it on the first poll.

Common situations: Incremental polling logic where the new start equals the previous end; clock skew making timestamps inverted; passing the same timestamp for both bounds expecting an inclusive single-instant query.

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/9aceef42c0ffccca. Report an issue: GitHub.