98: Populate EvelynPerpsService with real Jupiter positions

This commit is contained in:
2026-10-22 13:37:29 +02:00
parent 0ec22d5215
commit df5cc0eda7
15 changed files with 334 additions and 113 deletions
+21 -20
View File
@@ -434,25 +434,26 @@ Price Index beregnes og vises, mens de øvrige fem felter fortsat er nul. Legacy
## 12. Perps-service og Mission Control
`com.r35157.evelyn.service.perps.EvelynPerpsService` ejer positionsdata og udleverer hele det aktuelle,
immutable `PerpsSnapshot` gennem `getPositionsSnapshot()`. Snapshot'et indeholder wallets, deres venues og
positioner med stabile ID'er blandt søskende, adskilt fra visningsnavne. Positioner indeholder instrument,
`LONG`/`SHORT` og decimal USD-størrelse. Svaret indeholder ingen JavaFX-elementer, farver eller formatering.
`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` og injicerer de tilsvarende eksisterende `JupiterPerpsService`-views.
`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.
`impl.ref.EvelynPerpsServiceImpl` implementerer desuden `NenjimProcess`. Hub'en registrerer og starter det samme
objekt pr. miljø som `evelyn.perps.prod` og `evelyn.perps.test` før EMC.
Hub'en definerer den fælles hardcodede liste af `PerpsWalletConfiguration` (ID og visningsnavn) og giver den
til begge konstruktører. Hver instans kopierer konfigurationen og ejer sit eget snapshot. Eksempel-ID'erne er
ikke Solana-adresser. Servicen fordeler de to eksisterende eksempelporteføljer skiftevis efter wallet-rækkefølge;
positionerne for de to konfigurerede wallets er uændrede. Før `start()` afvises læsning; gentagen start afvises også.
Service-API'et eksponerer kun læsning, ingen lifecycle- eller positionsændringer. Servicen ejer de otte
uændrede eksempelpositioner fra den oprindelige Perps-fane og har ingen workers eller Jupiter/Phoenix-kald.
Servicen henter åbne Jupiter-positioner asynkront ved start og venter 60 sekunder efter hvert gennemløb,
så runder ikke overlapper. Den eksisterende Solana-cache med 60 sekunders TTL bevares. Data er derfor ikke
øjeblikkelige børsdata. Positionskontoen 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.
EMC læser hvert miljøs snapshot fra den tilsvarende service straks ved åbning og hvert tredje sekund på en
separat worker, uafhængigt af en eventuel senere integrations opdateringsinterval. Fejl i ét servicekald forhindrer ikke forsøget på det andet.
Begge miljøfaner viser fortsat de samme tydeligt markerede eksempler;
EMC ejer træstruktur, USD-formatering og grønne/røde rækker. Uændrede snapshots berører ikke eksisterende rækker,
foldestatus eller markering. Ved ændringer genbruges træelementer efter deres ID'er inden for samme forælder.
UI-opdateringer sker kun på JavaFX-tråden. Fejl bevarer visningen og stopper ikke næste pollingforsøg.
Skjul/luk stopper polling; genåbning starter én ny løkke og afviser gamle resultater. Servicen fortsætter uafhængigt
af vinduet. Der er ingen rigtig integration, simulerede ændringer, Refresh-knap eller handel i denne service.
Før første vellykkede opslag er der ingen positioner. Hver wallets succes publiceres i et nyt snapshot;
fejl logges og bevarer den wallets sidste data uden at afbryde de øvrige. Et vellykket tomt svar fjerner
positionerne, men bevarer wallet/Jupiter-gruppen. Der vises ingen indikator for forældede data.
Implementationens `close()` afbryder worker'en; consumer-API'et indeholder kun læsning.
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æstruktur, tre kolonner, farver, formatering,
foldestatus og markering bevares for eksisterende rækker. Nye/lukkede positioner afstemmes efter ID.
Der er ingen sample-positioner, Phoenix-eksempler, sample-tekster, Refresh-knap eller handel.
Den kanoniske systembeskrivelse findes i
[Evelyn: services og status](../../r35157_docs/projects/evelyn/README.md#evelynperpsservice).
+5 -3
View File
@@ -288,12 +288,14 @@ Historik ligger fortsat i `data/evelyn/<instance-name>/status.log` med formatver
Mission Control kræver både `evelyn.perps.prod` og `evelyn.perps.test` før `evelyn.mission-control.default`
i den private `conf/NenjimHub.conf`. Erstat den tidligere `evelyn.perps.default`-linje med de to nye ID'er;
det gamle ID accepteres ikke længere. Hub'en konstruerer, registrerer og starter to separate
`EvelynPerpsServiceImpl`-processer med samme hardcodede wallet-konfiguration og injicerer deres read-only
`EvelynPerpsServiceImpl`-processer med separate hardcodede wallet-lister og tilhørende Jupiter-integrationer.
Hub'en injicerer deres read-only
service-views til de tilsvarende miljøfaner i EMC. Process Manager viser begge ID'er efter vellykket startup.
Den lokale udviklingskonfiguration er opdateret; andre deployments med EMC skal migreres tilsvarende.
Udeladt eller forkert rækkefølge giver en tydelig konstruktionsfejl uden automatisk indsættelse.
Det minimale versionsstyrede bootstrap-eksempel er uændret. Hver service ejer sit eget snapshot med
uændrede in-memory-eksempler og foretager ingen eksterne positionskald.
Det minimale versionsstyrede bootstrap-eksempel er uændret. Hver service ejer sit eget snapshot og
opdaterer det fra Jupiter i baggrunden med 60 sekunders pause mellem gennemløb. EMC-læsninger bruger kun
det publicerede snapshot. Wallet-listerne fra issue #98 indeholder seks PROD-adresser og én TEST-adresse.
Hvis `evelyn.iou-burner.prod` vælges, skal både `assetaz.ticker.default` og
`evelyn.status.prod` stå før den. Hub'en injicerer Production status-servicen til
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-22
@@ -0,0 +1,22 @@
# Design
## Context
See proposal.md and issue #98. Hub already has distinct Jupiter readers backed by a shared Solana cache (60-second TTL). The current Perps service creates samples; EMC already reconciles immutable snapshots by identity.
## Goals / Non-Goals
Keep existing public snapshot reads and read-only behavior. No changes to Jupiter trading, its caching, new columns, Phoenix, or stale-data UI. System documentation in the sibling repository is explicitly within the user-authorized scope.
## Decisions
- Replace sample wallet ID/name configuration with a SolanaWalletId address; use it for both row ID and label. Hub supplies separate fixed lists and the existing matching Jupiter dependency.
- One scheduled daemon worker per instance starts immediately, then uses a 60-second fixed delay after completion. This avoids overlap and catch-up bursts on slow RPCs. Window visibility does not affect it. Implementation-only close interrupts/shuts down the worker; the consumer interface remains read-only.
- Maintain last successful wallets in worker-owned state and publish a new immutable snapshot after each successful wallet, in configured order. Initial snapshot is empty; successful empty reads retain the wallet/Jupiter grouping with no positions. Errors preserve that wallet only; interruption ends the cycle.
- Reuse the three known Jupiter mint mappings already present in the alarm code (SOL/BTC/ETH); unknown mints display verbatim. No new currency registry entries are needed. Sort positions by account for deterministic results from unordered sets.
- Remove sample labels in PerpsTab; retain its existing identity reconciliation and polling. No new UI states.
## Risks / Trade-offs
- Errors can leave old data visible → log per wallet and document this deliberate limitation; stale indicators remain out of scope.
- Existing Jupiter model has dummy PnL and prices → use only required mint, direction, account and actual positionSize.
- Live RPC availability may limit end-to-end verification → record actual outcomes without claiming live success from compilation alone.
## Migration Plan
No process ID or autostart changes. Rebuild and restart through normal deployment to load the new wallet lists. Verification must not start the full Hub or any trading process. Leave all edits uncommitted for review.
@@ -0,0 +1,21 @@
# Proposal
## Why
Issue #98 replaces sample portfolios with the owner's actual Jupiter positions so Mission Control can provide a useful read-only overview. Existing Jupiter position reads and the separate TEST/PROD services can be reused.
## What Changes
- Supply six PROD wallet addresses and one TEST address from Hub construction, with the corresponding existing Jupiter integrations.
- Refresh asynchronously at startup and every 60 seconds; keep immutable snapshot reads independent of integration calls.
- Preserve failed wallets' last successful data, clear successful empty results, and order positions deterministically.
- Remove sample/Phoenix positions and sample notices while retaining the existing table and polling.
- Update project documentation and the canonical system documentation in the sibling `r35157_docs` repository as explicitly requested.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `evelyn-perps-service`: Live Jupiter refresh, real environment-specific wallet configuration and per-wallet failure behavior.
- `evelyn-perps-overview`: Real Jupiter data without prototype labels, preserving the existing presentation and row reconciliation.
## Impact
Perps configuration and implementation, Hub construction, EMC labels, and documentation in both repositories. No changes to trading operations, Phoenix integration, cache TTLs, or the parameterless public read API.
@@ -0,0 +1,26 @@
## REMOVED Requirements
### Requirement: Sample positions communicate direction and USD size
**Reason**: Sample data is replaced by real Jupiter positions.
**Migration**: Keep existing formatting and colors for actual positions.
### Requirement: Prototype is clearly labeled and entirely local
**Reason**: Data now comes from real Jupiter reads performed by the service.
**Migration**: Remove sample and illustrative-Phoenix notices; preserve EMC's snapshot-only boundary.
## ADDED Requirements
### Requirement: Jupiter positions communicate direction and USD size
Position rows SHALL show Long or Short and USD sizes with grouping separators and two decimal places. Long rows SHALL have faint green backgrounds and Short rows faint red backgrounds; wallet and venue rows SHALL have no direction-based color. Existing columns and hierarchy SHALL be preserved.
#### Scenario: Inspect real positions
- **WHEN** real Jupiter positions are displayed
- **THEN** direction appears as readable text and background color, with formatted USD size
### Requirement: EMC presents service data without sample notices
EMC SHALL obtain positions only from its environment's in-memory service snapshot and SHALL contain no sample positions, sample notices, illustrative-Phoenix notices, or Refresh button. EMC SHALL own UI components, colors and formatting; integration reads SHALL belong to the service.
#### Scenario: Snapshot changes
- **WHEN** positions are added or removed after refresh
- **THEN** EMC reconciles rows by identity while preserving retained selections and expansion state
- **AND** no sample labels or Phoenix examples appear
@@ -0,0 +1,57 @@
## MODIFIED Requirements
### Requirement: Read-only current positions snapshot
The Perps service SHALL expose only a read operation returning its complete current in-memory snapshot. Wallets, venues within wallets, and positions within venues SHALL have stable identifiers across reads. Responses SHALL be immutable domain data without UI components, colors or formatting. Reading the snapshot SHALL make no Jupiter or Phoenix requests.
#### Scenario: Repeated reads
- **WHEN** clients repeatedly retrieve the started service's snapshot
- **THEN** they receive the latest published positions with stable identifiers and cannot mutate service-owned collections
- **AND** the reads trigger no exchange requests or position-management operations
### Requirement: Hub owns Perps startup
The service SHALL run as two separate Hub-managed processes registered as `evelyn.perps.test` and `evelyn.perps.prod`. The service API SHALL expose no lifecycle operations. Snapshot reads before process startup SHALL fail clearly. Startup SHALL schedule asynchronous retrieval without waiting for integration I/O.
#### Scenario: Service starts
- **WHEN** the Hub starts either registered Perps process
- **THEN** its service view is available without waiting for retrieval, initially empty until data arrives
- **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.
#### Scenario: Separate configured instances
- **WHEN** the Hub constructs and starts both instances
- **THEN** each instance queries only its configured wallets through its corresponding integration
- **AND** the implementation does not define the wallet list internally
## REMOVED Requirements
### Requirement: Service owns the unchanged sample positions
**Reason**: Real Jupiter positions replace all sample data.
**Migration**: Remove sample portfolios and Phoenix examples; configure real addresses in Hub.
## ADDED Requirements
### 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.
#### 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. Only Jupiter venues and actual positions SHALL be published.
#### 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.
#### 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
@@ -0,0 +1,20 @@
# Tasks
## 1. Live service and construction
- [x] 1.1 Replace samples with background Jupiter reads, address configuration and Hub injection; verify mapping, wallet isolation, empty results, deterministic ordering and cache-only snapshot reads in isolated execution.
- [x] 1.2 Document refresh lifecycle and configuration in project docs; verify exact issue wallet lists and removal of obsolete sample descriptions.
## 2. Presentation and system documentation
- [x] 2.1 Remove sample notices and retain identity-based rendering; verify additions/removals and retained selection/expansion in isolated JavaFX execution.
- [x] 2.2 Update canonical r35157_docs service overview and relevant links; verify documentation against code, distinguish code verification from deployment and check relative links.
## 3. Integration verification
- [x] 3.1 Run Gradle build, strict OpenSpec validation, diff review and a read-only live lookup if available; record evidence and leave all work uncommitted.
## Verification results
- Gradle build passed, including all 42 existing tests. No new repository tests were added, following project instructions.
- Isolated execution verified asynchronous initial retrieval, immutable/cached snapshot reads, configured address validation, mapping and unknown mint fallback, exact sizes, deterministic order, per-wallet failure retention, successful empty clearing and interruption.
- Isolated JavaFX execution verified removal of notices, stable selection/expansion/row identities and additions/removals; screenshot inspected. Service refresh remains independent of EMC polling.
- Read-only live calls through the existing Jupiter adapter completed for all seven configured addresses on 2026-10-22: each returned zero open positions. This verifies live reads/empty results, not actual open-position decoding; nonempty behavior was checked with isolated data. No wallet/signing operation or full Hub startup was invoked.
- Strict OpenSpec validation and diff checks passed. System documentation and its links were checked in r35157_docs; no deployment is claimed.
- Existing #97 index/worktree changes were left untouched. No commits or pushes; change remains active for review.
+16 -19
View File
@@ -2,7 +2,7 @@
## Purpose
Let the owner inspect Evelyn perpetual positions grouped by wallet and venue in Mission Control, beginning with a clearly labeled sample-data prototype.
Let the owner inspect Evelyn perpetual positions grouped by wallet and venue in Mission Control, with stable row identities across service snapshot updates.
## Requirements
@@ -18,24 +18,6 @@ The Perps tab SHALL display a tree table with columns `Position`, `Direction`, a
- **WHEN** the owner collapses and then expands a wallet or venue
- **THEN** its descendants are hidden and then shown again
### Requirement: Sample positions communicate direction and USD size
The overview SHALL include hardcoded Jupiter and Phoenix venues with multiple sample positions, including both Long and Short directions. Position rows SHALL show `Long` or `Short` text and USD sizes with grouping separators and two decimal places. Long rows SHALL have faint green backgrounds and Short rows faint red backgrounds with readable text; wallet and venue rows SHALL have no direction-based color.
#### Scenario: Inspect sample positions
- **WHEN** the owner inspects Jupiter and Phoenix examples
- **THEN** both venues have multiple positions and both directions are demonstrated
- **AND** position sizes use readable USD formatting
- **AND** only position rows use direction-based backgrounds, with direction also conveyed by text
### Requirement: Prototype is clearly labeled and entirely local
The Perps view SHALL explicitly label its data as sample data and explain that Phoenix rows are illustrative without an existing integration. It SHALL obtain positions only from the Perps service's current in-memory snapshot, with no external network calls, Refresh button, or simulated position changes. EMC SHALL own all UI components, row colors and formatting.
#### Scenario: Inspect the prototype boundary
- **WHEN** the Perps tab is displayed
- **THEN** sample-data and illustrative-Phoenix labels are visible
- **AND** there is no Refresh button or simulated update of sample positions
- **AND** displaying and interacting with the prototype uses only the service's local sample snapshot
### Requirement: Asynchronous polling follows window visibility
EMC SHALL request the current Perps snapshot from each environment's own service immediately on opening and every three seconds while visible, without blocking the UI thread. UI updates SHALL occur only on the JavaFX thread. Hiding the window SHALL stop polling; reopening SHALL create only one active polling loop and ignore results belonging to an earlier window session. Poll failures SHALL preserve the current view and allow subsequent attempts.
@@ -59,3 +41,18 @@ The Perps table SHALL retain matching wallet, venue and position identities acro
#### Scenario: Poll unchanged positions
- **WHEN** a user collapses a group or selects a position and repeated snapshots are unchanged
- **THEN** the same rows remain, with the same expansion and selection and no rebuild or refresh
### Requirement: Jupiter positions communicate direction and USD size
Position rows SHALL show Long or Short and USD sizes with grouping separators and two decimal places. Long rows SHALL have faint green backgrounds and Short rows faint red backgrounds; wallet and venue rows SHALL have no direction-based color. Existing columns and hierarchy SHALL be preserved.
#### Scenario: Inspect real positions
- **WHEN** real Jupiter positions are displayed
- **THEN** direction appears as readable text and background color, with formatted USD size
### Requirement: EMC presents service data without sample notices
EMC SHALL obtain positions only from its environment's in-memory service snapshot and SHALL contain no sample positions, sample notices, illustrative-Phoenix notices, or Refresh button. EMC SHALL own UI components, colors and formatting; integration reads SHALL belong to the service.
#### Scenario: Snapshot changes
- **WHEN** positions are added or removed after refresh
- **THEN** EMC reconciles rows by identity while preserving retained selections and expansion state
- **AND** no sample labels or Phoenix examples appear
+33 -17
View File
@@ -6,32 +6,48 @@ Provide a view-only source of current Evelyn perpetual positions independently o
## Requirements
### Requirement: Read-only current positions snapshot
The Perps service SHALL expose only a read operation returning its complete current in-memory snapshot. Wallets, venues within wallets, and positions within venues SHALL have stable identifiers across reads. Responses SHALL be immutable domain data without UI components, colors or formatting, and SHALL make no Jupiter or Phoenix requests.
The Perps service SHALL expose only a read operation returning its complete current in-memory snapshot. Wallets, venues within wallets, and positions within venues SHALL have stable identifiers across reads. Responses SHALL be immutable domain data without UI components, colors or formatting. Reading the snapshot SHALL make no Jupiter or Phoenix requests.
#### Scenario: Repeated reads
- **WHEN** clients repeatedly retrieve the started service's snapshot
- **THEN** they receive the same complete positions with stable identifiers and cannot mutate service-owned collections
- **AND** no exchange requests or position-management operations occur
### Requirement: Service owns the unchanged sample positions
The service SHALL own the existing eight sample positions grouped under Sample wallet A and Sample wallet B and their Jupiter and Phoenix venues, retaining every existing instrument, direction and decimal USD size. The snapshot SHALL remain unchanged for this task.
#### Scenario: Existing samples are retrieved
- **WHEN** EMC retrieves the service snapshot
- **THEN** wallet A contains Jupiter SOL Long 12500.00 and BTC Short 8250.50, Phoenix SOL Short 3200.75 and ETH Long 5400.00
- **AND** wallet B contains Jupiter ETH Long 1875.25 and SOL Short 950.00, Phoenix BTC Long 21000.00 and SOL Short 640.50
- **THEN** they receive the latest published positions with stable identifiers and cannot mutate service-owned collections
- **AND** the reads trigger no exchange requests or position-management operations
### Requirement: Hub owns Perps startup
The service SHALL run as two separate Hub-managed processes registered as `evelyn.perps.test` and `evelyn.perps.prod`. The service API SHALL expose no lifecycle operations. Snapshot reads before process startup SHALL fail clearly.
The service SHALL run as two separate Hub-managed processes registered as `evelyn.perps.test` and `evelyn.perps.prod`. The service API SHALL expose no lifecycle operations. Snapshot reads before process startup SHALL fail clearly. Startup SHALL schedule asynchronous retrieval without waiting for integration I/O.
#### Scenario: Service starts
- **WHEN** the Hub starts either registered Perps process
- **THEN** its service view makes the complete sample snapshot available without starting a polling or integration worker
- **THEN** its service view is available without waiting for retrieval, initially empty until data arrives
- **AND** no placeholder positions are generated before successful retrieval
### Requirement: Wallet configuration belongs to service construction
The Hub SHALL supply the same hardcoded wallet configuration to TEST and PROD at construction. Each service SHALL retain an immutable copy and own a separate snapshot. EMC SHALL neither own nor pass wallet configuration; snapshot 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 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 using the shared sample wallet configuration
- **THEN** each snapshot contains the configured wallet identities and existing sample positions
- **AND** the two instances own separate snapshots without defining the wallet list internally
- **WHEN** the Hub constructs and starts both instances
- **THEN** each instance queries only its configured wallets through its corresponding integration
- **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.
#### 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. Only Jupiter venues and actual positions SHALL be published.
#### 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.
#### 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
@@ -5,7 +5,6 @@ import com.r35157.evelyn.service.perps.PerpsSnapshot.Direction;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
@@ -69,13 +68,7 @@ final class PerpsTab {
}
});
Label sampleNotice = new Label(
"Sample data only — not live positions. Phoenix rows are illustrative; no Phoenix integration exists."
);
sampleNotice.setWrapText(true);
sampleNotice.setMinHeight(VBox.USE_PREF_SIZE);
VBox content = new VBox(8, sampleNotice, table);
VBox content = new VBox(table);
content.setPadding(new Insets(10));
content.setStyle("-fx-background-color: " + environmentColor + ";");
VBox.setVgrow(table, Priority.ALWAYS);
@@ -167,6 +160,6 @@ final class PerpsTab {
+ "-fx-control-inner-background-alt: #fce8e6;";
private final Tab tab;
private final TreeTableView<PositionRow> table = new TreeTableView<>(
new TreeItem<>(group("root", "Sample positions")));
new TreeItem<>(group("root", "Positions")));
private PerpsSnapshot renderedSnapshot;
}
@@ -3,7 +3,7 @@ package com.r35157.evelyn.service.perps;
import com.r35157.nenjim.component.NenjimComponent;
import org.jetbrains.annotations.NotNull;
/** Read-only current positions; startup belongs to the Hub, refresh timing to the caller. */
/** Read-only current positions; startup belongs to the Hub, integration refresh timing to the service. */
public interface EvelynPerpsService extends NenjimComponent {
/** Returns the complete immutable in-memory snapshot without making exchange requests. */
@NotNull PerpsSnapshot getPositionsSnapshot();
@@ -2,10 +2,12 @@ package com.r35157.evelyn.service.perps;
import java.util.Objects;
/** Hub-supplied wallet identity; sample IDs are not real Solana addresses. */
public record PerpsWalletConfiguration(String id, String name) {
/** Hub-supplied Solana wallet address, also used as its stable ID and display label. */
public record PerpsWalletConfiguration(ΩSolanaWalletIdΩ address) {
public PerpsWalletConfiguration {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(name, "name");
Objects.requireNonNull(address, "address");
if (address.isBlank()) {
throw new IllegalArgumentException("Perps wallet address must not be blank");
}
}
}
@@ -7,57 +7,101 @@ import com.r35157.evelyn.service.perps.PerpsSnapshot.Position;
import com.r35157.evelyn.service.perps.PerpsSnapshot.Venue;
import com.r35157.evelyn.service.perps.PerpsSnapshot.Wallet;
import com.r35157.evelyn.service.perps.PerpsSnapshot.Direction;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.nenjim.process.NenjimProcess;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/** Owns the fixed examples only. No exchange clients, position management or update worker. */
public final class EvelynPerpsServiceImpl implements EvelynPerpsService, NenjimProcess {
public EvelynPerpsServiceImpl(@NotNull List<PerpsWalletConfiguration> wallets) {
/** Reads Jupiter positions in the background; consumers only read the published snapshot. */
public final class EvelynPerpsServiceImpl implements EvelynPerpsService, NenjimProcess, AutoCloseable {
public EvelynPerpsServiceImpl(@NotNull List<PerpsWalletConfiguration> wallets,
@NotNull JupiterPerpsService jupiter) {
this.wallets = List.copyOf(wallets);
var ids = new HashSet<String>();
this.jupiter = Objects.requireNonNull(jupiter, "jupiter");
var addresses = new HashSet<String>();
var base58 = new Base58CodecImpl();
for (PerpsWalletConfiguration wallet : this.wallets) {
if (!ids.add(wallet.id())) {
throw new IllegalArgumentException("Duplicate Perps wallet ID: " + wallet.id());
if (base58.decode(wallet.address()).length != 32) {
throw new IllegalArgumentException("Invalid Solana wallet address: " + wallet.address());
}
if (!addresses.add(wallet.address())) {
throw new IllegalArgumentException("Duplicate Perps wallet address: " + wallet.address());
}
}
}
@Override
public synchronized void start() {
if (snapshot != null) {
throw new IllegalStateException("Perps service has already started");
if (worker != null || closed) {
throw new IllegalStateException("Perps service has already started or closed");
}
var configuredWallets = new ArrayList<Wallet>();
for (int index = 0; index < wallets.size(); index++) {
PerpsWalletConfiguration wallet = wallets.get(index);
configuredWallets.add(new Wallet(wallet.id(), wallet.name(), sampleVenues(index)));
}
snapshot = new PerpsSnapshot(configuredWallets);
snapshot = new PerpsSnapshot(List.of());
worker = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "evelyn-perps-refresh");
thread.setDaemon(true);
return thread;
});
worker.scheduleWithFixedDelay(this::refresh, 0, REFRESH_SECONDS, TimeUnit.SECONDS);
}
/** Alternate the original two sample portfolios in configured wallet order. */
private static List<Venue> sampleVenues(int walletIndex) {
if (walletIndex % 2 == 0) {
return List.of(
new Venue("jupiter", "Jupiter", List.of(
position("sol-long", "SOL-PERP", Direction.LONG, "12500.00"),
position("btc-short", "BTC-PERP", Direction.SHORT, "8250.50"))),
new Venue("phoenix", "Phoenix", List.of(
position("sol-short", "SOL-PERP", Direction.SHORT, "3200.75"),
position("eth-long", "ETH-PERP", Direction.LONG, "5400.00"))));
private void refresh() {
for (PerpsWalletConfiguration wallet : wallets) {
if (closed || Thread.currentThread().isInterrupted()) {
return;
}
return List.of(
new Venue("jupiter", "Jupiter", List.of(
position("eth-long", "ETH-PERP", Direction.LONG, "1875.25"),
position("sol-short", "SOL-PERP", Direction.SHORT, "950.00"))),
new Venue("phoenix", "Phoenix", List.of(
position("btc-long", "BTC-PERP", Direction.LONG, "21000.00"),
position("sol-short", "SOL-PERP", Direction.SHORT, "640.50"))));
try {
List<Position> positions = jupiter.getOpenPositions(wallet.address()).stream()
.sorted(Comparator.comparing(JupiterPerpsPosition::positionAccount))
.map(EvelynPerpsServiceImpl::position)
.toList();
if (closed || Thread.currentThread().isInterrupted()) {
return;
}
lastSuccessful.put(wallet.address(), new Wallet(wallet.address(), wallet.address(),
List.of(new Venue("jupiter", "Jupiter", positions))));
snapshot = new PerpsSnapshot(wallets.stream()
.map(configured -> lastSuccessful.get(configured.address()))
.filter(Objects::nonNull)
.toList());
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
return;
} catch (IOException | RuntimeException exception) {
log.warn("Unable to refresh Jupiter Perps positions for wallet {}", wallet.address(), exception);
}
}
}
private static Position position(JupiterPerpsPosition position) {
Direction direction = switch (position.direction()) {
case LONG -> Direction.LONG;
case SHORT -> Direction.SHORT;
};
return new Position(position.positionAccount(), instrument(position.tradedTokenMint()),
direction, position.positionSize());
}
private static String instrument(ΩSPLMintAddressΩ mint) {
return switch (mint) {
case "So11111111111111111111111111111111111111112" -> "SOL-PERP";
case "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh" -> "BTC-PERP";
case "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs" -> "ETH-PERP";
default -> mint;
};
}
@Override
@@ -69,10 +113,22 @@ public final class EvelynPerpsServiceImpl implements EvelynPerpsService, NenjimP
return current;
}
private static Position position(String id, String instrument, Direction direction, String sizeUsd) {
return new Position(id, instrument, direction, new ΩAmountΩ(sizeUsd));
/** Implementation lifecycle only; closing EMC does not close this service. */
@Override
public synchronized void close() {
closed = true;
if (worker != null) {
worker.shutdownNow();
}
}
private static final Logger log = LoggerFactory.getLogger(EvelynPerpsServiceImpl.class);
private static final long REFRESH_SECONDS = 60;
private final List<PerpsWalletConfiguration> wallets;
private final JupiterPerpsService jupiter;
// Accessed only by the single refresh worker; consumers see immutable snapshots.
private final Map<String, Wallet> lastSuccessful = new HashMap<>();
private ScheduledExecutorService worker;
private volatile boolean closed;
private volatile PerpsSnapshot snapshot;
}
@@ -362,13 +362,13 @@ public final class NenjimHubImpl implements NenjimHub {
}
private NenjimProcess constructEvelynPerpsProdProcess() {
EvelynPerpsServiceImpl process = new EvelynPerpsServiceImpl(EVELYN_PERPS_WALLETS);
EvelynPerpsServiceImpl process = new EvelynPerpsServiceImpl(EVELYN_PERPS_PROD_WALLETS, jupiterServicePerpsProd);
evelynPerpsServiceProd = process;
return process;
}
private NenjimProcess constructEvelynPerpsTestProcess() {
EvelynPerpsServiceImpl process = new EvelynPerpsServiceImpl(EVELYN_PERPS_WALLETS);
EvelynPerpsServiceImpl process = new EvelynPerpsServiceImpl(EVELYN_PERPS_TEST_WALLETS, jupiterServicePerpsTest);
evelynPerpsServiceTest = process;
return process;
}
@@ -567,9 +567,15 @@ public final class NenjimHubImpl implements NenjimHub {
new NenjimComponentId("evelyn.perps.prod");
private static final NenjimComponentId EVELYN_PERPS_TEST_ID =
new NenjimComponentId("evelyn.perps.test");
private static final List<PerpsWalletConfiguration> EVELYN_PERPS_WALLETS = List.of(
new PerpsWalletConfiguration("sample-wallet-a", "Sample wallet A"),
new PerpsWalletConfiguration("sample-wallet-b", "Sample wallet B"));
private static final List<PerpsWalletConfiguration> EVELYN_PERPS_PROD_WALLETS = List.of(
new PerpsWalletConfiguration("H5Yrdm7B4FHhTxhPw9SVwFzQyTEL6bAPptZuTyRxcepq"),
new PerpsWalletConfiguration("7PttBqapKgydZG1KGnv4hDFRVRumLqRTAaqN7SbcDmok"),
new PerpsWalletConfiguration("vshTPUDaZjU94uM85cDiu5nyVxUQifkeMsGa3UBL6Ct"),
new PerpsWalletConfiguration("8XGrkDQCSHVP9VnXGReRodPwJ5bU95PUnCY7kPYAb2p5"),
new PerpsWalletConfiguration("4wzxcYQ8o3LZsKiMBmJEHu5WXCJvUsUqxBWsZZuXea9j"),
new PerpsWalletConfiguration("HM13QYMvDEBB1HfHwc6LPFyvX26bJQWiz4BLeUdwrKyF"));
private static final List<PerpsWalletConfiguration> EVELYN_PERPS_TEST_WALLETS = List.of(
new PerpsWalletConfiguration("6NyYEoXmJyBXvU4pvv9aoxZNdyqqtkmPBZFEDkRDHcqp"));
private static final NenjimComponentId JAVAFX_RUNTIME_SERVICE_ID =
new NenjimComponentId("nenjim.javafx-runtime.service");
private static final NenjimComponentId PROCESS_MANAGER_ID =