Fills and trades are the only history endpoints that still reject time ranges wider than 7 days. This SoW migrates them to unbounded cursor pagination, removes the GUI code that works around the 7-day limit, and then removes the limit from the codebase. Transactions, funding transactions, funding rates, and orders already meet the requirements below.
Tracking: TBD.
The client-facing contract is
docs/internal/overview/pagination.mdx. On the server, every
history endpoint meets these five requirements:
next_cursor. Iteration ends only
when next_cursor is absent. A handler whose scan runs long
returns early with a scan-boundary cursor
({timestamp_ns}).FINAL and no per-page
COUNT(*). The handler deduplicates rows on read.
total_count is absent or a lower bound.SELECT_SETTINGS
(max_execution_time=4). A timeout returns a 400 that tells
the caller to narrow the range or add a filter. The handler detects the
timeout by downcasting to
KlickhouseError::ServerException { code, .. }, not by
matching error text.Server. get_historical_fills and
get_historical_trades
(rs/api-gateway/src/utils.rs) serve the user
/fills route and the admin fills and trades tabs. They are
the only callers of
ensure_within_max_window(MAX_HISTORICAL_QUERY_WINDOW_NS).
Their queries (rs/sdk-internal/clickhouse/src/trades.rs)
read trades FINAL, run a COUNT(*) … FINAL on
every page, carry no max_execution_time, and use a cursor
predicate without a range conjunct.
Table. trades is a
ReplacingMergeTree with
PRIMARY KEY (symbol, timestamp_ns, trade_id), monthly
partitions, and bloom indexes on the two order-id columns only. The
account filter
(taker_account_id = ? OR maker_account_id = ?) prunes no
granules.
GUI. These clamps and walkers remain:
| Location | Behavior | Endpoint has a cap |
|---|---|---|
App and california order history:
timeframeToQueryParams, isRangeTooWide in
TimeframeFilter |
Clamps a picked range to the latest 7 days; rejects picks wider than 7 days | No |
Admin useOrders, useTransactions,
useFunding: normalizeHistoryRangeNs |
Clamps ranges wider than 7 days to the latest 7 days | No |
Admin useLendingPoolActivity:
normalizeHistoryRangeNs |
Same clamp | Confirm in PR 1 |
Admin useFills, useTrades |
Same clamp, plus the "walk back 7 days" controls | Yes, until PR 3 |
App useFills with
useWindowedCursorPagination and
stepWindowedQuery |
Walks 7-day windows client-side, up to 90 days back | Yes, until PR 3 |
gui/packages/admin/src/utils/filterTime.ts labels
MAX_HISTORY_WINDOW_MS as "backend cap: 7 days", and the
admin preset list stops at 7d.
Alerting. The
clickhouse-query-timeouts/order-gateway rule watches
clickhouse_query_timeouts_total for
order_gateway. api_gateway, which serves fills
and trades, has no metrics recorder and no rule.
Order: PR 1, PR 2, and PR 5 have no dependencies. PR 3 needs PR 2 live on the target environment. PR 4 needs PR 3 deployed. PR 6 needs PR 3 and PR 4.
timeframeToQueryParams passes the picked range through
unchanged. A single bound stays a single bound. Remove the
isRangeTooWide check from
TimeframeFilter.useOrders, useTransactions, and
useFunding stop calling
normalizeHistoryRangeNs. Do the same for
useLendingPoolActivity after confirming its endpoint
accepts wide ranges.useFills and
useTrades clamp.filterTime.ts. Add 30d and All
presets to the tabs whose endpoints have no cap.trades account indexes (DDL and ops)bloom_filter(0.01) skip indexes on
maker_account_id and taker_account_id in
db/clickhouse/init.sql. Apply with
just migrate-db <env>.db/clickhouse/a-XXXX-materialize-trades-account-idx.sql
that runs MATERIALIZE INDEX for both indexes, and run it on
demo and prod.EXPLAIN indexes=1 on an
account-scoped trades query for a low-activity account
shows skipped granules, and the same query finishes inside the 4-second
budget.Rewrite ChTradeFilters, ChTradesQuery, and
both handlers to the shape used by
rs/sdk-internal/clickhouse/src/transactions.rs:
ensure_within_max_window calls. The range
becomes optional.FINAL. Deduplicate by trade_id
within the page, and overfetch enough rows to fill the page after
duplicates collapse.COUNT(*).SETTINGS {SELECT_SETTINGS}. Map the timeout to a
400 by downcasting, and call record_query_timeout() in that
branch.ts <= $c AND (…) for descending,
ts >= $c AND (…) for ascending).Page::from_overfetch and return a
scan-boundary cursor when the scan stops early./fills and the admin routes:
the range is optional, pages may be partial, and clients iterate on
next_cursor.Both handlers share these helpers, so one PR covers the user route and both admin tabs.
Tests: cursor round-trip across a page boundary that contains collapsed duplicates; timeout returns 400; unbounded account-scoped query; continuation from an empty page that carries a boundary cursor.
useFills.tsx uses useCursorPagination,
the same way useHistoricalOrders does.useWindowedCursorPagination.ts, and delete
stepWindowedQuery, HISTORY_MAX_LOOKBACK_NS,
and the rest of the windowing code in
gui/packages/app/util/historyWindow.ts with its tests.useFills and useTrades pass ranges
through unclamped and page with an infinite query and "Load more".
Remove the "walk back 7 days" controls.total_count is absent.api_gatewayapi_gateway a metrics recorder so that
clickhouse_query_timeouts_total is exported.clickhouse-query-timeouts/api-gateway rule to
configs/grafana/alerter-prod.yaml.tpl and
alerter-demo.yaml.tpl, matching the
order-gateway rule.This PR has no dependencies. Deploy it before PR 3 so that a slow access path raises an alert.
MAX_HISTORICAL_QUERY_WINDOW_NS and
ensure_within_max_window from
rs/sdk/src/protocol/time_range.rs, with their tests. After
PR 3 they have no callers. Both are public SDK items, so this is a
breaking SDK change and needs a changelog entry.normalizeHistoryRangeNs,
MAX_HISTORY_WINDOW_MS, and the allowWideRange
option from the admin GUI once nothing calls them.| Question | Proposed answer |
|---|---|
| Admin fills or trades request with no account, symbol, or order filter and an unbounded range | Reject with a 400 that asks for a filter or a bounded range.
validate_historical_orders_bounds does this for
orders. |
total_count on fills and trades |
Omit it. Transactions omits it, and the admin tables render without it. |
Measure p99 latency of unbounded account-scoped fills, trades, and orders requests on prod for one week, along with the daily count of timeout 400s per endpoint. Two items depend on the result:
| Item | Do it when |
|---|---|
Add a (timestamp_ns, trade_id) projection to
trades. This requires
deduplicate_merge_projection_mode on the table and a
rebuild of existing parts. |
Requests with no symbol filter spend most of their budget sorting. |
| Add the scan-boundary early return to the orders handler. Today an orders scan that exceeds the budget returns a 400. | Account-scoped orders requests still time out with
proj_account_ts in place. |
EXPLAIN indexes=1 shows skipped granules on an
account-scoped trades query./fills, admin fills, and admin trades accept an
unbounded range, return a 400 on timeout, run no per-page count, and
page correctly across collapsed duplicates.stepWindowedQuery,
useWindowedCursorPagination, and
normalizeHistoryRangeNs do not exist. No code in app,
admin, or california clamps a range to 7 days.total_count.read_overflow_mode='break' partial results.