Skip to content

Apply phantom-OI suppression to /api/stats protocol open interest - #2483

Open
0x-SquidSol wants to merge 1 commit into
dcccrypto:playgroundfrom
0x-SquidSol:fix/stats-phantom-oi-suppression
Open

Apply phantom-OI suppression to /api/stats protocol open interest#2483
0x-SquidSol wants to merge 1 commit into
dcccrypto:playgroundfrom
0x-SquidSol:fix/stats-phantom-oi-suppression

Conversation

@0x-SquidSol

@0x-SquidSol 0x-SquidSol commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

Apply the phantom-OI suppression to /api/stats' protocol-wide open-interest sum so
it matches /api/markets.

Closes #2482.

Why

computeStatsFromMarketsApi summed total_open_interest_usd directly, skipping the
isPhantomOpenInterest + computeDisplayOiUsd suppression /api/markets applies
(markets/route.ts:827-833). A market suppressed to $0 OI in the list (no accounts /
dust vault) therefore still inflated the protocol-wide totalOpenInterest. The
/api/markets comment names these helpers the single source of truth for both
routes; the stats shared-loader path just wasn't using them. (The existing
stats-phantom-oi-guard tests kept passing because they test a mirror of the
intended logic, not the route's actual summation.)

Changes

  • stats/route — compute per-market display OI with isPhantomOpenInterest +
    computeDisplayOiUsd (from total_accounts, vault_balance,
    total_open_interest, total_open_interest_usd) before summing, mirroring
    /api/markets exactly.
  • regression test — a raw sum over-counts a phantom market; the helper-based
    sum excludes it (and the phantom market is individually suppressed to 0).

Testing

  • npx tsc --noEmit — clean.
  • Stats cluster + new test — 8 files, 82 tests, all pass (incl.
    stats-phantom-oi-guard, stats-active-total-consistency, ProtocolStatsBar).

Notes

  • Frontend + devnet scope only; no program, keeper, or mainnet changes.
  • Reuses the existing shared helpers — no new predicate, so the two routes stay
    definitionally consistent.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected statistics so phantom open interest is excluded from displayed totals.
    • Preserved open interest from valid markets when calculating aggregate USD values.
  • Tests

    • Added regression coverage to verify phantom open interest is suppressed without affecting real-market data.

/api/stats' shared-loader path (computeStatsFromMarketsApi) summed
total_open_interest_usd directly, without the phantom-OI suppression /api/markets
applies (isPhantomOpenInterest + computeDisplayOiUsd). A market whose OI is
suppressed to $0 in the market list therefore still inflated the protocol-wide
totalOpenInterest stat — the exact drift the shared predicate exists to prevent
(markets/route.ts documents it as the single source of truth for both routes).

Compute per-market display OI with the shared helpers before summing, mirroring
/api/markets exactly.

- stats/route: isPhantomOpenInterest + computeDisplayOiUsd per row in the
  merged-rows OI sum
- add a regression test (raw sum over-counts a phantom market; the helper-based
  sum excludes it)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@0x-SquidSol
0x-SquidSol requested a review from dcccrypto as a code owner August 5, 2026 03:24
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

@0x-SquidSol is attempting to deploy a commit to the Khubair Nasir's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The /api/stats route now suppresses phantom open interest through shared helpers before summing display OI. New tests verify that phantom markets do not inflate totals while real-market OI remains included.

Stats aggregation and regression coverage

Layer / File(s) Summary
Apply display OI logic during stats aggregation
app/app/api/stats/route.ts
The route uses isPhantomOpenInterest and computeDisplayOiUsd before validating and accumulating market OI.
Validate phantom-OI aggregation
app/__tests__/api/stats-phantom-oi.test.ts
Tests cover phantom and real market fixtures, raw overcounting, and suppressed aggregate and per-market OI.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: dcccrypto

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies phantom-OI suppression in the /api/stats protocol open-interest total.
Linked Issues check ✅ Passed The changes apply the shared phantom-OI suppression helpers before aggregation and add a regression test for issue #2482.
Out of Scope Changes check ✅ Passed The changes are limited to the /api/stats aggregation logic and its focused regression test.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/__tests__/api/stats-phantom-oi.test.ts`:
- Around line 27-45: Replace the local helper-only assertions in the “stats
phantom-OI suppression” tests with an integration-style test of the `/api/stats`
route: mock `loadMergedMarketRows` with the phantom and real markets, invoke the
exported `GET`, and assert the response’s `totalOpenInterest` is 10,000. Keep
coverage for phantom suppression through the route so regressions in
`computeStatsFromMarketsApi` are detected.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e111a4ff-0393-425d-a01c-a62e5e3b21e1

📥 Commits

Reviewing files that changed from the base of the PR and between f2a3bbe and e7d0eaf.

📒 Files selected for processing (2)
  • app/__tests__/api/stats-phantom-oi.test.ts
  • app/app/api/stats/route.ts

Comment on lines +27 to +45
describe("stats phantom-OI suppression", () => {
it("raw summation over-counts a phantom market (the bug)", () => {
const rawSum = [phantom, real].reduce((s, m) => s + (m.total_open_interest_usd ?? 0), 0);
expect(rawSum).toBe(15_000); // phantom's stale $5k is wrongly included
});

it("applying isPhantomOpenInterest + computeDisplayOiUsd excludes it (the fix)", () => {
const fixedSum = [phantom, real].reduce((s, m) => {
const isPhantom = isPhantomOpenInterest(m.total_accounts, m.vault_balance);
const oi = computeDisplayOiUsd(m.total_open_interest_usd, isPhantom, m.total_open_interest);
return s + (oi ?? 0);
}, 0);
expect(fixedSum).toBe(10_000); // phantom suppressed to 0, only the real market counts
});

it("the phantom market is individually suppressed to 0, the real one is unchanged", () => {
expect(computeDisplayOiUsd(phantom.total_open_interest_usd, isPhantomOpenInterest(phantom.total_accounts, phantom.vault_balance), phantom.total_open_interest)).toBe(0);
expect(computeDisplayOiUsd(real.total_open_interest_usd, isPhantomOpenInterest(real.total_accounts, real.vault_balance), real.total_open_interest)).toBe(10_000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test the /api/stats route instead of a local copy of its logic.

These tests do not import app/app/api/stats/route.ts or invoke GET. They would pass before this PR because the local reduce already uses the shared helpers.

Mock loadMergedMarketRows, call GET, and assert that totalOpenInterest is 10_000 for these two markets. This makes the test fail if computeStatsFromMarketsApi again sums total_open_interest_usd directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/__tests__/api/stats-phantom-oi.test.ts` around lines 27 - 45, Replace the
local helper-only assertions in the “stats phantom-OI suppression” tests with an
integration-style test of the `/api/stats` route: mock `loadMergedMarketRows`
with the phantom and real markets, invoke the exported `GET`, and assert the
response’s `totalOpenInterest` is 10,000. Keep coverage for phantom suppression
through the route so regressions in `computeStatsFromMarketsApi` are detected.

@dcccrypto dcccrypto left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed on head e7d0eaff. Fix is correct. Suite clean: 2922 passed / 0 failed (282 files).

I chased one specific hazard and it doesn't apply — worth recording so nobody re-chases it

The new line coerces an unsupplied column to zero:

const isPhantomOI = isPhantomOpenInterest(numericOrNull(row.total_accounts), numericOrNull(row.vault_balance) ?? 0);

Two things made that look dangerous:

  1. computeStatsFromMarketsApi defines asSupplied() eight lines above precisely
    to distinguish "column not supplied" (undefined) from a value, and the zombie
    filter directly above uses asSupplied(row, "vault_balance") — so the new code
    bypasses the file's own discipline for the same field.
  2. isPhantomOpenInterest's doc comment warns about exactly this on the other
    parameter: "Unknown is not zero … an unknown account count simply abstains
    instead of voting phantom."
    Coercing an unknown vault to 0 makes
    0 < MIN_VAULT_FOR_OI true, i.e. votes phantom, and market-registry.ts:27-28
    records that vault_balance was dropped from the reduced schema — so a
    registry-only row genuinely has undefined there.

That would zero protocol OI whenever the live RPC read degrades — which
loadMergedMarketRows explicitly supports (if (!live) return m;, documented as
"partial RPC results degrade to the pre-merge behaviour rather than zeroing a
market out"
).

It's a non-issue, for two independent reasons, and I checked both before saying
anything:

  • /api/markets reads from the same loadMergedMarketRows() (:726) and applies
    the same coercion (numericOrNull(m.vault_balance) at :784 → ?? 0 at :826).
    So the PR mirrors it faithfully. Any change here would have made the two
    surfaces disagree again — the opposite of the goal.
  • On a registry-only row the outcome is unchanged anyway. total_open_interest_usd
    was dropped in the same schema reduction, so the old code's
    if (oiUsd != null …) skipped the row (contributing 0) and the new code returns
    0 via the phantom branch. Identical.

So the behaviour change is confined to rows that do have live data, which is
exactly the intended fix. isSaneMarketValue moving from the raw to the displayed
value is also fine — the only new value it sees is 0.

Test binding

stats-phantom-oi.test.ts imports the two helpers and re-applies them to local
fixtures; it never imports app/api/stats/route. Deleting the new block from the
route leaves it green, so it documents the intended arithmetic rather than pinning
the route to it.

Your own issue text makes this point better than I can — it's why the bug survived
in the first place:

The existing stats-phantom-oi-guard tests pass because they exercise a
mirror of the intended logic, not the route's actual summation.

That is precisely what the new test also does, so the next drift in this path will
be just as invisible. This is the sixth PR in the run with that shape (#2467,
#2470, #2472, #2475, #2480, and now this one) — and having diagnosed it explicitly
here, this seems like the right one to break the pattern on: assert the route's
summation, not a re-derivation of it.

Minor: the issue title got mangled

#2482's title reads C:/Program Files/Git/api/stats over-counts … — Git Bash
MSYS path conversion rewrote the leading /api/stats. Prefixing the command with
MSYS_NO_PATHCONV=1 (or writing //api/stats) avoids it. Cosmetic, but it makes
the issue hard to find by name.

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