101: EMC Perps - show Phoenix TWAP progress and time until next suborder

This commit is contained in:
2026-10-24 19:30:20 +02:00
parent 28640f3203
commit 2f30712004
21 changed files with 712 additions and 32 deletions
+58 -4
View File
@@ -1,6 +1,6 @@
# Phoenix Perps read-only integration
`PhoenixPerpsService`, `PhoenixPerpsAccount` and `PhoenixPerpsPosition` live in
`PhoenixPerpsService`, `PhoenixPerpsAccount`, `PhoenixPerpsPosition` and `PhoenixTwapOrder` 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
@@ -26,11 +26,15 @@ The implementation uses these official endpoints at `https://perp-api.phoenix.tr
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.
position remains visible with unknown price-dependent metrics when the mark is absent.
A present nonpositive or malformed price still fails the lookup.
4. [`GET /v1/trader/{authority}/twap?traderPdaIndex=0`](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/api/traders/client.ts)
supplies active TWAP snapshots when the wallet has held positions. Only filled orders
with remaining children on matching existing positions are returned; see [TWAP observations](#twap-observations).
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
held in several subaccounts, followed by one TWAP read. 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
@@ -88,13 +92,63 @@ 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
60-second pause (32 active markets need at least about 34 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.
## TWAP observations
TWAP data is matched by requested authority, PDA index, subaccount and market `assetId`
using Phoenix metadata. Only an existing nonzero position can carry a TWAP, and the
order itself must have `baseLotsFilled > 0` and remaining children. A new order without
fills stays invisible even if the market already has a position. No TWAP-only row or
group total is created. A successful snapshot without that active order clears its
TWAP data. A failed or invalid TWAP read fails the whole wallet/venue observation,
retaining the previous complete snapshot and its original timestamps as `(retained)`.
The [SDK snapshot](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/api/traders/twap.ts)
reports `nChildOrders` as the total execution count including dust. With explicit
`nDustOrders > 0`, regular count is total minus dust count. Legacy zero/omitted dust
count with a positive `dustOrderSize` replaces one final regular child with dust.
Total quantity is regular count × regular lot size plus dust count × dust lot size;
the next size switches to dust when the regular children have executed. Convert all
sizes and cumulative `baseLotsFilled` with `baseLotsDecimals`, using exact arithmetic.
If size/dust metadata is unavailable, sizes remain unknown while independent counters
and fills remain available. Execution progress is executed/total children; it does
not assert that the planned quantity has filled completely.
Time estimates use the current nominal **200 ms per Solana slot**, documented by the
[Solana Foundation in October 2026](https://solana.com/fr/upgrades/reduced-slot-times).
The older 400 ms assumption no longer applies. At receipt of the TWAP response:
```text
estimated next = observation time + (dueSlot - snapshot.slot) × 200 ms
final slot = dueSlot + (childOrdersRemaining - 1) × cooldownSlots
estimated end = observation time + (final slot - snapshot.slot) × 200 ms
```
Receipt time is an approximate anchor; API lag, variable slot times and delayed
execution can shift the estimates. No separate RPC sampling or live timer is used.
Already-due schedules yield unavailable estimates, as do overflow or expiry before
the scheduled child/completion. `lastValidSlot` is only an expiry boundary.
EMC appends **TWAP** after uPnL. Its progressbar overlays a snapshot duration with
unbounded total hours, such as `≈ 28:32:10` or `≈ 128:32:10`. It updates only with new
snapshots and is explicitly as of the tooltip's `Updated` time. Due/unavailable
timing shows `Due / unknown`; a failed venue shows `Retained` instead of a fresh
countdown. Other venues, positions without a relevant order and all groups have
empty TWAP cells. Tooltips show progress, `Estimated completion`, total/next/fill
quantities, different dust sizes where relevant, observation time and limitations.
Total and next-child USD values reuse that position's already fetched Phoenix mark,
independently of the Ticker. They are approximate current notionals, excluding fees,
not historical spend or a promise about future execution cost. An absent mark keeps
token quantities visible with unavailable USD values; affected exposure, margin,
PnL/equity/Value and dependent totals are unavailable rather than zero.
## Valuations, funding and margin