nautechsystems/nautilus_trader · error · anyhow::Error
AX fills pagination returned duplicate trade ID {}
Error message
AX fills pagination returned duplicate trade ID {} What it means
Every fill accepted from a page is inserted into a seen_trade_ids HashSet; if a trade_id was already seen the request fails immediately. With sort_ts=desc, any fill that is executed (or corrected) while pagination is in progress shifts later pages and re-serves rows. The guard prevents the same execution from being counted twice in reconciliation.
Source
Thrown at crates/adapters/architect_ax/src/http/client.rs:2272
if let Some(total_count) = response.total_count {
anyhow::ensure!(
total_count >= 0,
"AX fills total_count must be non-negative, was {total_count}"
);
if let Some(expected) = expected_total {
anyhow::ensure!(
total_count == expected,
"AX fills total_count changed during pagination: expected {expected}, was {total_count}"
);
} else {
expected_total = Some(total_count);
}
}
for fill in response.fills {
anyhow::ensure!(
seen_trade_ids.insert(fill.trade_id.clone()),
"AX fills pagination returned duplicate trade ID {}",
fill.trade_id
);
fills.push(fill);
}
if let Some(total_count) = expected_total {
anyhow::ensure!(
fills.len() as i64 <= total_count,
"AX fills pagination returned more unique rows ({}) than total_count {total_count}",
fills.len()
);
}
match response.next_cursor {
Some(next_cursor) => {
anyhow::ensure!(View on GitHub (pinned to a4b06ed870)
Solutions
- Retry the entire traversal when the account is quiet — duplicates from shifting pages are transient
- Pass explicit start/end anchored in the past so new executions fall outside the window
- Shrink the window (e.g. day-by-day slices) so each traversal is short
- If it reproduces deterministically on a frozen dataset, record the raw page sequence (curl with cursor param) and report to AX — the cursor chain is serving overlapping pages
Defensive patterns
Strategy: retry
Try / catch
match client.request_fill_reports(account_id, Some(start), Some(end)).await {
Ok(reports) => Ok(reports),
Err(e) if e.to_string().contains("duplicate trade ID") => {
log::warn!("overlapping fills pages (active trading?); retrying: {e}");
tokio::time::sleep(Duration::from_secs(3)).await;
client.request_fill_reports(account_id, Some(start), Some(end)).await
}
Err(e) => Err(e),
} Prevention
- Prefer fully historical windows that cannot gain new fills mid-traversal
- Defer reconciliation while strategies are submitting orders on the same account
- If duplicates recur on static data, persist raw pages once and analyze the cursor chain instead of retrying blindly
When it happens
Trigger: A fill executes between fetching page N and page N+1; because pages are ordered newest-first with an offset-style cursor, the new row pushes existing rows into the next page and the same trade_id appears twice. Also occurs if AX restates a fill with an unchanged trade_id.
Common situations: Reconciling fills on an actively trading account; long cursor chains (large 7-day windows with PAGE_SIZE 100) that raise the chance of overlap; concurrent connections mutating the same fill history.
Related errors
- AX fills total_count changed during pagination: expected {ex
- AX fills total_count must be non-negative, was {total_count}
- AX fills pagination returned more unique rows ({}) than tota
- AX fills returned an empty next_cursor
- AX fills returned an empty page with a next_cursor
AI-assisted analysis of nautechsystems/nautilus_trader@a4b06ed870 (2026-08-16).
Data as JSON: /api/errors/69d3a09328052056.
Report an issue: GitHub.