98: Populate EvelynPerpsService with real Jupiter positions
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+91
-35
@@ -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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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"))));
|
||||
}
|
||||
|
||||
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 =
|
||||
|
||||
Reference in New Issue
Block a user