Skip to content

feat(0135): publish the latest priced close in current_prices - #228

Merged
stkrolikiewicz merged 6 commits into
developfrom
feat/0135_price-usd-outlier-exposure
Aug 21, 2026
Merged

feat(0135): publish the latest priced close in current_prices#228
stkrolikiewicz merged 6 commits into
developfrom
feat/0135_price-usd-outlier-exposure

Conversation

@stkrolikiewicz

Copy link
Copy Markdown
Collaborator

Summary

  • Implement the contract decided in task 0135 (2026-08-05): argMaxIf(close_usd, timestamp, close_usd > 0) on both aggregation sites in current.sqlunfiltered.price_usd and per_source.src_price.
  • price_usd becomes the latest priced close: up to one enrichment cycle of staleness (~25 min avg) instead of a permanent zero — production publishes the "0" stub on 88 of the top-200 volume assets today, XLM and EURC included. A remaining 0 now means exactly "no USD-priceable trade in the 24 h window". price_xlm and market_cap_usd stop collapsing with it.
  • Scope correction C2: a venue whose newest candle is un-enriched is carried in sources and in the vwap_24h weighting at its latest priced close; the src_price > 0 filter is now an explicit no-priced-candle-in-window rule instead of an enrichment-timing side effect.
  • current_mv_it.rs fixture 7 rewritten from pinning the bug to asserting the contract (price 1.90, change −5%/+90%, vwap weighs both venues, price_xlm 3.8) — 3/3 green on the 26.3.10.60 pin, with the pre-guard −100% control kept so the fixture cannot pass vacuously. §4.2 documents the semantics.
  • Deploy: apply current.sql (self-DROPs, TO table keeps serving during the ~1-min gap); no backfillcurrent_prices fully recomputes each minute. Post-deploy gate: zero_but_vwap_ok = 0 and the 0120 conformance suite's price-sentinel checks going green (42 of its 55 failures are this defect family). The task's original outlier question (failure mode 1) stays open and is deliberately not touched here.

Both argMax sites in current.sql now skip un-enriched candles
(argMaxIf close_usd > 0): price_usd trades up to one enrichment cycle
of staleness for the previous permanent zero, and a source with an
un-enriched tip stays in sources and the vwap weighting at its latest
priced close. The src_price > 0 filter becomes an explicit no-priced-
candle rule. Fixture 7 now asserts the contract instead of pinning the
bug; docs state the semantics in 4.2.
Review of the first cut found the unbounded carry unsafe: a chronically
unpriced venue kept voting in the 5.5 median (demonstrated case: the only
live venue evicted by two stale ones) and price_usd could certify a
24h-old close while updated_at read fresh. Both sites now carry only
while the newest priced candle is within 2h of the newest candle.

ref_7d took the oldest close available, so a freshly-listed asset
published a short move labelled 7-day - now the [7d, 5d] band. xlm_usd
uses the same argMaxIf idiom.

Coverage for the shapes that had none: over-bound asset (the only test
of the load-bearing 7d numerator guard), single-source carry, mask
arming over a carried price, market_cap against a seeded supply. New
static lint enforces the contract in CI. views.sql sentinel table, the
0072 runbook, dto.rs OpenAPI descriptions and 4.2 all synced.
@karczuRF

Copy link
Copy Markdown
Collaborator

Review findings

Reviewed the full diff, read current.sql in context, checked the consumers (views.sql, dto.rs, queries_ch.rs), and validated the new SQL semantics against a real ClickHouse 26.3.10.60 — the if(maxIf(...) >= max(...) - INTERVAL 2 HOUR, argMaxIf(...), toDecimal128(0,14)) types, the empty-group behaviour (maxIf → epoch 0 → sentinel), and toString(Decimal) trailing-zero trimming (so the new contains("\"2\"") assertion does hold).

Findings 1, 2 and 3 share a root: the docs assert a freshness guarantee in three places that the SQL does not actually provide.


🔴 1. HIGH — current.sql:244: the asset-level carry bound can still publish the 0 sentinel for XLM

per_source bounds the carry per source; unfiltered bounds it per asset. But max(timestamp) there spans all quote legs, including exotic-quoted pairs whose close_usd is 0 forever.

Concretely: XLM trades on an exotic pair every minute (never priceable) while its newest USD-priceable candle is 2 h 05 m old. maxIf(timestamp, close_usd > 0) fails the bound → price_usd = 0, price_xlm = 0, market_cap_usd = 0, and change_24h_pct / change_7d_pct computed on the sentinel. Meanwhile per_source — bounded against each source's own newest candle — carries the priced venues fine, so sources and vwap_24h publish real numbers.

That is the "zero price_usd beside a populated sources and a real vwap" shape 0135 calls indefensible, reintroduced through the new bound. The task file's own unchecked AC states the mechanism: "XLM's newest candle is … often an exotic-quote pair that will never be enriched at all."

Deriving price_usd from the already-bounded per_source rows, or restricting max(timestamp) to priceable legs, avoids it.

⚠️ No fixture covers "one never-priced venue + one priced venue on the same asset" — asset 6 (EXO) is single-source and entirely unpriced — so this is untested in either direction.

2. MEDIUM — docs/runbooks/0072-current-prices-mv-rollout.md:264,328: reinstates an abort gate the same file records as having caused three false aborts

It now says price_xlm = 0 on XLM is "worth aborting on", and a price_usd of "0" on native after deploy "IS a deploy-verification failure". Both are reachable with nothing broken: (a) the exotic-quote mechanism in finding 1; (b) enrichment lag exceeding the 2 h bound.

⚠️ The 2 h figure comes from 0144's ~50 min measurement, and that is now out of date. Task 0204's 2026-08-19 measurement put the USDT-leg enrichment ceiling around 30 h. Worse, as of 2026-08-20 the USDT pivot has been measured as having never priced a price_ohlcv_1m row (pivot_written = 0 against peg_written = 1,564,045) — see 0209 (root cause) and 0212 (the peg-valued rows). On that leg the lag is currently unbounded, so an operator following this runbook aborts a healthy deploy.

3. MEDIUM — current.sql:105: xlm_usd has no carry bound, so price_xlm does not inherit the ≤2 h guarantee

The scalar became argMaxIf (behaviourally identical to the old WHERE filter) but was not wrapped in the if(... - INTERVAL 2 HOUR ...) guard applied at the other two sites.

Scenario: XLM-quoted pairs stall in enrichment for 8 h while asset A's own quote leg keeps being enriched. price_usd(A) is fresh and in-bound, xlm_usd is an 8-h-old close, and price_xlm(A) publishes as current with unbounded staleness. dto.rs:19-20 ("shares price_usd's semantics") and the new views.sql sentinel entry ("both inherit the ≤2h staleness bound described under price_usd") are wrong for this path.

4. MEDIUM — current.sql:218: narrowing ref_7d to [7d, 5d] is an out-of-scope behaviour change, untested at both edges

Previously any priced 1h close in the trailing 7 d served as the baseline; now it must fall inside a 48 h window. An illiquid asset that trades most days but has no 1h candle between 5 and 7 days ago now publishes the 0 sentinel where develop published a value.

Every fixture places its 1h reference at exactly 6 d, so a wrong sign or a mis-sized cutoff at the now() - 5 DAY edge would not be caught. The comment also overstates the result — a baseline at 5 d is still a shorter-span move labelled 7d, just bounded to ~28 % span error rather than unbounded.

5. MEDIUM — current.sql:137 and :244: the 2 h bound is a magic literal at two sites, and the new lint does not pin it

Comments describe it as if it were a shared constant CARRY_BOUND, but it is duplicated. current_sql_uses_no_unguarded_argmax_on_close_usd enforces the argMaxIf spelling and a count of 5; nothing ties the two INTERVAL 2 HOUR occurrences together. Tuning one and missing the other makes the per-source and per-asset carries disagree — which produces exactly finding 1's contradiction, with the lint still green.

6. LOW — packages/prices-api/src/assets/dto.rs:12: the published OpenAPI description understates permitted staleness

It says the value "can trail real time by up to one enrichment cycle (~25 min typical)". The query permits the full 2 h carry — and (unchanged, but more material now that price_usd is presented as freshness-bounded) up to the full 24 h window when an asset stops trading and its last candle is priced. The bound constrains the gap between newest candle and newest priced candle, not the age of the published close.

7. LOW — packages/prices-clickhouse/schema/views.sql:501: the view header still advertises updated_at as the staleness signal

The header says the view returns "the latest USD price + updated_at for the consumer's own staleness policy". BE reads this surface in-cluster under the 0199 contract. After this change price_usd can be a carried close up to 2 h old while updated_at is the current refresh time, so a policy keyed on updated_at cannot see it. The sentinel table further up the file was updated to say so; this view's header was not.

Runbook: the post-deploy check is back to investigate-not-abort. Two
benign causes of a zero survive 0135 - legs enrichment never reaches,
and lag beyond the carry bound, measured at ~30h on the USDT leg (0204)
with the pivot having priced zero rows to date (0212) - so gating on it
would fail a healthy deploy, which this same file records happening
three times before.

ref_7d: the comment claimed the band removes the mislabelled span; it
only bounds it to ~28%, and the narrowing is a deliberate scope
addition - both now stated. New fixture 13 pins the recent edge: its
only 1h baseline sits at 4d, and without the cutoff the row publishes
a 4-day move as change_7d_pct (verified: +100 without, sentinel with).

dto.rs and views.sql header: the carry bound limits how far valuation
may lag the newest candle, NOT the age of the published close - an
asset that stopped trading publishes its last priced close, and
updated_at cannot see that.
…rice

Settled with okarcz. price_usd goes back to a plain argMaxIf: a stale
headline price for an asset that stopped trading was never this task's
bug (pre-0135 argMax published the same close), and blanking it costs
more than it buys - 1,091 of 4,444 prod assets (24.5%) already publish a
hard zero, and a consumer cannot separate 'worthless' from 'unknown'.

The bound survives in per_source, where the actual regression was: a
dead venue voting in the unweighted 5.5 median can evict the one live
venue. Its reference moves from max(timestamp) - which spans quote legs
enrichment can never price, so it advances forever - to now(). The 2h
comes from the enrichment schedule, not from an observed lag; the
pipeline has been failing on every run for ~2 days (0215).

Lint now pins the bound to exactly one site. Fixture 10 flips to
asserting the asymmetry (price published, venue dropped); the change_7d
numerator guard moves to asset 6, the only shape that still reaches it.
Contracts and the runbook follow, and 0216 carries the age column.
@stkrolikiewicz
stkrolikiewicz merged commit 7785fc6 into develop Aug 21, 2026
3 checks 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.

2 participants