99: Add read-only Phoenix Perps positions to EvelynPerpsService
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-22
|
||||
@@ -0,0 +1,31 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
See proposal.md and issue #99. The existing worker handles Jupiter wallets and publishes immutable snapshots; EMC already supports multiple venues and stable row reconciliation. Phoenix trader state contains all subaccounts but represents positions in signed base lots rather than USD.
|
||||
|
||||
## Goals / Non-Goals
|
||||
Add only read-only REST access and venue-level failure isolation. Keep wallet lists, process names, UI, polling and Jupiter behavior. System documentation in r35157_docs is explicitly in scope. No SDK runtime, trading, streaming or generalized integration framework.
|
||||
|
||||
## Decisions
|
||||
- Add PhoenixPerpsService and PhoenixPerpsPosition in com.r35157.libs.phoenix.perps; implementation in impl.ref. Return subaccount identity, symbol, signed base quantity and current USD size as domain data, without referencing Evelyn or UI types.
|
||||
- GET trader/state/{authority}?traderPdaIndex=0, exchange/markets for baseLotsDecimals, and market/{symbol}/mark-price for active symbols. Empty portfolios need only trader state. Reuse each symbol's mark price within a lookup. Use the existing HTTP/Jackson stack with 10-second request timeout and no redirects/retries. All numeric conversion uses BigInteger/BigDecimal; JSON floating numbers must be parsed directly as decimal.
|
||||
- Formula verified against official Rise SDK ts/src/units.ts: baseLotsToBaseUnits(lots, decimals) = lots / 10**decimals. USD notional = abs(base quantity) * markPrice.price. This avoids additional Solana PDA derivation needed for trader-view notional. Negative decimal exponents are supported. No use of entryPriceUsd or virtualQuotePositionLots.
|
||||
- Validate authority, PDA index, required arrays/numeric fields, duplicate subaccount/position identities and matching market symbols. Missing positions arrays are permitted only as the documented optional field; null/wrong types fail. A malformed nonempty portfolio fails atomically, not partially.
|
||||
- Hub supplies one shared read-only Phoenix adapter with no portfolio cache; each Evelyn instance owns independent last-successful state keyed by wallet and venue. Existing single scheduled worker reads Jupiter and Phoenix separately with independent catches; publish ordered Jupiter/Phoenix venues after each success. Interruption ends the cycle.
|
||||
- Stable Phoenix IDs are PDA:subaccount:symbol; sequence numbers are not identity. Labels add -PERP only if absent. No new tree level.
|
||||
|
||||
## Risks / Trade-offs
|
||||
- REST prices and position snapshots may come from different slots → periodic current estimates, not an atomic exchange valuation; document this limitation.
|
||||
- Public reads currently work without credentials although generated OpenAPI security annotations suggest auth → use observed public access; reject 401/403 and retain data, never attempt signing/login automatically.
|
||||
- Up to one mark request per active symbol per wallet → live verification exposed HTTP 429 during bursts. Space request starts at least one second apart in the shared adapter, and honor Retry-After (seconds/HTTP date, 60 seconds fallback). During cooldown fail fast without external calls, preserving venue failure isolation. No automatic retries or generic cache framework. This increases cycle duration before the existing one-minute delay.
|
||||
- Previous data may persist on failure → log wallet/venue and document the existing lack of stale indicators.
|
||||
|
||||
## Migration Plan
|
||||
No new configuration entries or dependencies. Build and restart through normal deployment after review. Do not start full Hub/trading during verification. Leave changes uncommitted and the OpenSpec change active.
|
||||
|
||||
## Sources
|
||||
- https://docs.phoenix.trade/api/trader/get-trader-state
|
||||
- https://docs.phoenix.trade/api/exchange/list-markets
|
||||
- https://docs.phoenix.trade/api/exchange/get-mark-price
|
||||
- https://docs.phoenix.trade/sdk/accounts
|
||||
- https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/units.ts
|
||||
@@ -0,0 +1,19 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
The owner has Phoenix cross-margin positions that are missing from EMC. Issue #99 extends the existing view-only Perps overview to Phoenix using the same wallet configuration and service boundaries.
|
||||
|
||||
## What Changes
|
||||
- Add a read-only Phoenix REST integration covering cross-margin and isolated subaccounts, with exact current USD notional conversion.
|
||||
- Inject it through Hub and retain successful snapshots independently per wallet and venue.
|
||||
- Keep the 60-second background refresh delay, 3-second EMC polling, and existing table presentation.
|
||||
- Update project and canonical r35157_docs documentation, including API limitations and live verification evidence.
|
||||
|
||||
## Capabilities
|
||||
### New Capabilities
|
||||
- `phoenix-perps-service`: Read-only position retrieval and precise USD mapping from Phoenix REST data.
|
||||
### Modified Capabilities
|
||||
- `evelyn-perps-service`: Multi-venue refresh, construction and failure isolation.
|
||||
|
||||
## Impact
|
||||
New Phoenix API/reference implementation, EvelynPerpsService implementation, Hub composition and documentation. No UI redesign, transaction signing, trading, new configuration framework or external library dependencies.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Wallet configuration belongs to service construction
|
||||
The Hub SHALL supply the six PROD wallet addresses and one TEST wallet address specified in issue #98 and the corresponding existing Jupiter integration plus a read-only Phoenix integration at construction. Each service SHALL retain immutable configuration and own its snapshot. Wallet addresses SHALL be used as stable identities and display labels. EMC SHALL neither own nor pass wallet configuration; reads SHALL remain parameterless.
|
||||
|
||||
#### Scenario: Separate configured instances
|
||||
- **WHEN** the Hub constructs and starts both instances
|
||||
- **THEN** each instance queries only its configured wallets through its injected integrations
|
||||
- **AND** the implementation does not define the wallet list internally
|
||||
|
||||
### Requirement: Background Jupiter refresh
|
||||
The service SHALL refresh immediately after startup and wait 60 seconds between completed cycles, without overlapping cycles or depending on EMC visibility. It SHALL use Jupiter and Phoenix read operations and respect the existing Jupiter cache. Snapshot reads SHALL make no integration calls. No transaction signing or position-management operation SHALL occur.
|
||||
|
||||
#### Scenario: Background update while EMC is hidden
|
||||
- **WHEN** EMC is hidden and the next refresh is due
|
||||
- **THEN** the service still refreshes configured wallets and publishes immutable snapshots
|
||||
- **AND** no freshness guarantee bypasses the existing 60-second integration cache
|
||||
|
||||
### Requirement: Position mapping is stable and complete
|
||||
The service SHALL map position account to stable ID, mint to known instrument label or raw mint fallback, direction to Long/Short, and positionSize to USD size. Wallet order SHALL follow configuration, and positions SHALL be ordered deterministically by position account. Phoenix SHALL use PDA index, subaccount index and symbol as stable identity, signed quantity for direction and absolute current USD notional for size. Jupiter and Phoenix SHALL appear as separate venues, with no subaccount tree level.
|
||||
|
||||
#### Scenario: Changed and unknown positions
|
||||
- **WHEN** a successful read returns newly opened positions including an unknown token mint
|
||||
- **THEN** all returned positions appear with stable IDs and exact USD sizes, and the unknown mint is displayed rather than dropped
|
||||
|
||||
### Requirement: Refresh failures are isolated per wallet
|
||||
A failed read or mapping SHALL retain the last successful data for that wallet and venue, log the failure and allow other wallets and venues to refresh. Successful empty results SHALL remove only that wallet/venue's previous positions. Interruption SHALL stop the current cycle. Failed first reads SHALL not invent positions.
|
||||
|
||||
#### Scenario: Failure followed by recovery
|
||||
- **WHEN** one wallet fails while another succeeds
|
||||
- **THEN** only the successful wallet/venue's data is replaced
|
||||
- **AND** a later successful empty read clears the recovered wallet/venue's previous positions
|
||||
|
||||
#### Scenario: One venue fails
|
||||
- **WHEN** Phoenix fails for a wallet while Jupiter succeeds, or vice versa
|
||||
- **THEN** only the successful venue is updated and the failed venue retains its previous data
|
||||
- **AND** subsequent wallets are still attempted
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
## Purpose
|
||||
Read actual Phoenix perpetual positions for a Solana authority without signing or modifying accounts, preserving precise current USD notional and subaccount identities.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Read every active position under the authority
|
||||
The integration SHALL read official Phoenix REST trader state for the requested wallet authority at PDA index 0. It SHALL include nonzero positions from cross-margin and isolated subaccounts, preserve subaccount/symbol identity independently of sequence numbers, and return an immutable deterministically ordered result. No signing, trading or account activation SHALL occur.
|
||||
|
||||
#### Scenario: Cross and isolated positions share a symbol
|
||||
- **WHEN** subaccounts 0 and 2 both have a nonzero SOL position
|
||||
- **THEN** both positions are returned with distinct identities
|
||||
- **AND** zero-sized positions are excluded
|
||||
|
||||
### Requirement: Current USD notional uses documented market units
|
||||
Position direction SHALL follow signed quantity. USD size SHALL be the absolute current notional, calculated using exact decimal arithmetic from base lots divided by 10 to the market's base-lot decimal exponent and multiplied by its current mark price. Collateral, entry value and raw lot counts SHALL NOT be used as USD size. Missing conversion data SHALL fail the read.
|
||||
|
||||
#### Scenario: Convert a short position
|
||||
- **WHEN** a position has -125 base lots, base-lot exponent 2 and mark price 104.45 USD
|
||||
- **THEN** it represents -1.25 base units, Short direction and 130.5625 USD notional
|
||||
|
||||
### Requirement: Read failures cannot masquerade as empty portfolios
|
||||
HTTP, authentication, malformed schema, identity mismatch and numeric conversion errors SHALL fail the read. Requests SHALL have bounded timeouts. Only a valid response with no nonzero positions SHALL produce an empty result. Required mark prices SHALL be positive and finite.
|
||||
|
||||
#### Scenario: Missing mark price or rejected access
|
||||
- **WHEN** a required price is missing or an HTTP request is rejected
|
||||
- **THEN** the read fails instead of returning an empty or partial portfolio
|
||||
|
||||
#### Scenario: Empty account
|
||||
- **WHEN** a valid trader snapshot has no subaccounts or no nonzero positions
|
||||
- **THEN** an empty result is returned without market-price requests
|
||||
|
||||
#### Scenario: Public API rate limit
|
||||
- **WHEN** the shared adapter issues requests for PROD and TEST
|
||||
- **THEN** request starts SHALL be spaced at least one second apart
|
||||
- **AND** HTTP 429 SHALL fail the lookup and establish a shared cooldown from Retry-After (seconds or HTTP date, 60 seconds fallback)
|
||||
- **AND** lookups during cooldown SHALL fail without external calls, with no immediate retry
|
||||
@@ -0,0 +1,14 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Phoenix integration
|
||||
- [x] 1.1 Implement read-only REST API/domain types and strict exact-decimal conversion; verify documented units, cross/isolated identities, empty results, malformed data, errors and timeouts in isolated execution and live nonempty responses.
|
||||
- [x] 1.2 Document REST sources, unit conversion and limitations in project documentation; check statements against official docs and observed responses.
|
||||
|
||||
## 2. Evelyn integration and system documentation
|
||||
- [x] 2.1 Inject Phoenix through Hub and isolate last-successful data per wallet/venue; verify failure combinations, empty clearing, unchanged Jupiter mapping, polling and cached snapshot reads in isolated execution.
|
||||
- [x] 2.2 Update r35157_docs and related project documentation; verify removal of obsolete Jupiter-only statements and relative links.
|
||||
|
||||
## 3. Integration verification
|
||||
- [x] 3.1 Run build/existing tests, strict OpenSpec validation, live/UI checks and final diff review; record evidence and leave all edits uncommitted.
|
||||
|
||||
Verification evidence: [verification.md](verification.md).
|
||||
@@ -0,0 +1,65 @@
|
||||
# Verification — 2026-10-22
|
||||
|
||||
## Build and specifications
|
||||
|
||||
- `JAVA_HOME=/usr/local/software/java/jfx-25 ./gradlew build`: successful; 42 existing tests, no failures/errors.
|
||||
- `openspec validate 99-phoenix-perps-positions --strict`: valid.
|
||||
- Relative file links in the changed project/system documentation resolve; whitespace checks pass.
|
||||
- No repository test suite was added, in accordance with OpenSpec project context. Focused temporary execution harnesses are under `/tmp/emc99`.
|
||||
|
||||
## Phoenix reader
|
||||
|
||||
Temporary local HTTP responses exercised exact quantities beyond double's integer precision,
|
||||
negative lot exponents, precise decimal prices, nonzero filtering, both cross and isolated
|
||||
subaccounts holding the same symbol, deterministic sorting, immutable results and sequence-independent identity.
|
||||
|
||||
Verified valid empty states (including omitted optional positions), rejection of malformed/null
|
||||
state, metadata and prices, wrong authority/PDA/version, duplicate identities, missing markets,
|
||||
conversion overflow, HTTP 401/403/404/429/500, header and body timeouts, and interruption.
|
||||
The shared Jackson mapper is unchanged. The harness accepts GET only. Pacing, fail-fast cooldown
|
||||
and Retry-After seconds/HTTP-date parsing (including invalid/overflow fallback) were exercised.
|
||||
|
||||
## Evelyn and EMC
|
||||
|
||||
Temporary readers exercised immediate asynchronous startup, defensive wallet configuration,
|
||||
1,000 cached snapshot reads during a blocked integration request, immutable snapshots,
|
||||
unchanged Jupiter mapping, Phoenix subaccount identity/direction and deterministic ordering.
|
||||
Independent combinations of Jupiter/Phoenix success/failure/empty for two wallets confirmed
|
||||
retention and clearing per wallet/venue, including continued attempts after errors and cycle
|
||||
termination on interruption. The single scheduled worker and 60-second fixed delay remain in place.
|
||||
The existing six PROD/one TEST wallet lists are unchanged and validate.
|
||||
|
||||
A standalone JavaFX Perps view exercised same-symbol subaccounts, value updates, added/removed
|
||||
positions and empty results while retaining row identity, selection and expansion. The existing
|
||||
three columns and direction colors rendered the captured live data correctly. Screenshot:
|
||||
`/tmp/emc99/view.png`. EMC code and its three-second snapshot polling are unchanged.
|
||||
|
||||
## Live reference
|
||||
|
||||
The adapter read all seven configured wallets through a local recording proxy forwarding
|
||||
GET requests to the official Phoenix API. Captured trader state, metadata and mark-price responses
|
||||
were independently checked with Python Decimal against every adapter result, including exact
|
||||
quantity, notional, authority/subaccount/symbol identity and count.
|
||||
|
||||
| Environment / wallet prefix | Positions | Long | Short |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| PROD H5Yrdm7B… | 32 | 30 | 2 |
|
||||
| PROD 7PttBqap… | 32 | 29 | 3 |
|
||||
| PROD vshTPUDa… | 31 | 10 | 21 |
|
||||
| PROD 8XGrkDQC… | 32 | 3 | 29 |
|
||||
| PROD 4wzxcYQ8… | 29 | 6 | 23 |
|
||||
| PROD HM13QYMv… | 14 | 7 | 7 |
|
||||
| TEST 6NyYEoXm… | 0 | 0 | 0 |
|
||||
|
||||
All 170 nonzero positions matched; H5's short positions were FET and TLT.
|
||||
These are observations, not fixed expectations. Isolated positions were verified with controlled
|
||||
responses, as the live positions were cross-margin.
|
||||
|
||||
An initial burst of price requests hit HTTP 429. The final shared one-second pacing completed
|
||||
the live check without errors. At this portfolio size the sequential reads take about three
|
||||
minutes before the existing 60-second pause; this is not a fresh full portfolio every 60 seconds.
|
||||
Positions and mark prices are not atomic snapshots. These limitations and retained old data
|
||||
on errors are documented in `docs/PhoenixPerps.md` and the canonical Evelyn system page.
|
||||
|
||||
No full Hub, trading/signing flow or deployment was started. Code and documentation are left
|
||||
uncommitted; this OpenSpec change remains active for the owner's review/archive.
|
||||
@@ -22,15 +22,15 @@ The service SHALL run as two separate Hub-managed processes registered as `evely
|
||||
- **AND** no placeholder positions are generated before successful retrieval
|
||||
|
||||
### Requirement: Wallet configuration belongs to service construction
|
||||
The Hub SHALL supply the six PROD wallet addresses and one TEST wallet address specified in issue #98 and the corresponding existing Jupiter integration at construction. Each service SHALL retain immutable configuration and own its snapshot. Wallet addresses SHALL be used as stable identities and display labels. EMC SHALL neither own nor pass wallet configuration; reads SHALL remain parameterless.
|
||||
The Hub SHALL supply the six PROD wallet addresses and one TEST wallet address specified in issue #98 and the corresponding existing Jupiter integration plus a read-only Phoenix integration at construction. Each service SHALL retain immutable configuration and own its snapshot. Wallet addresses SHALL be used as stable identities and display labels. EMC SHALL neither own nor pass wallet configuration; reads SHALL remain parameterless.
|
||||
|
||||
#### Scenario: Separate configured instances
|
||||
- **WHEN** the Hub constructs and starts both instances
|
||||
- **THEN** each instance queries only its configured wallets through its corresponding integration
|
||||
- **THEN** each instance queries only its configured wallets through its injected integrations
|
||||
- **AND** the implementation does not define the wallet list internally
|
||||
|
||||
### Requirement: Background Jupiter refresh
|
||||
The service SHALL refresh immediately after startup and wait 60 seconds between completed cycles, without overlapping cycles or depending on EMC visibility. It SHALL use only existing Jupiter position reads and respect their cache. Snapshot reads SHALL make no integration calls. No transaction signing or position-management operation SHALL occur.
|
||||
The service SHALL refresh immediately after startup and wait 60 seconds between completed cycles, without overlapping cycles or depending on EMC visibility. It SHALL use Jupiter and Phoenix read operations and respect the existing Jupiter cache. Snapshot reads SHALL make no integration calls. No transaction signing or position-management operation SHALL occur.
|
||||
|
||||
#### Scenario: Background update while EMC is hidden
|
||||
- **WHEN** EMC is hidden and the next refresh is due
|
||||
@@ -38,16 +38,21 @@ The service SHALL refresh immediately after startup and wait 60 seconds between
|
||||
- **AND** no freshness guarantee bypasses the existing 60-second integration cache
|
||||
|
||||
### Requirement: Position mapping is stable and complete
|
||||
The service SHALL map position account to stable ID, mint to known instrument label or raw mint fallback, direction to Long/Short, and positionSize to USD size. Wallet order SHALL follow configuration, and positions SHALL be ordered deterministically by position account. Only Jupiter venues and actual positions SHALL be published.
|
||||
The service SHALL map position account to stable ID, mint to known instrument label or raw mint fallback, direction to Long/Short, and positionSize to USD size. Wallet order SHALL follow configuration, and positions SHALL be ordered deterministically by position account. Phoenix SHALL use PDA index, subaccount index and symbol as stable identity, signed quantity for direction and absolute current USD notional for size. Jupiter and Phoenix SHALL appear as separate venues, with no subaccount tree level.
|
||||
|
||||
#### Scenario: Changed and unknown positions
|
||||
- **WHEN** a successful read returns newly opened positions including an unknown token mint
|
||||
- **THEN** all returned positions appear with stable IDs and exact USD sizes, and the unknown mint is displayed rather than dropped
|
||||
|
||||
### Requirement: Refresh failures are isolated per wallet
|
||||
A failed wallet read or mapping SHALL retain its last successful data, log the failure and allow other wallets to refresh. Successful empty results SHALL remove previous positions. Interruption SHALL stop the current cycle. Failed first reads SHALL not invent positions.
|
||||
A failed read or mapping SHALL retain the last successful data for that wallet and venue, log the failure and allow other wallets and venues to refresh. Successful empty results SHALL remove only that wallet/venue's previous positions. Interruption SHALL stop the current cycle. Failed first reads SHALL not invent positions.
|
||||
|
||||
#### Scenario: Failure followed by recovery
|
||||
- **WHEN** one wallet fails while another succeeds
|
||||
- **THEN** only the successful wallet's data is replaced
|
||||
- **AND** a later successful empty read clears the recovered wallet's previous positions
|
||||
- **THEN** only the successful wallet/venue's data is replaced
|
||||
- **AND** a later successful empty read clears the recovered wallet/venue's previous positions
|
||||
|
||||
#### Scenario: One venue fails
|
||||
- **WHEN** Phoenix fails for a wallet while Jupiter succeeds, or vice versa
|
||||
- **THEN** only the successful venue is updated and the failed venue retains its previous data
|
||||
- **AND** subsequent wallets are still attempted
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# phoenix-perps-service Specification
|
||||
|
||||
## Purpose
|
||||
Read actual Phoenix perpetual positions for a Solana authority without signing or modifying accounts, preserving precise current USD notional and subaccount identities.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Read every active position under the authority
|
||||
The integration SHALL read official Phoenix REST trader state for the requested wallet authority at PDA index 0. It SHALL include nonzero positions from cross-margin and isolated subaccounts, preserve subaccount/symbol identity independently of sequence numbers, and return an immutable deterministically ordered result. No signing, trading or account activation SHALL occur.
|
||||
|
||||
#### Scenario: Cross and isolated positions share a symbol
|
||||
- **WHEN** subaccounts 0 and 2 both have a nonzero SOL position
|
||||
- **THEN** both positions are returned with distinct identities
|
||||
- **AND** zero-sized positions are excluded
|
||||
|
||||
### Requirement: Current USD notional uses documented market units
|
||||
Position direction SHALL follow signed quantity. USD size SHALL be the absolute current notional, calculated using exact decimal arithmetic from base lots divided by 10 to the market's base-lot decimal exponent and multiplied by its current mark price. Collateral, entry value and raw lot counts SHALL NOT be used as USD size. Missing conversion data SHALL fail the read.
|
||||
|
||||
#### Scenario: Convert a short position
|
||||
- **WHEN** a position has -125 base lots, base-lot exponent 2 and mark price 104.45 USD
|
||||
- **THEN** it represents -1.25 base units, Short direction and 130.5625 USD notional
|
||||
|
||||
### Requirement: Read failures cannot masquerade as empty portfolios
|
||||
HTTP, authentication, malformed schema, identity mismatch and numeric conversion errors SHALL fail the read. Requests SHALL have bounded timeouts. Only a valid response with no nonzero positions SHALL produce an empty result. Required mark prices SHALL be positive and finite.
|
||||
|
||||
#### Scenario: Missing mark price or rejected access
|
||||
- **WHEN** a required price is missing or an HTTP request is rejected
|
||||
- **THEN** the read fails instead of returning an empty or partial portfolio
|
||||
|
||||
#### Scenario: Empty account
|
||||
- **WHEN** a valid trader snapshot has no subaccounts or no nonzero positions
|
||||
- **THEN** an empty result is returned without market-price requests
|
||||
|
||||
#### Scenario: Public API rate limit
|
||||
- **WHEN** the shared adapter issues requests for PROD and TEST
|
||||
- **THEN** request starts SHALL be spaced at least one second apart
|
||||
- **AND** HTTP 429 SHALL fail the lookup and establish a shared cooldown from Retry-After (seconds or HTTP date, 60 seconds fallback)
|
||||
- **AND** lookups during cooldown SHALL fail without external calls, with no immediate retry
|
||||
Reference in New Issue
Block a user