Skip to content

api,web: mark Kalshi rows computed from too few observations - #799

Merged
juan-malbeclabs merged 6 commits into
mainfrom
kalshi/latency-sample-floor
Aug 24, 2026
Merged

juan-malbeclabs merged 6 commits into
mainfrom
kalshi/latency-sample-floor

Conversation

@juan-malbeclabs

@juan-malbeclabs juan-malbeclabs commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary of Changes

  • Two metrics on the Kalshi scoreboard published numbers computed from a handful of rows, side by side with numbers computed from millions, with nothing to distinguish them. Both now carry a threshold, and a row below it shows the count behind it instead of the statistic.
  • Path latency needs 1000 observations per (feed, vantage). Over five samples p99 is the maximum of five samples, and the row renders p50 = p90 = p99 — one value repeated three times. Below the threshold the three percentile cells are dashed and the sample count is called out.
  • The race breakdown needs 100 races, at two grains: a vantage's own total, and each (vantage, competitor) cell inside a published row. A vantage with 3000 races against one feed and four against a freshly configured one was showing the second as a lead figure with nothing saying what it rested on.
  • Both thresholds travel in the payload (min_samples, min_races) so the page cannot hold a second copy of the number.
  • Nothing is filtered out, and that is deliberate: removing a row removes the evidence along with the number. An empty table cannot explain itself — in a low-volume window "no data available" reads as a broken capture — dropping one side of a pairing leaves the other alone at that vantage in the flattering direction, and a row that vanishes when the reader switches window reads as a recorder going down.
  • The path-latency query is scoped to the perps symbols, which is a separate defect the thresholds cannot reach. isKalshiDZFeed labels every tob_ source "DoubleZero" and kalshiEdgeWSArmFilter admits any of them, so a sports lane recorded at some vantage landed in this table as a DoubleZero row. A sample floor is only a volume proxy for that: a lane updating about once a second clears a thousand samples in seventeen minutes and then reads as solid. Only the perps symbols carry the venue orderbook stamp the metric is defined against.

What prompted it

On mainnet, four sources at the cmh vantage with five samples each reported 0.3 ms path latency, against the 23 ms the same vantage measures on the perps lane it actually carries. That reads as a hundredfold path advantage rather than as an unmeasured feed — the worst direction for this metric to be wrong in, since it flatters DoubleZero. The same rows also formed a vantage of their own in the breakdown, with a win rate over 8 to 40 races.

Those rows were spurious and have been removed at the source. Of the two changes here, the symbol scope is what stops that particular row from coming back; the thresholds are what stop the next thin row from being read as a result.

Diff Breakdown

Category Files Lines (+/-) Net
Core logic 2 +151 / -26 +125
Tests 1 +176 / -45 +131
Scaffolding 1 +4 / -0 +4
Total 4 +331 / -71 +260

Two thresholds and a query scope in one handler, the rendering that reads them, and the fixtures that now have to clear them.

Key files (click to expand)
  • api/handlers/kalshi_scoreboard.goKalshiLatencyMinSamples and KalshiVantageMinRaces as published display thresholds, and the perps symbol scope on the latency query
  • api/handlers/kalshi_scoreboard_test.go — burst fixtures, a test per threshold, and one for the symbol scope
  • web/src/components/kalshi-scoreboard-page.tsx — dashed percentiles, "too few races", and per-cell race counts
  • web/src/lib/api.ts — the two threshold fields

Testing Verification

  • TestKalshiPathLatency_ThinRowKeepsItsPlace — a five-sample competitor row beside a real 23 ms burst at the same vantage: both rows are returned, the thin one identified by a count below the published threshold. It pins the review's point that a pairing must not lose one side.
  • TestKalshiPathLatency_ExcludesSymbolsWithoutTheVenueClock — a sports lane seeded well past the sample floor is still absent. It would pass a volume check, which is the point of having it.
  • TestKalshiScoreboard_ThinVantageKeepsItsRow — a three-race vantage beside a real one: both published, the headline still counting every race the fleet ran.
  • The existing counting-semantics tests (dual-lane loss counted once, multi-competitor race counted once, per-vantage win share) seed bursts through one (source_ts_ms, bbo_hash) key per iteration, so they assert the same properties on rows the report would actually show. ExcludesIncomparableClocks gives every arm a burst over the threshold, so a single surviving row can only be the clock filter's doing.

Note

The thresholds are chosen to sit far below anything real and far above noise — a live vantage logs millions of observations in 24h, and a live pairing thousands of races an hour — not from the measured per-vantage distribution. Checking them against that distribution needs the feeds service directly: lake proxies those tables read-only and the public ClickHouse user cannot see them.

The race threshold stays an absolute count rather than a rate scaled by window: "fewer than a hundred races cannot carry a percentile" is true of the 1h window and of a ?symbol= split for the same reason it is true of 7d. The visible consequence is that a genuinely thin fleet shows counts at 1h where it shows percentages at 24h.

The path latency table put a row backed by five observations next to one
backed by fifteen million, with nothing in the number to say which was
which. Over five samples p99 is the maximum of five samples, and the row
renders p50 = p90 = p99 — one value repeated.

Observed on mainnet: four cmh sources with five samples each reported
0.3 ms against the 23 ms the same vantage measures on the perps lane it
carries, reading as a hundredfold path advantage rather than as an
unmeasured feed.

A (feed, vantage) row now needs 1000 observations in the window to be
published. Rows below it are dropped in Go rather than by a HAVING clause
so each one is logged by name: falling below the floor usually means a
producer that just started or one writing where it should not be.
Same defect as the path-latency floor, in the metric beside it. The vantage
dimension is discovered from the data — every measurement_node_id in the
race summary becomes a row — so a recorder that wrote a handful of races
rendered a win rate and lead percentiles next to a vantage backed by
millions. Measured on mainnet: a fabricated node carrying 8 to 40 races per
competitor sat beside one carrying 1.17 billion observations.

A vantage now needs 100 races in the window to appear in the breakdown, two
orders of magnitude below the smallest plausible live vantage in the
shortest window served. The floor is applied to the vantage dimension and
not to the competitor dimension: competitors are an operator allow-list, so
a thin competitor was configured deliberately and is worth seeing.

Thin vantages are dropped from the breakdown only. Their races still count
in the headline and the per-competitor totals, which come from separate
CUBE rows.
@github-actions

Copy link
Copy Markdown

🔗 Preview: https://pr-799.data.malbeclabs.com

Comment thread api/handlers/kalshi_scoreboard.go Outdated
Comment thread api/handlers/kalshi_scoreboard.go Outdated
Comment thread api/handlers/kalshi_scoreboard.go Outdated
Comment thread api/handlers/kalshi_scoreboard.go
Comment thread api/handlers/kalshi_scoreboard.go
Comment thread api/handlers/kalshi_scoreboard.go Outdated
Comment thread api/handlers/kalshi_scoreboard.go Outdated
Review on #799 found that filtering rows out of the payload created four
problems, all the same mistake — removing the evidence along with the
number. An empty table cannot explain itself, and in a low-volume window
"no data available" reads as a broken capture. Dropping one side of a
pairing left the other alone at that vantage, a one-sided comparison in the
flattering direction. A row that vanished when the reader switched window
read as a recorder going down. And the compensating INFO line ran on every
refresh — roughly 1500-3000 lines a day per replica for one thin recorder.

Both floors are now display thresholds carried in the payload
(`min_samples`, `min_races`), so the page withholds the number and shows
the count behind it. Nothing is removed and nothing is logged. The race
threshold applies per (vantage, competitor) cell as well as per vantage,
which is the same defect one dimension deeper: a vantage with 3000 races
against one feed and four against one configured this morning showed the
second as a lead figure.

The path-latency query is also scoped to the perps symbols. A sample floor
is a volume proxy for what was really a provenance defect: every tob_
source is labelled DoubleZero and the arm filter admits any of them, so a
sports lane updating about once a second clears a thousand samples in
seventeen minutes and then reads as a solid sub-millisecond DoubleZero row.
Only the perps symbols carry the venue orderbook stamp this metric is
defined against.
@juan-malbeclabs juan-malbeclabs changed the title api: do not publish Kalshi metrics computed from too few rows api,web: mark Kalshi rows computed from too few observations Aug 21, 2026
@juan-malbeclabs
juan-malbeclabs enabled auto-merge (squash) August 21, 2026 20:43
@juan-malbeclabs
juan-malbeclabs merged commit 4462a22 into main Aug 24, 2026
7 of 8 checks passed
@juan-malbeclabs
juan-malbeclabs deleted the kalshi/latency-sample-floor branch August 24, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants