158 lines
9.7 KiB
Markdown
158 lines
9.7 KiB
Markdown
# Phoenix Perps read-only integration
|
||
|
||
`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
|
||
Perps processes. Each process owns its own wallet list, refresh worker and snapshot.
|
||
See [Evelyn](Evelyn.md#12-perps-service-og-mission-control) for consumer behavior and
|
||
[the canonical system overview](../../r35157_docs/projects/evelyn/README.md#evelynperpsservice)
|
||
for configuration and deployment status.
|
||
|
||
## REST reads and account model
|
||
|
||
The implementation uses these official endpoints at `https://perp-api.phoenix.trade`:
|
||
|
||
1. [`GET /v1/trader/state/{authority}?traderPdaIndex=0`](https://docs.phoenix.trade/api/trader/get-trader-state)
|
||
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. `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`, `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.
|
||
|
||
An empty trader state needs only the first request. Otherwise metadata is fetched once
|
||
and each active symbol's price once per wallet lookup, including when that symbol is
|
||
held in several subaccounts. Request starts are spaced at least one second apart across both environments to avoid
|
||
bursts of per-market reads. Each HTTP request has a 10-second timeout and uses the
|
||
Hub's existing HTTP client (no redirects by default). On HTTP 429 the adapter respects
|
||
`Retry-After` (seconds or HTTP date; 60 seconds if missing/invalid). Further calls
|
||
fail fast during that shared cooldown, letting Jupiter and other wallet attempts
|
||
continue; the normal refresh cycle retries later. There are no automatic
|
||
retries, new API keys or authentication/signing flows. Public reads worked during
|
||
verification, despite generated API security annotations referring to bearer auth.
|
||
HTTP failures, including 401/403/404/429, are errors rather than empty portfolios.
|
||
The shared pacing follows Phoenix's [rate-limit guidance](https://docs.phoenix.trade/sdk/ws-api-best-practices);
|
||
it does not guarantee a fixed quota.
|
||
|
||
## Units, precision and identity
|
||
|
||
The official [SDK's `baseLotsToBaseUnits`](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/units.ts)
|
||
uses `lots / 10 ** baseLotsDecimals`; the exponent may be negative. The adapter uses:
|
||
|
||
```text
|
||
quantity = BigDecimal(basePositionLots).scaleByPowerOfTen(-baseLotsDecimals)
|
||
sizeUsd = abs(quantity) * markPrice.price
|
||
```
|
||
|
||
All lot counts use `BigInteger`; conversion and multiplication use `BigDecimal`
|
||
without a rounding context. Jackson parses JSON price numbers directly as decimal,
|
||
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 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`. 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.
|
||
|
||
Authority/PDA identity, snapshot version, arrays, symbols, duplicate subaccounts or
|
||
positions, integer lots, metadata and prices are validated. A malformed lookup fails
|
||
as a whole; it never publishes a partially decoded portfolio. The shared Jackson
|
||
mapper is copied so strict parsing does not change other integrations.
|
||
|
||
## Refresh behavior and limitations
|
||
|
||
Each Evelyn process performs an initial background refresh, then waits 60 seconds
|
||
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
|
||
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. 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 `—`.
|