100: Add Perps collateral, leverage, value, uPnL and gross-exposure totals in EMC

This commit is contained in:
2026-10-24 17:56:20 +02:00
parent 7e36b7ef95
commit 28640f3203
35 changed files with 9110 additions and 180 deletions
+80 -8
View File
@@ -1,6 +1,6 @@
# Phoenix Perps read-only integration
`PhoenixPerpsService` and `PhoenixPerpsPosition` live in
`PhoenixPerpsService`, `PhoenixPerpsAccount` and `PhoenixPerpsPosition` live in
`com.r35157.libs.phoenix.perps`; the REST implementation is in `impl.ref`.
They contain no Evelyn/UI types or signing/trading operations. NenjimHub creates
one adapter without a portfolio cache as `phoenix.perps.default` and injects it into both Evelyn
@@ -17,11 +17,13 @@ The implementation uses these official endpoints at `https://perp-api.phoenix.tr
returns the wallet authority's version-1 snapshot and subaccounts. The
[account model](https://docs.phoenix.trade/sdk/accounts) currently uses trader PDA
index 0 for user accounts. Subaccount 0 is cross-margin; other returned subaccounts
are isolated. Read every returned subaccount and include only nonzero
`basePositionLots`. An omitted `positions` field is documented as optional and
are isolated. `getAccounts()` returns actual balances separately from positions;
`getOpenPositions()` remains a convenience flattening of those accounts. Keep funded
empty Cross accounts in the overview and include only nonzero `basePositionLots`
as position rows. An omitted `positions` field is documented as optional and
means empty; null or a wrong type is rejected.
2. [`GET /v1/view/exchange/markets`](https://docs.phoenix.trade/api/exchange/list-markets)
supplies each symbol's `baseLotsDecimals`.
supplies each symbol's `baseLotsDecimals`, `tickSize`, leverage tiers and taker fee.
3. [`GET /v1/market/{symbol}/mark-price`](https://docs.phoenix.trade/api/exchange/get-mark-price)
supplies `markPrice.price` in USD. The API permits an absent mark price; a held
position without a valid positive price fails the lookup rather than appearing as zero.
@@ -55,11 +57,14 @@ without a rounding context. Jackson parses JSON price numbers directly as decima
without a `double` intermediate. For example, 2,144 SOL lots at `baseLotsDecimals=2`
are 21.44 SOL; at a mark price of 104.45 USD, notional is 2,239.408 USD. Entry price,
collateral, virtual quote lots and margin are not substitutes for current notional.
Rounding to cents occurs only in the existing EMC display formatter.
Rounding to cents occurs only in the EMC display formatter. Margin and estimated
exit fees additionally follow the venue rounding described below.
Within each wallet's Phoenix venue, identity is `traderPdaIndex:subaccountIndex:symbol`.
Sequence numbers do not affect identity. Rows sort by PDA index, subaccount index
and symbol; labels use `SYMBOL-PERP`, with no extra subaccount tree level. Positive
and symbol; labels use `SYMBOL-PERP`. Isolated rows remain directly beneath the venue.
Cross positions belong beneath their explicit `Cross` account group, which supersedes
the former no-subaccount-level restriction from issue #99. Positive
quantity means Long and negative means Short. Thus two SOL subaccounts remain two
rows even though their labels match.
@@ -74,12 +79,79 @@ Each Evelyn process performs an initial background refresh, then waits 60 second
following each completed cycle. Jupiter and Phoenix are attempted independently for
each configured wallet. Successful results replace only that wallet/venue; failures
log wallet and venue and retain its last successful result. A valid empty result
clears only that venue's positions, preserving its group. Snapshot reads never fetch
replaces that venue's positions and account balances. EMC hides the venue heading
when it has neither position rows nor Cross accounts; its read status remains in
the wallet tooltip and the scope still contributes to snapshot totals.
Funded Cross accounts remain visible when they no longer hold positions. Snapshot reads never fetch
external data; EMC's three-second polling remains unchanged.
Positions and prices are separate REST reads and can belong to different slots.
This is a periodic current-notional estimate, not an atomic exchange valuation.
A cycle's duration includes all sequential reads and request pacing before the
60-second pause (32 active markets need at least about 33 seconds for their reads). Failed
reads can leave old data visible indefinitely; there is no stale/error indicator yet.
reads can leave old data visible indefinitely. A visible venue shows `(retained)` after
a failed refresh; cell tooltips include the last-success time. Childless venues are hidden,
with failed or pending first-read status available in their wallet's tooltip. Such unknown
scopes cannot contribute fabricated zero totals.
Trader PDA indices other than 0, streaming and position management are out of scope.
## Valuations, funding and margin
Monetary arithmetic uses exact `BigDecimal`/`BigInteger` values. Quote lots are
1e-6 USD. The [official SDK computation](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/margin/compute.ts)
and [PnL definition](https://docs.phoenix.trade/phoenix/margin-and-risk/entry-price-pnl)
separate price PnL from funding and fees:
```text
price uPnL = quantity × markPrice + virtualQuotePositionLots / 1e6
funding = unsettledFundingQuoteLots / 1e6 (signed; positive adds equity)
equity = collateral + sum(price uPnL + funding)
exit fee = ceil_to_micro_USD(abs(quantity) × markPrice × takerFee × REST takerFeeOverrideMultiplier)
Value = equity - sum(estimated exit fees)
```
The REST override is already a decimal multiplier (observed 0.9), not the raw
on-chain i8 representation. Missing override, fee, funding or virtual quote data
makes affected valuations unavailable, rather than defaulting to zero. Settled
trading fees and `accumulatedFundingQuoteLots` are not subtracted again; collateral
already reflects settlement. Equity is undiscounted: no positive-uPnL risk discount
or spot-collateral haircut is substituted for owned capital.
`collateral` is quote collateral in micro-USD. Each nonzero `spotCollaterals.balance`
currently makes collateral/equity/Value unavailable: this version does not have a
verified spot index-price valuation. It never quietly ignores SOL or another
collateral asset. Zero spot balances do not change quote-only valuations. Missing
collateral is also unavailable. Independent price PnL/exposure remain computable.
For a Cross position the Collateral column is its **initial margin requirement**,
not a share of the account balance. The implementation follows the
[SDK margin math](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/margin/math.ts)
and [integer price conversion](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/units.ts):
```text
markTicks = floor(markPrice × 10^(6-baseLotsDecimals) / tickSize)
grossQuoteLots = abs(basePositionLots) × markTicks × tickSize
L = first-tier leverage, or truncated linear interpolation of leverage between
the surrounding maxSizeBaseLots bounds; beyond the final bound use 1
initialMarginUsd = ceil(grossQuoteLots / L) / 1e6
```
Interpolation truncates the complete positive leverage, including when leverage
is decreasing. For 150 lots between (100 lots, 10×) and (200 lots, 5×), L is 7,
not 7.5 or 8. At 100 USD and exponent 2, margin is 21.428572 USD. Exposure retains
the exact original REST mark, preserving issue #99's current-notional meaning.
Cross account leverage is gross exposure / positive undiscounted equity before
estimated exit fees. Cross position leverage and ROI are unavailable because the
venue's displayed conventions have not been verified; the UI explains this and
shows dollar PnL. Isolated leverage/ROI use actual collateral. Cross position
Value is inapplicable; Cross account Value counts all shared capital once,
including free collateral when its last position closes. Other aggregate ratios
are blank. No account margin-utilization percentage is claimed; orders/risk
adjustments are not included in such a metric.
Value is an estimate at separately read reference prices, not an executable quote.
Slippage, price impact, swaps, network fees, account rent, future funding and
unrelated wallet balances are excluded. No profit cap is applied to Phoenix.
Unsupported/missing values and any higher totals requiring them show `—`.