Skip to content

Price the fresh-open liquidation preview against full cross-margin capital - #2492

Open
0x-SquidSol wants to merge 1 commit into
dcccrypto:playgroundfrom
0x-SquidSol:fix/order-ticket-fresh-open-liq-capital
Open

Price the fresh-open liquidation preview against full cross-margin capital#2492
0x-SquidSol wants to merge 1 commit into
dcccrypto:playgroundfrom
0x-SquidSol:fix/order-ticket-fresh-open-liq-capital

Conversation

@0x-SquidSol

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

Copy link
Copy Markdown
Contributor

What

Price the order ticket's fresh-open liquidation preview against the full
cross-margined account capital, matching the scale-in branch and PositionsDock.

Why

OrderTicket priced a FRESH open (existingPositionSize === 0) with
computePreTradeLiqPrice against only the order's marginNative, while every other
liq surface (the scale-in branch, PositionsDock, useLiqPrice) uses full
capital. Percolator v17 is cross-margined (withdraw is blocked while any leg is
open), so the whole account backs the position. The margin-only preview therefore
understated liquidation distance — the confirm modal showed a scarier (closer) liq
price than reality, which then jumped once the position opened and recomputed on
full capital (e.g. a preview liq of ~$81 for a position that is actually
unliquidatable at full capital).

Changes

  • OrderTicket — drop the fresh-open special case. When there's no existing
    position, combinedSignedSize / combinedEntryPriceE6 already equal this order's
    own signed size and estimated entry, so the existing capital-based
    computeLiqPrice branch is correct for both fresh-open and scale-in. Removed the
    now-unused computePreTradeLiqPrice import.
  • regression test — real SDK math showing the margin-only preview disagrees with
    the capital-based value (margin-only shows a real liq for a position that is
    unliquidatable / safer at full capital), and that when order margin == capital the
    two agree (no false positive).

Testing

  • npx tsc --noEmit — clean.
  • New PoC + liquidation-distance suite — 13 tests, all pass.

Notes

  • Frontend + devnet scope; no program/keeper/mainnet changes.
  • Scale-in and before-open behavior is unchanged; only the fresh-open preview is
    corrected so it matches what the opened position displays (no more jump). The old
    preview erred conservative (scarier); this shows the true (farther / unliquidatable)
    liq.

Summary by CodeRabbit

  • Bug Fixes

    • Improved liquidation price previews for fresh orders and existing-position adjustments.
    • Previews now consistently account for full account capital and combined positions, providing more accurate results.
  • Tests

    • Added regression coverage to verify liquidation pricing across margin-only and full-capital scenarios.

…n capital

OrderTicket's liquidation preview priced a FRESH open (no existing position) with
computePreTradeLiqPrice against only the order's margin, while the scale-in branch
(and PositionsDock / useLiqPrice) use full account capital. Percolator v17 is
cross-margined — the whole account backs the position — so the margin-only preview
understated liquidation distance: the confirm modal showed a scarier (closer) liq
price than the real on-chain one, which then visibly jumped once the position
opened and recomputed on full capital (e.g. a preview liq of ~$81 for a position
that is actually unliquidatable).

When there is no existing position, combinedSignedSize / combinedEntryPriceE6
already reduce to this order's own signed size and estimated entry, so the existing
capital-based computeLiqPrice path is correct for both fresh-open and scale-in.
Drop the fresh-open special case (and the now-unused computePreTradeLiqPrice
import) so the preview matches what the position shows once open.

- OrderTicket: single capital-based afterLiqPrice branch for fresh-open + scale-in
- add a PoC/regression using the real SDK math (margin-only preview shows a scary
  liq for a position that is unliquidatable / safer at full capital)

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 18:16
@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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f9125ebd-1614-41b3-9ca9-18beeb0eb805

📥 Commits

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

📒 Files selected for processing (2)
  • app/__tests__/components/order-ticket-fresh-open-liq.test.ts
  • app/components/trade/OrderTicket.tsx

📝 Walkthrough

Walkthrough

The order ticket now uses full account capital and combined position data for after-liquidation-price calculations. A regression test covers fresh-open pricing and verifies the matching-capital control case.

Changes

Fresh-open liquidation pricing

Layer / File(s) Summary
Unified liquidation price calculation
app/components/trade/OrderTicket.tsx, app/__tests__/components/order-ticket-fresh-open-liq.test.ts
The order ticket uses computeLiqPrice with full account capital, combined signed position, and combined entry price. Vitest coverage checks fresh-open and matching-capital calculations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

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 and concisely describes the main change to use full cross-margin capital for fresh-open liquidation previews.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@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 6901d574. Fix is correct, and the load-bearing claim in the
comment holds exactly. Suite clean: 2921 passed / 0 failed.

The reduction claim checks out

The change collapses two branches into one on the strength of "when there is no
existing position, combinedSignedSize and combinedEntryPriceE6 already reduce
to this order's own signed size and estimated entry"
. If that were even slightly
off, the fresh-open preview would silently show the wrong number rather than
fail, so I worked it through with existingPositionSize === 0n:

  • combinedSignedSize = 0n + newSignedSizenewSignedSize
  • sameDirection short-circuits to true on existingPositionSize === 0n
  • combinedEntryPriceE6 = (existingEntryPriceE6 * 0n + estEntry * positionSize) / (0n + positionSize)
    estEntry ✓ (and positionSize > 0n is guaranteed by the
    combinedSignedSize !== 0n guard)

So the merged branch is exactly the old fresh-open inputs, priced against
capital instead of marginNative. Clean collapse, not an approximation.

One edge I chased and it doesn't bite

Switching the denominator from marginNative to capital made me check the
first-timer case: capital = userAccount ? userAccount.account.capital : 0n, and
computeLiqPrice(entry, 0n, size, mm) on a zero-capital account would be
meaningless — where the old marginNative was always non-zero.

Unreachable: needsDeposit = connected && userAccount && capital === 0n feeds
ticketLocked, and :1250 renders the deposit prompt instead of the order UI, so
no order can be previewed at capital === 0n. Recording it because "we changed
the denominator to something that can be zero" is the obvious thing to worry
about here, and the answer isn't visible from the diff.

The test doesn't bind the fix — eighth in the run

order-ticket-fresh-open-liq.test.ts calls computePreTradeLiqPrice and
computeLiqPrice directly and compares them. It never renders or imports
OrderTicket, so it asserts that the two SDK functions differ, which was true
before the PR and is still true after.

I restored the original branch verbatim:

const afterLiqPrice = hasOrder
  ? (existingPositionSize === 0n
      ? computePreTradeLiqPrice(oracleE6, marginNative, positionSize, maintenanceMarginBps, tradingFeeBps, direction)
      : ...)

2/2 still green with the bug fully back.

This one is cheap to bind without rendering the component, because the whole
change is a pure expression over already-computed values. Extracting it — say
computeAfterLiqPrice({ hasOrder, combinedSignedSize, combinedEntryPriceE6, capital, maintenanceMarginBps })
into lib/trading beside computeEstimatedEntryPrice — makes it directly
testable, and the fresh-open reduction above becomes an assertion rather than a
comment. That also puts it next to PositionsDock / useLiqPrice, which is where
the inconsistency came from in the first place: three surfaces computing liq three
ways is what produced both this bug and the H7-style drift on the stale-oracle
gate (#2484).

Worth noting the computePreTradeLiqPrice import is now dropped from
OrderTicket entirely — after this, is it used anywhere? If not, the SDK export
being unreferenced is a useful signal that the margin-only convention is gone
repo-wide rather than just here.

@dcccrypto

Copy link
Copy Markdown
Owner

Answering my own closing question so nobody re-checks it:

computePreTradeLiqPrice is re-exported by lib/trading.ts:5-15, so dropping
the OrderTicket import doesn't orphan the symbol — it stays on that module's
public surface.

But nothing calls it. After this PR the only references left in app/ are that
re-export and the two comments in OrderTicket describing the old behaviour. So
the margin-only convention is gone repo-wide, not just from this component —
which is the signal I was hoping for. Nothing to change; it just means the fix is
complete rather than local.

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