Skip to content

fix: stop dropping never-filled orders from the fade rate numerator - #477

Draft
claude[bot] wants to merge 1 commit into
mainfrom
fix/fade-rate-null-token-drops-never-filled
Draft

fix: stop dropping never-filled orders from the fade rate numerator#477
claude[bot] wants to merge 1 commit into
mainfrom
fix/fade-rate-null-token-drops-never-filled

Conversation

@claude

@claude claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Requested by Cody Born · Slack thread

⚠️ Draft on purpose. This is live circuit-breaker logic — it changes which fillers get blocked. Please review before this goes anywhere near prod.

Before / After

Before: a filler that wins an order and then never fills it does not count toward its fade rate at all. The order is dropped from the fade query entirely — it contributes to neither the numerator nor the denominator. Never filling is the most complete form of fading, and it was invisible.

After: it counts. A won-and-never-filled order is scored as a fade, exactly as V2_FADE_RATE_SQL already says it should be (WHEN fillTimestamp IS NULL THEN 1).

How

The latestRfqsV2 view projected tokenIn/tokenOut from the archivedorders side of a LEFT OUTER JOIN. A never-filled order has no archivedorders row, so both columns come back NULL.

The fade query then filters permissioned tokens with no NULL handling:

WHERE LOWER(tokenIn) NOT IN ('0x7712c342…', '0x14d60e7f…')
AND   LOWER(tokenOut) NOT IN ('0x7712c342…', '0x14d60e7f…')

NULL NOT IN (...) evaluates to NULL, not TRUE, and a WHERE clause keeps only rows that evaluate to TRUE. So every never-filled order was silently deleted by a clause meant to exclude two tokens. Standard three-valued logic — not a Redshift quirk.

postedorders carries its own non-nullable tokenIn/tokenOut, so the fix is to source the filtered columns from the posted-orders side (latestOrdersV2) instead. The permissioned-token exclusion keeps working as intended, and now evaluates to FALSE for a genuinely permissioned order rather than NULL.

fillTimestamp deliberately stays on the archived side — its NULL is the never-filled signal, and it is read with an explicit IS NULL, which is NULL-safe. The test pins that asymmetry so it doesn't get "tidied up" later.

Measured impact

Over 2026-07-15 → 2026-08-04 (21 complete days, BigQuery mirrors of postedorders / archivedorders, replicating the cron's order of operations):

Rows silently dropped by the predicate 1,166
…of which never-filled 1,166 (all of them)
…of which genuinely permissioned 0
Fade numerator, as written 2,511
Fade numerator, corrected 3,677
Numerator loss 31.7% of all fades

Two things worth calling out:

  • The correlation is perfect in both directions: every never-filled order in the cohort had a NULL tokenIn, and every NULL-tokenIn order was never-filled. Not one survived the predicate.
  • Not a single order in those 21 days actually involved a permissioned token. The clause's intended function was a complete no-op over the window; its only observable effect in production was deleting the never-filled cohort.

What this does NOT fix — please don't read it as more than it is

This fix alone would not have blocked the currently-worst fillers. Riverside, Orbt, Elk Capital and Wraxyn all exceed the 12% threshold with the bug present, in the large majority of evaluation hours:

filler smoothed rate as-written hours over 12% as-written hours the fix flips
Riverside 55.1% 302 / 306 3
Orbt 28.4% 156 / 157 0
Elk Capital 26.0% 314 / 479 2
Wraxyn 16.3% 363 / 504 130

If those fillers were not being blocked in production, this bug does not explain why — the cause is elsewhere and should be chased separately.

Where the fix actually changes the outcome is the band of mid-range faders sitting near the line, whose true rate is roughly 12–25%:

  • KyberSwap/Kipseli: 0 → 20 hours over threshold (loses 71.7% of its fades to the bug)
  • Kodea: 0 → 24 hours (73.9% of its fades dropped)
  • ElFomo 24 → 86, Portus 14 → 76, Prycto 77 → 148, Frax 24 → 48

That is the honest severity: the bug converted a real fade signal into invisibility for moderate faders, while the egregious ones tripped anyway.

Measurement caveat: the rolling replica behind the hours-over-threshold columns omits fadeWindowStart (the per-filler post-block clean-slate floor), which lives in DynamoDB and wasn't reachable from the analysis environment. That over-states hours-above-threshold for any filler that was actually blocked and returned. It biases both variants identically, so the as-written-vs-corrected comparison (the flip counts) is unaffected.

Scope

Deliberately one defect only. No change to FADE_RATE_BLOCK_THRESHOLD, LAPLACE_ALPHA/LAPLACE_BETA, the 24h window, ORDERS_PER_FILLER_LIMIT, or MAX_FILLER_ADDRESSES. Other findings in this area are still under investigation and belong in their own PRs.

Net diff: two identifiers in the view's projection list, two exports so the SQL can be asserted on, and one new test file.

Schema verification

Per the repo's CLAUDE.md (Redshift schemas are owned by data-eng-workflows, and "a column may exist but only be populated for some order types"):

  1. tokenIn / tokenOut are declared STRING in data-eng-workflowslib/spaces/uniswap_x/functions/uniswap_x_hourly_config/tables/load/posted_orders.yaml.
  2. Populated, not just declared — over the same 21 days, across 39,101 Dutch_V2/Dutch_V3 posted orders: 0 NULL, 0 empty string on the posted side.
  3. Same values as the archived side where both exist: 0 mismatches on LOWER(tokenIn) and 0 on LOWER(tokenOut) across every joined row. The fix does not change which token the filter compares.

Test plan

New test/repositories/fades-repository.test.ts. Rather than string-matching the fixed spelling, it asserts the underlying invariant — every column the unguarded NOT IN predicate filters on must be projected from the non-nullable posted-orders side — by parsing the view's projection list into alias → source table. That also catches a future regression on a different column.

Verified red-before / green-after: reverting just the two identifiers back to archivedOrders fails 2 of the 7 tests; with the fix all 7 pass.

$ yarn build      # tsc — Done in 32.60s, exit 0
$ yarn lint       # prettier --check + eslint — 0 errors (79 pre-existing warnings, none in changed files), exit 0
$ yarn test:unit  # 30 suites passed, 265 tests passed, exit 0

No visual surface — backend cron SQL only.

Note for whoever merges

Open PR #464 ("bound V2 fades view truncation to lookback window with deterministic ORDER BY") touches the same view and adds the same two exports and the same test file path, for a different defect. The changes are orthogonal in substance but will conflict textually; whichever lands second needs a trivial merge (two describe blocks, identical export lines). I kept the export comment wording identical to #464's to make that resolution mechanical.

Co-authored-by: Cody Born cody.born@uniswap.org


Generated by Claude Code

@claude claude Bot assigned codyborn Aug 6, 2026
@claude
claude Bot force-pushed the fix/fade-rate-null-token-drops-never-filled branch from 7571d72 to 3adb781 Compare August 6, 2026 02:28
@codyborn
codyborn requested a review from gretzke August 6, 2026 03:14
The latestRfqsV2 view projected tokenIn/tokenOut from the archivedorders
side of a LEFT OUTER JOIN, and the fade query filters permissioned tokens
with an unguarded LOWER(tokenIn) NOT IN (...). A never-filled order has no
archivedorders row, so those columns are NULL, and NULL NOT IN (...) is
NULL rather than TRUE -- the WHERE clause therefore discarded every
never-filled order, which is exactly the "never filled => fade" cohort.

postedorders carries its own non-nullable tokenIn/tokenOut (confirmed in
data-eng-workflows posted_orders.yaml, and measured 0 NULL / 0 empty over
39,101 Dutch_V2/V3 orders in the last 21 days), so source the filtered
columns from the posted-orders side. The permissioned-token exclusion keeps
working as intended and now evaluates to FALSE for a genuinely permissioned
order instead of NULL. fillTimestamp deliberately stays on the archived
side: its NULL is the never-filled signal and it is read with IS NULL.

Measured over 2026-07-15..2026-08-04: 1,166 rows dropped, all of them
never-filled, cutting the fade numerator from 3,677 to 2,511 (31.7%).

Co-authored-by: Cody Born <cody.born@uniswap.org>
Co-Authored-By: Claude <noreply@anthropic.com>
@claude
claude Bot force-pushed the fix/fade-rate-null-token-drops-never-filled branch from 3adb781 to 96ae282 Compare August 6, 2026 03:25
@codyborn codyborn closed this Aug 12, 2026
@codyborn codyborn reopened this Aug 18, 2026
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