Skip to content

docs: correct Trading cube errors and document the query operator layer - #261

Merged
buddies2705 merged 7 commits into
mainfrom
docs/trading-cube-p0-fixes
Sep 2, 2026
Merged

docs: correct Trading cube errors and document the query operator layer#261
buddies2705 merged 7 commits into
mainfrom
docs/trading-cube-p0-fixes

Conversation

@buddies2705

@buddies2705 buddies2705 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Every change here was verified against the live API, independently re-tested, and adversarially challenged before being written. Claims that failed verification were dropped rather than published — including two that would have made the docs worse.

How this was validated

Findings came from a live capability probe of the Trading cube. They were then audited claim-by-claim, and every "confirmed" verdict was handed to a second agent whose job was to refute it. That mattered: 47% of the second-tier claims were wrong or imprecise, and several confident findings did not survive.

Two proposed fixes were withdrawn because re-testing showed the docs were already right:

  • Removing {descending: Block_Time} from two example orderBy arrays — that key is load-bearing; Block_Time is the bucket-end timestamp and does not extend the GROUP BY.
  • Rewriting a "current price" example as non-deterministic — the variation came from the example's own moving since_relative window, not from argmax.

No point-in-time figures are published anywhere, per the evergreen docs rule.

Correctness fixes

  • Volume-based intervals are inertVolumeBased / TargetVolume match nothing and return [] with HTTP 200, so a query against them reads as "no data". Documented as unavailable on two pages; replaced the copy-pasteable broken example.
  • Five subscription calculate blocks returned null — field references need a $ sigil in streams, and function calls like round() are unsupported there. Query blocks were never broken and are untouched.
  • pairs.md routed pool lookups to Market.Address, which on EVM is the protocol factory — following it silently returns a different pool.
  • IsQuotedInUsd is a denomination mode, not duplicated rows. Deleted the de-duplication advice readers were coding against.
  • Watchlist limitBy grouped by Token_Address, collapsing every native asset (they share an empty address) into one row.
  • Currency OHLC is a computed index, not a min/max envelope across chains.
  • statistics.md documented theils, which does not exist, inverted its sign semantics, and claimed an exception that never fires.
  • calculations.md described argmax backwards.
  • Solana retention was overstated 14xDEXPools and BalanceUpdates said ~7 days; both hold ~12 hours, re-measured on two consecutive days. DEXPools has no archive dataset at all.
  • Network filters take API names — Polygon is "Matic".

New: the query operator layer

docs/trading/ was organised entirely by use case, with no home for the operators underneath. New docs/trading/query-operators/:

Page Closes
selectwhere-screeners Chain-scale screening — advertised 3x, never shown
argmax-selectors Used 13x in our own docs, explained zero times
filters-and-operators "Filter Operators" returned 0 files across the tree
statistics ~15 metrics, zero hits under docs/trading/
sweeps-and-pagination limitBy as a whole-population sweep
calculate-expressions Used 18x, never explained
array-intersect-wallet-overlap The "who trades both" question

Plus docs/subscriptions/what-does-not-survive-streaming.mdselectWhere and if are absent from streams and 18 statistical metrics are query-only, so the "any query converts to a subscription" claim is false.

Trades cube caveats

A new section in trades-api.md covering what silently corrupts aggregates: a row is a swap leg not a transaction; AmountsInUsd.Base is built from a smoothed reference price and diverges on fast-moving tokens (Quote is the sound leg); and the cube emits exact duplicate legs with no distinguishing field — with a dedup key. Verified against Solana.Transfers.

Known gaps

Deliberately not included, and tracked separately:

  • Trading.Currencies structure — audited to DO_NOT_WRITE_YET; several claims were backwards, and separately its stablecoin rows report zero volume/marketcap/supply while bid:bitcoin does not reconcile. Those look like product bugs rather than doc bugs.
  • Whether calculate expressions can reference objects outside the query's own data. The grammar is now documented from direct measurement, but this specific question was deliberately not probed and remains open for engineering.
  • Error messages echo the generated SQL including physical table names — worth a look independently.

🤖 Generated with Claude Code

Gaurav Agarwal and others added 6 commits September 2, 2026 18:33
Every change here is backed by a live probe against streaming.bitquery.io,
independently re-tested, and adversarially challenged before being written.
Claims that failed verification were dropped rather than published.

Correctness fixes (P0):
- Volume-based intervals are inert (VolumeBased/TargetVolume match nothing);
  replaced the claim on two pages and the copy-pasteable broken example
- Five subscription calculate blocks returned null: field references need a
  $ sigil in streams, and function calls (round/floor/abs) are unsupported
  there. Query blocks left untouched — they were never broken
- pairs.md routed pool lookups to Market.Address, which on EVM is the
  protocol factory and silently returns a different pool
- Price.IsQuotedInUsd is a denomination mode, not duplicated rows; deleted
  the de-duplication advice readers were coding against
- Watchlist limitBy grouped by Token_Address, collapsing every native asset
  (they share an empty address) into one row
- Currency OHLC is a computed index, not a min/max envelope across chains
- statistics.md documented a metric named `theils` that does not exist,
  inverted its sign semantics, and claimed an exception that never fires
- calculations.md described argmax backwards; noted where: is rejected on
  Trading cubes in favour of if:
- array-intersect.md denied output fields and non-address values; both work
- Network filters take API names: Polygon is "Matic"

New: docs/subscriptions/what-does-not-survive-streaming.md — selectWhere and
if are absent from streams, 18 statistical metrics are query-only, interval:
is a silent no-op, and calculate behaves differently. Softened the two pages
claiming any query converts to a subscription.

New: docs/trading/query-operators/ — the operator layer had no home in a tree
organised entirely by use case:
- selectwhere-screeners: HAVING semantics and the correct pipeline order
  (selectWhere runs before orderBy and limitBy, not after)
- argmax-selectors: Field(maximum: Key) is argMax; row coherence within one
  key, ties, and the both-arguments footgun
- filters-and-operators: operator sets per scalar type, `any` as the only
  boolean combinator, per-cube branches, and the traps
- statistics: exact definitions, and which metrics are numerically unsafe on
  price columns
- array-intersect-wallet-overlap: the mechanism, and its gotchas

No point-in-time figures are published, per the evergreen docs rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hanics

Second batch of audited P1 items. As before, every claim was re-tested and
adversarially challenged; several from the original report were refuted during
the audit and are deliberately absent here.

trading-data-overview: new "what the Trading cube cannot do" section —
no liquidity/TVL/reserves, no trader grain outside Trades, no joins to
chain-level cubes (verified: no join* fields on any of the four), no
success/failure filter, no volume-based intervals, no native candle above
3600s. Corrected the custom-interval routing: 4h/daily/weekly candles ARE
buildable on the Trading cube by rolling native candles up.

pairs.md: Pairs is Tokens plus venue/quote identity with no Tokens-only
fields; liquidity is absent entirely. Row fan-out is per
(Market, Pool, Pool.Id, QuoteToken), not per market. Ranking.Position is a
competition rank with ties and gaps, Weight needs normalising, and a
Position eq 1 filter silently drops tokens whose top market was quiet.
Pool.Id is not "always empty" — it carries pool identity on singleton-contract
protocols, where Market.Address cannot.

tokens.md: Price.Average.Estimate is the price behind both supply valuations;
Volume.BaseAttributedToUsd; identity-field table (Token.Did is chain-level and
empty, Token.Decimals does not exist, a bare address in Token.Id returns
nothing); and the metadata-arrival duplicate that makes
(Token.Id, Interval.Time.Start) a non-unique key.

supply-fields: FDV is TotalSupply x Estimate — MaxSupply is reference data and
is never a multiplier. Supply is one currency-level figure repeated on every
row, so it is not derived per pool and summing MarketCap across rows
double-counts.

if.md: a Trading buy/sell-in-one-row example, the three metrics that reject
if:, and the nested-any crash.

New: query-operators/sweeps-and-pagination — limitBy as a whole-population
sweep, the difference between limitBy.offset and limit.offset, and why offset
without orderBy silently duplicates rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
limit: {count: 0} bypasses the documented 25,000-row cap. Replaced with an
explicit count so the example matches the published pagination guidance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retention (data-coverage-retention.mdx) — both figures re-measured on two
consecutive days against the live API:
- Solana DEXPools said ~7 days; it holds ~12 hours. Its archive/combined
  datasets do not exist at all — a query against them errors with
  "no API tables for cube DEXPool" — so historical pool state needs a cloud
  export, not a dataset switch.
- Solana BalanceUpdates said ~7 days; it holds ~12 hours.
- Rewrote the warning that generalised from the bad numbers. DEXTrades vs
  DEXTradeByTokens is the exception; every other Solana realtime cube measured
  (BalanceUpdates, DEXPools, Transfers, Instructions, TokenSupplyUpdates) holds
  roughly 12 hours.

A 14x overstatement on two cubes is the difference between planning a week of
history and getting half a day.

Trades (trades-api.md) — new section covering what silently corrupts
aggregates:
- A row is a swap leg, not a transaction; count distinct tx hashes instead.
- AmountsInUsd.Base is Amounts.Base x a smoothed reference price, so it
  diverges from the executed value on fast-moving tokens. AmountsInUsd.Quote
  is the sound leg for USD volume and PnL.
- The cube emits exact duplicate rows for the same swap leg, with no field
  distinguishing them; added a dedup key. Verified against Solana.Transfers.
- Market.Address means the pool on Solana and the factory on EVM; only
  Pool.Address and Market.Id are cross-chain stable.
- Realtime only, ~30 days, inclusive bounds, and not DEX-only — it carries
  Seaport and Polymarket activity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bitquery-for-ai.md is what an agent reads before planning an analysis, and it
described a price feed. Six rules added to §6 — all of which otherwise produce
a plausible wrong number rather than an error: the mandatory Duration filter
and its ~10x inflation, AmountsInUsd.Quote over .Base, duplicate swap legs,
case-sensitive network names, argmax semantics, and the absence of liquidity.

New §7 "Answering questions about a whole population": limitBy sweeps one row
per entity across the entire population rather than a top-N, selectWhere is a
server-side HAVING, and — the thing worth stating plainly — a window defined
relative to each row's own event cannot be expressed in GraphQL at all. That
last point is what makes people conclude an analysis needs a query per token
when it needs a handful.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
calculate() is used in 18+ example queries across the docs and has never been
explained. Every rule below was measured live, then adversarially re-tested —
which refuted one of my own findings and corrected two more.

- $name refers to a RESPONSE KEY in the same selection set (explicit alias, or
  the field path underscore-joined), not to a column. The $ is required;
  references are order-independent and chain to any depth.
- calculate never triggers a GROUP BY on its own. With no native metric the
  query stays in row mode and bare column references work; once any native
  metric appears the query groups, and a bare column must be a selected
  dimension. The resulting Code 215 names the dimension, not the expression,
  which sends people looking in the wrong place.
- The expression is wrapped in toFloat64(), so everything coerces to Float:
  string-producing expressions fail unless the string parses as a number,
  booleans flatten to 1/0 and dates to epoch numbers.
- Infinity, NaN and NULL are silently serialized as 0, so a divide-by-zero
  ratio is indistinguishable from a real zero and sorts first ascending.
  Documented the nullIf guard.
- ^ is not exponentiation and the ternary ?: collides with the SQL builder's
  placeholder; use pow() and if().
- orderBy accepts a calculate alias and composes with limitBy — the
  rank-by-derived-ratio pattern — but calculate takes no selectWhere, so the
  gate goes on a sibling aggregate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@buddies2705
buddies2705 force-pushed the docs/trading-cube-p0-fixes branch from f53143e to dda68c5 Compare September 2, 2026 13:06
Three pages gave three different numbers for the same ClickHouse threshold
(65,536 / 65,000 / 35,000). It is 2^16; the lower figures were observations of
where drift becomes visible, not the switch point. Stated once, the same way,
with the observed behaviour described separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@buddies2705
buddies2705 merged commit 9cf2783 into main Sep 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant