99: Add read-only Phoenix Perps positions to EvelynPerpsService
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# Phoenix Perps read-only integration
|
||||
|
||||
`PhoenixPerpsService` 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. Read every returned subaccount and include only nonzero
|
||||
`basePositionLots`. 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`.
|
||||
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 existing EMC display formatter.
|
||||
|
||||
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
|
||||
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
|
||||
clears only that venue's positions, preserving its group. 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.
|
||||
Trader PDA indices other than 0, streaming and position management are out of scope.
|
||||
Reference in New Issue
Block a user