Skip to content

nova on the leverage bench and the cost tool, and the spread I nearly copied - #3168

Merged
Flotapponnier merged 3 commits into
devfrom
feat/nova-leverage-terms
Oct 22, 2026
Merged

Flotapponnier merged 3 commits into
devfrom
feat/nova-leverage-terms

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Nova on bench 295 and the perp-cost tool, which I had said was impossible.

The correction first

I concluded Nova could not go on either because it "lists neither BTC nor ETH". That was wrong, and wrong for a reason worth recording: I had read app.nova.markets/api/markets, which is the Hyperliquid-routed frontend list. The gTrade deployment is a different thing, and its pair 0 is BTC/USD, pair 1 ETH/USD.

All five selectors the leverage source uses answer on Nova's diamond, so the reader now takes the venue and the diamond as arguments rather than hardcoding Gains' — the same shape as the volume source in #3150, instead of a second copy of 150 lines of ABI decoding.

What the two deployments actually are

Read on chain 2026-10-22, both venues, both markets:

product bounds threshold Gains fee/leg Nova fee/leg
standard pair 1.1x–200x 90/75 between 25x and 60x 0.035% 0.050%
x500 degen 500x fixed 80% flat 0.020% 0.020%

One contract priced twice: identical thresholds and bounds, a dearer standard pair. The fee sits inside the liquidation trigger, so Nova leaves strictly less room at every leverage — 8.90% against 8.93% at 10x, 0.275% against 0.305% at 200x.

The near miss

perp-cost-model.ts carries a spreadBps per row and Gains' is 1.0. Copying it would have looked entirely right beside the identical thresholds, and been wrong.

Word 7 of pairs() is the pair spread at 1e10. It reads 100000000 on Gains and 0 on Nova. So Nova is 5.0 bps a leg, not 5.5, and the "43% dearer" I quoted earlier from the taker fee alone is really 25% once the spread is counted on both sides:

  • Gains: 3.5 taker + half of a 1.0 spread = 4.0 bps
  • Nova: 5.0 taker + no spread = 5.0 bps

That file's header says two venues were left out rather than guessed at. This is the reason it says that, so nothing here was copied from the neighbouring row.

Nova's figures were read a day after everything else in that file, so rows can now carry their own measuredAt, shown beside the provenance tag, and the footer no longer claims one date for figures gathered on two.

Two pre-existing problems found while editing bench 295

The methodology described a method the harness no longer uses. It said the bench reads "pairs 300 and 313, the x500 cohort rather than the standard pairs which cap at 200x". It reads all four: gainsPairs is {BTC: [0, 300], ETH: [1, 313]} and the publisher picks whichever product can be opened at each leverage tab. Gains' own formula field said the same thing. Both corrected, and the bullet split in two because the parameters and the pair list are separate facts and one bullet over the length limit was the hint.

A factual ordering claim that Nova falsifies. The copy said "Papertrade leaves the most room and Gains the least". With Nova on the board that is false — Nova is the floor, on the same thresholds and a higher fee. Fixed, along with "four venues" in six other places. A count in prose is exactly what goes stale when a cohort grows.

Cache keys: all three this time

Bench 295 declares dimensions (kind and bucket), so bench-filters matters here where it did not for 041 and 116, and a provider missing from it is invisible on every tab. v94 / v87 / v36.

My own cache-key test from #3150 did not know that third key existed. It does now: defined once, environment-scoped, and never pinned to a per-bench version that has not happened. The conditional part of the rule cannot be checked statically, so the test says so in words instead of pretending otherwise.

Tests

In the harness: the two deployments must agree on leverage and disagree on fee at the standard pair, the venues and diamonds cannot collide, and the pair indices are checked against each venue's own registry rather than trusted to stay aligned — a wrong index would publish another asset's terms under this venue's name, plausibly, since pair 7 also answers with a real leverage and a real threshold. Both live tests behind OCB_LIVE=1, both run: 493 pairs each, indices verified.

In the model: identical bounds and maintenance curve, the standard pair as the only difference with Nova the dearer, Nova's spread zero and not Gains', the degen rows identical as their label claims, Nova leaving less room at every shared leverage, and every gTrade row marked chain.

Each verified against a mutation, including the copied spread and the dropped date.

Gate: validate 253 specs, typecheck, lint at its 9 pre-existing warnings, test 711 pass, go build / go vet / go test green.

After merge

perp-cohort-stats needs redeploying for the Nova rows on 295 to carry numbers. The perp-cost tool is static data and is live with the build.

… copied

I said Nova could not go on bench 295 or the perp-cost tool because it
lists neither BTC nor ETH. That was wrong, and wrong for a reason worth
recording: I had read app.nova.markets/api/markets, which is the
Hyperliquid-routed FRONTEND list. The gTrade deployment is a different
thing and its pair 0 is BTC/USD, pair 1 ETH/USD.

All five selectors the leverage source uses answer on Nova's diamond, so
the reader takes the venue and the diamond as arguments instead of
hardcoding Gains', the same shape as the volume source. Measured on
chain 2026-10-22, both deployments, both markets:

  standard pair   1.1x-200x, threshold 90/75 between 25x and 60x
                  gains 0.035% a leg   nova 0.050% a leg
  x500 degen      500x fixed, threshold 80% flat
                  gains 0.020% a leg   nova 0.020% a leg

So it is one contract priced twice: identical thresholds and bounds, a
dearer standard pair. The fee sits inside the liquidation trigger, so
Nova leaves strictly less room at every leverage (10x: 8.90% against
8.93%; 200x: 0.275% against 0.305%).

THE NEAR MISS. perp-cost-model carries a spreadBps per row, and Gains'
is 1.0. Copying it would have looked right beside the identical
thresholds and been wrong: word 7 of pairs() is the pair spread at 1e10
and reads 100000000 on Gains but 0 on Nova. Nova is 5.0 bps a leg, not
5.5, and the earlier "43% dearer" I quoted from the taker fee alone is
really 25% once the spread is counted on both sides. The file's own
header says two venues were left out rather than guessed at; this is why.

Nova's figures were read a day after everything else in that file, so
rows can now carry their own measuredAt and the footer no longer claims
one date for figures gathered on two.

Found while editing bench 295, both pre-existing:

- Its methodology and Gains' formula said the bench reads "pairs 300 and
  313, the x500 cohort rather than the standard pairs". It reads all
  four: gainsPairs is {BTC: [0, 300], ETH: [1, 313]} and the publisher
  picks per leverage tab. Corrected, and split in two because the
  parameters and the pair list are separate facts.
- The prose said "four venues" in six places and, worse, claimed
  "Papertrade leaves the most room and Gains the least". With Nova that
  ordering is false. Both fixed.

Cache keys: all three move this time. Bench 295 declares dimensions, so
bench-filters matters here where it did not for 041 and 116, and a
provider missing from it is invisible on every tab. The cache-key test
did not know that key existed; it does now.

Tests: the two deployments agree on leverage and disagree on fee, the
degen rows are identical as their label claims, Nova's spread is zero and
not Gains', Nova leaves less room at every shared leverage, and the pair
INDICES are checked against each venue's own registry rather than trusted
to stay aligned, because a wrong index would publish another asset's
terms plausibly. Each verified against a mutation, including the copied
spread.
Review of my own previous commit. fetchGains became fetchGtrade but its
helpers kept the Gains name, so the code read s.gainsCall(novaDiamond,
...) and gtradePairs was still gainsPairs while being the pair map for
both deployments. Half a rename misleads more than either option.

24 references, compiler-verified. gainsArbRPC and gainsDiamondArb keep
their names because they really are about Gains: one is the RPC that
harness has always used, the other is Gains' own diamond address.
Audit of my own change, three things on the perp-cost page.

"Nine of the ten products below charge a share of your notional" is now
eleven of twelve.

The meta description said "ten perp venues". The page listed ten
PRODUCTS across nine venues, so it was already wrong, and Nova makes it
accidentally right at ten venues and twelve products. A count that is
only correct by coincidence is worse than no count, so it names no
number now.

And the footer gives every venue a paragraph saying how its figures were
read, which left Nova's two rows marked "chain" with no account of what
that meant. Nova now has its own, including the thing worth writing
down: word 7 of pairs() is the pair spread at 1e10 and reads zero on
Nova where Gains reads 0.0100%, so the row carries zero rather than the
neighbouring value. Copying it would have put Nova at 5.5 bps a leg
instead of 5.0 and looked right beside the identical thresholds.

The existing Gains paragraph already documented that 0.0100% pair
spread, which is independent confirmation that word 7 is the field I
read rather than my own inference about it.
@Flotapponnier
Flotapponnier merged commit 935efc0 into dev Oct 22, 2026
2 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.

1 participant