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
+62 -28
View File
@@ -435,38 +435,72 @@ Price Index beregnes og vises, mens de øvrige fem felter fortsat er nul. Legacy
## 12. Perps-service og Mission Control
`EvelynPerpsService` udleverer et immutable `PerpsSnapshot` gennem det parameterløse
`getPositionsSnapshot()`. Læsning foretager ingen integrationskald. Hub'en starter separate processer
`evelyn.perps.prod` og `evelyn.perps.test`, injicerer de tilsvarende eksisterende `JupiterPerpsService`-views
og en fælles `PhoenixPerpsService` uden positionscache registreret som `phoenix.perps.default`.
`PerpsWalletConfiguration` indeholder en `SolanaWalletId`-adresse, der også bruges som række-ID og label.
Hub'en ejer de hardcodede lister: seks PROD-adresser og én TEST-adresse fra issue #98.
Begge integrationer læser de samme wallets i det pågældende miljø.
`getPositionsSnapshot()`. Læsning foretager ingen integrationskald. Hub'en starter
separate processer `evelyn.perps.prod` og `evelyn.perps.test`, hver med sin eksisterende
wallet-liste: seks PROD-adresser og én TEST-adresse fra issue #98. Begge får en fælles
read-only `JupiterPerpsOverview` og en fælles `PhoenixPerpsService`; sidstnævnte er
registreret som `phoenix.perps.default`. De eksisterende Jupiter-services til handel
og alarmer beholder deres afhængigheder og cache.
Servicen henter åbne Jupiter- og Phoenix-positioner asynkront ved start og venter 60 sekunder efter hvert
fuldført gennemløb, så runder ikke overlapper. Den eksisterende Solana-cache med 60 sekunders TTL bevares
for Jupiter. Phoenix læses gennem REST med 10 sekunders timeout pr. request, mindst ét sekund mellem
request-start på tværs af miljøer og fælles cooldown ved HTTP 429. Data er periodiske snapshots.
Servicen læser asynkront ved start og venter 60 sekunder efter hvert fuldført gennemløb.
Runder overlapper ikke og afhænger ikke af EMC's synlighed. Jupiter-overblikket bruger
ét REST-opslag pr. wallet. Phoenix bruger trader-state, metadata og én mark price pr.
unikt åbent marked. Begge har 10 sekunders request-timeout; Phoenix bevarer fælles
pacing og 429-cooldown. En fuld opdatering tager opslagstid plus pausen, ikke nødvendigvis
ét minut. Implementeringens `close()` afbryder worker'en; consumer-API'et har kun læsning.
Jupiters positionskonto er stabilt ID; retning og `positionSize()` vises, kendte mints bliver
SOL-PERP/BTC-PERP/ETH-PERP, og ukendte mints vises som adressen. Positioner sorteres efter konto.
Phoenix bruger authority-wallet og trader PDA index 0 med alle returnerede cross/isolated-subaccounts.
Kun nonzero-positioner medtages. ID og sortering bygger på PDA-index, subaccount-index og markedssymbol;
sekvensnumre indgår ikke. Retningen kommer fra signed quantity, label er fx SOL-PERP, og USD-størrelsen er
absolut base-mængde gange mark price, beregnet med decimalpræcision. Se [Phoenix-integrationen](PhoenixPerps.md)
for dokumenterede enheder, endpoints og begrænsninger.
Snapshot'et adskiller isolerede positioner fra Cross-konti med egne balancer. Pengebeløb
er præcise USD-decimaler, og null betyder utilgængelig. Før første succes indeholder
hver konfigureret wallet/venue en ukendt scope, ingen syntetiske positioner. Succes
publiceres pr. wallet/venue; en fejl beholder den kombinations sidste observation og
oprindelige succes-tidspunkt. Andre wallets/venues forsøges stadig. Et gyldigt tomt svar
rydder den pågældende scope og kan totalisere nul; HTTP-/formatfejl kan ikke. En gyldig
Cross-konto med kapital bevares, selv om dens sidste position lukkes.
Før første vellykkede opslag er der ingen positioner. Succes publiceres i et nyt snapshot pr. wallet/venue;
fejl logges og bevarer netop den kombinations sidste data. De øvrige venues og wallets forsøges stadig.
Et vellykket tomt svar fjerner kun den pågældende venues positioner og bevarer gruppen.
Fejl i HTTP, authentication, svarformat eller konvertering er ikke tomme porteføljer.
Der vises ingen indikator for forældede data. Implementationens `close()` afbryder worker'en;
consumer-API'et indeholder kun læsning.
EMC viser **Position | Direction | Margin | Collateral | Leverage | Exposure | Value | uPnL**
under den synlige rod **Perpetual Contracts**. Isolerede positioner har wallet → venue →
position; Phoenix Cross har wallet → venue → Cross → position. Positionens konto-ID
bestemmer identitet og tilhørsforhold, aldrig instrumentteksten. Wallets følger den
hidtidige konfiguration med Jupiter før Phoenix. En venue uden positioner eller Cross-konti
skjules og vises igen, når der kommer indhold. Skjulte venues indgår fortsat i snapshot
og totaler; deres læsestatus findes på wallet-rækkens tooltip. En Cross-konto med kapital
holder derfor Phoenix synlig, selv uden åbne positioner. Eksisterende foldestatus, markering og
rækkeobjekter bevares; uændrede snapshots opdaterer ikke træet. Vinduesstart er 1100 × 600,
og smallere vinduer kan scrolle vandret, når kolonnernes minimumsbredder kræver det.
EMC spørger hver miljøservice straks ved åbning og hvert tredje sekund uden at blokere JavaFX-tråden.
Skjul/luk stopper kun EMC-polling; services fortsætter. Træet er wallet → venue → position med Jupiter før
Phoenix og uden et ekstra subaccount-niveau. Tre kolonner, farver, formatering, foldestatus og markering
bevares for eksisterende rækker. Nye/lukkede positioner afstemmes efter ID.
Der er ingen sample-data, Refresh-knap, handel, signering eller kontoaktivering i denne læsevej.
Exposure er brutto: 100 USD Long + 100 USD Short giver 200 USD. Dollar-uPnL summeres
med fortegn. Collateral/Value tæller hver isoleret position og hver delt Cross-konto én
gang, inklusive ubrugt kapital på de inkluderede Cross-konti. Cross-positionernes
marginkrav lægges ikke til faktisk kontokapital. Mangler en nødvendig del, er netop
feltets total utilgængelig; en delsum præsenteres aldrig som total. Ingen summering eller
gennemsnitsberegning af leverage eller PnL-procenter. Totalerne afhænger ikke af foldning.
Isoleret leverage er Exposure / positiv Collateral; uPnL-procent er 100 × pris-uPnL /
positiv Collateral. På Cross-positioner er Collateral et initialt marginkrav i USD,
ikke særskilt ejet kapital. Cross-gruppens leverage er bruttoeksponering / udiskonteret
equity før estimerede exitgebyrer. Cross-positionernes leverage og ROI-procent er
utilgængelige, fordi Phoenix-konventionerne ikke er verificeret; dollar-uPnL vises.
Cross-positioners Value er ikke entydigt defineret og vises som —. Kontoudnyttelse er
ikke implementeret.
Value er et estimat ved referencepriser, ikke et eksekverbart tilbud. Phoenix medregner
collateral, pris-uPnL og signed uafregnet funding og fratrækker estimerede takergebyrer
med kontoens fee override. Slippage, price impact, swaps, netværksgebyrer og kontorent
indgår ikke. Ukendt spot-collateral gør de berørte værdiansættelser utilgængelige.
**Jupiter Value er foreløbig utilgængelig**, fordi API'ets netværdi trækker åbningsgebyrer
fra igen, og collateral-/profitloft-semantikken ikke er fuldt verificeret. Se
[Jupiter-overblikket](JupiterPerpsOverview.md) og [Phoenix-integrationen](PhoenixPerps.md)
for datakilder, enheder og beregninger.
Beløb har tusindtalsseparator og to decimaler, leverage × og pris-uPnL et synligt fortegn.
Positiv uPnL er grøn, negativ rød, afrundet nul neutral uden negativt nul. De svage
Long/Short-baggrunde bevares; grupper har tydeligere skrift og ingen retningsfarve.
Tooltips forklarer definitioner, manglende værdier og sidste succesfulde læsetidspunkt.
Efter fejl markeres en synlig venue som retained; læsestatus for skjulte venues findes
på wallet-rækkens tooltip. Gamle tal beskrives ikke som nye.
EMC spørger hver miljøservice straks ved åbning og hvert tredje sekund uden at blokere
JavaFX-tråden. Skjul/luk stopper kun EMC-polling. Der er ingen sample-data, Refresh-knap,
handel, signering, kontoaktivering eller automatisk genstart af Hub'en i denne læsevej.
Den kanoniske systembeskrivelse findes i
[Evelyn: services og status](../../r35157_docs/projects/evelyn/README.md#evelynperpsservice).
+59
View File
@@ -0,0 +1,59 @@
# Jupiter read-only Perps overview
`JupiterPerpsOverview` is an overview-only interface under
`com.r35157.libs.jupiter.perps`; `impl.ref.RestJupiterPerpsOverview` reads
`GET https://perps-api.jup.ag/v2/positions?walletAddress=<address>` once per wallet.
The Hub injects this reader into both Evelyn Perps processes with their existing
separate wallet lists. It has a 10-second request timeout, no immediate retries
and no portfolio cache. Signing, transactions, alarms, their existing Solana cache
and entry-price behavior remain in the existing `JupiterPerpsService`.
## Response contract and definitions
The official [CLI response type](https://github.com/jup-ag/cli/blob/main/src/clients/PerpsClient.ts)
and [API reference](https://perps-api.jup.ag/v1/docs) describe the position list.
The implementation requires an integer `count` matching the complete `dataList`,
unique valid position accounts, valid asset mints, Long/Short side and positive
size. HTTP/schema failures throw; only an explicit count-zero empty list clears
the scope. Missing optional valuation fields become unavailable; malformed
amounts fail the read.
| Display | Source / definition |
| --- | --- |
| Exposure | `sizeUsd / 10^6`; preserves the existing recorded USD-size basis |
| Collateral | `collateralUsd / 10^6`, the venue's recorded post-fee USD collateral |
| Leverage | Exposure / positive Collateral; not equity-based leverage |
| uPnL | `pnlBeforeFeesUsd / 10^6`, price PnL before separately reported fees |
| uPnL percent | 100 × uPnL / positive Collateral |
| Value | Unavailable; API net-value semantics have not passed verification |
All amounts are parsed as decimal integers and converted exactly. The API's
`pnlBeforeFeesPct` is already percentage points (95.48 means 95.48%), but the
overview derives the requested percentage against precise collateral itself.
The API's rounded `leverage` is likewise not used for the derived ratio.
## Why API Value is not displayed
On 24 October 2026 the funded configured wallet's six API sizes and collateral
amounts matched read-only on-chain position data. For its SOL Short, the response
contained these integer micro-USD values:
```text
collateralUsd = 18559 pnlBeforeFeesUsd = 17721
openFeesUsd = 361 closeFeesUsd = 361
borrowFeesUsd = 7 valueUsd = 35551
```
`35551 = 18559 + 17721 - 361 - 361 - 7`. The on-chain collateral was also
18559; the official [position account documentation](https://developers.jup.ag/docs/perps/position-account)
defines collateral as after fees. Blindly displaying this API Value would
subtract the settled opening fee again. Adding it back alone does not establish
the full contract: collateral-currency valuation and the `lockedAmount` profit
cap must also be verified. The response does not expose that cap. The overview
therefore shows an em dash with an explanation, and higher Value totals containing
such a position also remain unavailable. It does not merge unmatched on-chain
observations into the API snapshot or build unsigned closing transactions.
This is a known first-version limitation, not verified closing-value parity with
Jupiter. Counts, amounts and prices above are observations, not fixed expectations.
See the change's `verification.md` for observation times and current checks.
+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 `—`.