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

  1. Retry the entire traversal when the account is quiet — duplicates from shifting pages are transient
  2. Pass explicit start/end anchored in the past so new executions fall outside the window
  3. Shrink the window (e.g. day-by-day slices) so each traversal is short
  4. 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

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


AI-assisted analysis of nautechsystems/nautilus_trader@a4b06ed870 (2026-08-16). Data as JSON: /api/errors/69d3a09328052056. Report an issue: GitHub.