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
- Before calling, check start < end and skip/adjust the request when the range is empty
- Make the end bound exclusive in your caller logic (e.g. last_ts + 1)
- 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
- Check range emptiness before each poll and skip no-op requests
- Treat end as exclusive in your watermark logic
- Unit-test incremental fetch boundaries (start == end, start > end)
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
- Invalid ticker format '{ticker}', expected 'BASE-QUOTE' (e.g
- Invalid ticker format '{ticker}', base and quote cannot be e
- Stop buy trigger_price ({trigger_price}) must be >= limit pr
- Stop sell trigger_price ({trigger_price}) must be <= limit p
- Take profit buy trigger_price ({trigger_price}) must be <= l
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/9aceef42c0ffccca.
Report an issue: GitHub.