96: Extract Perps sample data into EvelynPerpsService and poll it from EMC

This commit is contained in:
2026-10-22 10:37:04 +02:00
parent 2dce841cf4
commit 1ab439b578
19 changed files with 592 additions and 56 deletions
@@ -1,11 +1,15 @@
package com.r35157.evelyn.emc.impl.ref;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.evelyn.service.perps.EvelynPerpsService;
import com.r35157.evelyn.service.perps.PerpsSnapshot;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.evelyn.emc.EvelynMissionControl;
import com.r35157.nenjim.process.NenjimProcess;
import com.r35157.service.javafx.JavaFxRuntimeService;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
@@ -26,6 +30,9 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
public final class EvelynMissionControlImpl implements EvelynMissionControl, NenjimProcess {
@@ -34,10 +41,12 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
public EvelynMissionControlImpl(
@NotNull EvelynStatusService evelynStatusServiceProd,
@NotNull EvelynStatusService evelynStatusServiceTest,
@NotNull EvelynPerpsService evelynPerpsService,
@NotNull JavaFxRuntimeService javaFxRuntimeService
) {
this.evelynStatusProd = Objects.requireNonNull(evelynStatusServiceProd, "evelynStatusServiceProd");
this.evelynStatusTest = Objects.requireNonNull(evelynStatusServiceTest, "evelynStatusServiceTest");
this.evelynPerpsService = Objects.requireNonNull(evelynPerpsService, "evelynPerpsService");
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
@@ -145,23 +154,28 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
if (window == null) {
window = new Stage();
window.setTitle("Evelyn Mission Control");
window.setOnHidden(event -> refreshTimeline.stop());
window.setOnHidden(event -> {
refreshTimeline.stop();
stopPerpsPolling();
});
window.setScene(new Scene(createRootTabs(), 600, 400));
}
window.setScene(new Scene(createRootTabs(), 600, 400));
refreshCharts();
window.show();
refreshTimeline.playFromStart();
startPerpsPolling();
}
private @NotNull TabPane createMissionControlTabs(
@NotNull String color,
@NotNull StatusIndexChart statusIndexChart
@NotNull StatusIndexChart statusIndexChart,
@NotNull PerpsTab perpsTab
) {
TabPane tabs = new TabPane(
createOverviewTab(color, statusIndexChart),
createMissionControlTab("Portfolio", color),
PerpsTab.create(color),
perpsTab.tab(),
createMissionControlTab("Spot", color),
createMissionControlTab("Liquidity Pools", color),
createMissionControlTab("Alarms", color),
@@ -239,16 +253,18 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
private @NotNull TabPane createRootTabs() {
productionChart = createStatusIndexChart();
testChart = createStatusIndexChart();
productionPerps = new PerpsTab(PRODUCTION_COLOR);
testPerps = new PerpsTab(TEST_COLOR);
Tab productionTab = new Tab(
"Production",
createMissionControlTabs(PRODUCTION_COLOR, productionChart)
createMissionControlTabs(PRODUCTION_COLOR, productionChart, productionPerps)
);
productionTab.setStyle("-fx-background-color: " + PRODUCTION_COLOR + ";");
Tab testTab = new Tab(
"Test",
createMissionControlTabs(TEST_COLOR, testChart)
createMissionControlTabs(TEST_COLOR, testChart, testPerps)
);
testTab.setStyle("-fx-background-color: " + TEST_COLOR + ";");
@@ -262,6 +278,37 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
refreshChart(productionChart, evelynStatusProd.getStatusIndexHistory());
refreshChart(testChart, evelynStatusTest.getStatusIndexHistory());
}
private void startPerpsPolling() {
stopPerpsPolling();
ScheduledExecutorService session = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "emc-perps-poll");
thread.setDaemon(true);
return thread;
});
perpsPolling = session;
session.scheduleAtFixedRate(() -> {
try {
PerpsSnapshot snapshot = evelynPerpsService.getPositionsSnapshot();
javaFxRuntimeService.runLater(() -> {
if (perpsPolling == session && window.isShowing()) {
productionPerps.update(snapshot);
testPerps.update(snapshot);
}
});
} catch (RuntimeException exception) {
log.warn("Unable to refresh Perps positions", exception);
}
}, 0, PERPS_REFRESH_SECONDS, TimeUnit.SECONDS);
}
private void stopPerpsPolling() {
ScheduledExecutorService session = perpsPolling;
perpsPolling = null;
if (session != null) {
session.shutdownNow();
}
}
// endregion
// region Inner classes
@@ -307,6 +354,8 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
private static final String TEST_COLOR = "#d8f3dc";
private static final double EMPTY_Y_BOUND = 1.0;
private static final double CHART_REFRESH_SECONDS = 1.0;
private static final long PERPS_REFRESH_SECONDS = 3;
private static final Logger log = LoggerFactory.getLogger(EvelynMissionControlImpl.class);
private static final long SINGLE_POINT_X_PADDING_MILLIS = 30_000;
private static final DateTimeFormatter TIMESTAMP_TICK_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm dd/MM/yy")
@@ -333,6 +382,7 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
// region Dependencies
private final EvelynStatusService evelynStatusProd;
private final EvelynStatusService evelynStatusTest;
private final EvelynPerpsService evelynPerpsService;
private final JavaFxRuntimeService javaFxRuntimeService;
// endregion
@@ -340,5 +390,8 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl, Nen
private Stage window;
private StatusIndexChart productionChart;
private StatusIndexChart testChart;
private PerpsTab productionPerps;
private PerpsTab testPerps;
private ScheduledExecutorService perpsPolling;
// endregion
}
@@ -1,5 +1,8 @@
package com.r35157.evelyn.emc.impl.ref;
import com.r35157.evelyn.service.perps.PerpsSnapshot;
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;
@@ -13,10 +16,15 @@ import javafx.scene.layout.VBox;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
final class PerpsTab {
static Tab create(String environmentColor) {
PerpsTab(String environmentColor) {
NumberFormat usdFormat = NumberFormat.getCurrencyInstance(Locale.US);
TreeTableColumn<PositionRow, String> position = new TreeTableColumn<>("Position");
@@ -28,7 +36,7 @@ final class PerpsTab {
TreeTableColumn<PositionRow, String> direction = new TreeTableColumn<>("Direction");
direction.setCellValueFactory(cell -> {
Direction value = cell.getValue().getValue().direction();
return new ReadOnlyStringWrapper(value == null ? "" : value.label);
return new ReadOnlyStringWrapper(value == null ? "" : value == Direction.LONG ? "Long" : "Short");
});
direction.setPrefWidth(90);
direction.setMinWidth(80);
@@ -44,7 +52,7 @@ final class PerpsTab {
size.setStyle("-fx-alignment: CENTER-RIGHT;");
size.setSortable(false);
TreeTableView<PositionRow> table = new TreeTableView<>(createSampleRoot());
table.getRoot().setExpanded(true);
table.setShowRoot(false);
table.setTreeColumn(position);
table.getColumns().add(position);
@@ -57,7 +65,7 @@ final class PerpsTab {
super.updateItem(item, empty);
setStyle(empty || item == null || item.direction() == null
? ""
: item.direction().rowStyle);
: item.direction() == Direction.LONG ? LONG_ROW_STYLE : SHORT_ROW_STYLE);
}
});
@@ -72,58 +80,93 @@ final class PerpsTab {
content.setStyle("-fx-background-color: " + environmentColor + ";");
VBox.setVgrow(table, Priority.ALWAYS);
Tab tab = new Tab("Perps", content);
tab = new Tab("Perps", content);
tab.setStyle("-fx-background-color: " + environmentColor + ";");
}
Tab tab() {
return tab;
}
private static TreeItem<PositionRow> createSampleRoot() {
return group("Sample positions",
group("Sample wallet A",
group("Jupiter",
position("SOL-PERP", Direction.LONG, "12500.00"),
position("BTC-PERP", Direction.SHORT, "8250.50")),
group("Phoenix",
position("SOL-PERP", Direction.SHORT, "3200.75"),
position("ETH-PERP", Direction.LONG, "5400.00"))),
group("Sample wallet B",
group("Jupiter",
position("ETH-PERP", Direction.LONG, "1875.25"),
position("SOL-PERP", Direction.SHORT, "950.00")),
group("Phoenix",
position("BTC-PERP", Direction.LONG, "21000.00"),
position("SOL-PERP", Direction.SHORT, "640.50"))));
void update(PerpsSnapshot snapshot) {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Perps views must be updated on the JavaFX Application Thread");
}
Objects.requireNonNull(snapshot, "snapshot");
if (snapshot.equals(renderedSnapshot)) {
return;
}
TreeItem<PositionRow> selected = table.getSelectionModel().getSelectedItem();
List<TreeItem<PositionRow>> wallets = new ArrayList<>();
Map<String, TreeItem<PositionRow>> existingWallets = childrenById(table.getRoot());
for (PerpsSnapshot.Wallet wallet : snapshot.wallets()) {
TreeItem<PositionRow> walletItem = item(existingWallets, group(wallet.id(), wallet.name()));
List<TreeItem<PositionRow>> venues = new ArrayList<>();
Map<String, TreeItem<PositionRow>> existingVenues = childrenById(walletItem);
for (PerpsSnapshot.Venue venue : wallet.venues()) {
TreeItem<PositionRow> venueItem = item(existingVenues, group(venue.id(), venue.name()));
List<TreeItem<PositionRow>> positions = new ArrayList<>();
Map<String, TreeItem<PositionRow>> existingPositions = childrenById(venueItem);
for (PerpsSnapshot.Position position : venue.positions()) {
positions.add(item(existingPositions, new PositionRow(
position.id(), position.instrument(), position.direction(), position.sizeUsd())));
}
updateChildren(venueItem, positions);
venues.add(venueItem);
}
updateChildren(walletItem, venues);
wallets.add(walletItem);
}
updateChildren(table.getRoot(), wallets);
if (selected != null && table.getSelectionModel().getSelectedItem() != selected) {
int selectedRow = table.getRow(selected);
if (selectedRow >= 0) {
table.getSelectionModel().select(selectedRow);
}
}
renderedSnapshot = snapshot;
}
@SafeVarargs
private static TreeItem<PositionRow> group(String label, TreeItem<PositionRow>... children) {
TreeItem<PositionRow> item = new TreeItem<>(new PositionRow(label, null, null));
item.getChildren().addAll(children);
item.setExpanded(true);
private static PositionRow group(String id, String label) {
return new PositionRow(id, label, null, null);
}
private static Map<String, TreeItem<PositionRow>> childrenById(TreeItem<PositionRow> parent) {
Map<String, TreeItem<PositionRow>> children = new HashMap<>();
for (TreeItem<PositionRow> child : parent.getChildren()) {
children.put(child.getValue().id(), child);
}
return children;
}
private static TreeItem<PositionRow> item(
Map<String, TreeItem<PositionRow>> existing, PositionRow row) {
TreeItem<PositionRow> item = existing.get(row.id());
if (item == null) {
item = new TreeItem<>(row);
item.setExpanded(true);
} else if (!row.equals(item.getValue())) {
item.setValue(row);
}
return item;
}
private static TreeItem<PositionRow> position(String label, Direction direction, String sizeUsd) {
return new TreeItem<>(new PositionRow(label, direction, new BigDecimal(sizeUsd)));
}
private record PositionRow(String label, Direction direction, ΩAmountΩ sizeUsd) {
}
private enum Direction {
LONG("Long", "#e6f4ea"),
SHORT("Short", "#fce8e6");
Direction(String label, String background) {
this.label = label;
this.rowStyle = "-fx-control-inner-background: " + background + ";"
+ "-fx-control-inner-background-alt: " + background + ";";
private static void updateChildren(TreeItem<PositionRow> parent, List<TreeItem<PositionRow>> children) {
if (!parent.getChildren().equals(children)) {
parent.getChildren().setAll(children);
}
private final String label;
private final String rowStyle;
}
private PerpsTab() {
private record PositionRow(String id, String label, Direction direction, ΩAmountΩ sizeUsd) {
}
private static final String LONG_ROW_STYLE = "-fx-control-inner-background: #e6f4ea;"
+ "-fx-control-inner-background-alt: #e6f4ea;";
private static final String SHORT_ROW_STYLE = "-fx-control-inner-background: #fce8e6;"
+ "-fx-control-inner-background-alt: #fce8e6;";
private final Tab tab;
private final TreeTableView<PositionRow> table = new TreeTableView<>(
new TreeItem<>(group("root", "Sample positions")));
private PerpsSnapshot renderedSnapshot;
}
@@ -0,0 +1,10 @@
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. */
public interface EvelynPerpsService extends NenjimComponent {
/** Returns the complete immutable in-memory snapshot without making exchange requests. */
@NotNull PerpsSnapshot getPositionsSnapshot();
}
@@ -0,0 +1,41 @@
package com.r35157.evelyn.service.perps;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
/** IDs are stable across reads and unique among siblings, independently of display names. */
public record PerpsSnapshot(List<Wallet> wallets) {
public PerpsSnapshot {
wallets = List.copyOf(wallets);
}
public record Wallet(String id, String name, List<Venue> venues) {
public Wallet {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(name, "name");
venues = List.copyOf(venues);
}
}
public record Venue(String id, String name, List<Position> positions) {
public Venue {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(name, "name");
positions = List.copyOf(positions);
}
}
public record Position(String id, String instrument, Direction direction, ΩAmountΩ sizeUsd) {
public Position {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(instrument, "instrument");
Objects.requireNonNull(direction, "direction");
Objects.requireNonNull(sizeUsd, "sizeUsd");
}
}
public enum Direction {
LONG, SHORT
}
}
@@ -0,0 +1,54 @@
package com.r35157.evelyn.service.perps.impl.ref;
import com.r35157.evelyn.service.perps.EvelynPerpsService;
import com.r35157.evelyn.service.perps.PerpsSnapshot;
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.nenjim.process.NenjimProcess;
import org.jetbrains.annotations.NotNull;
import java.math.BigDecimal;
import java.util.List;
/** Owns the fixed examples only. No exchange clients, position management or update worker. */
public final class EvelynPerpsServiceImpl implements EvelynPerpsService, NenjimProcess {
@Override
public synchronized void start() {
if (snapshot != null) {
throw new IllegalStateException("Perps service has already started");
}
snapshot = new PerpsSnapshot(List.of(
new Wallet("sample-wallet-a", "Sample wallet A", 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"))))),
new Wallet("sample-wallet-b", "Sample wallet B", 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")))))
));
}
@Override
public @NotNull PerpsSnapshot getPositionsSnapshot() {
PerpsSnapshot current = snapshot;
if (current == null) {
throw new IllegalStateException("Perps service has not started");
}
return current;
}
private static Position position(String id, String instrument, Direction direction, String sizeUsd) {
return new Position(id, instrument, direction, new ΩAmountΩ(sizeUsd));
}
private volatile PerpsSnapshot snapshot;
}
@@ -7,6 +7,8 @@ import com.r35157.evelyn.service.liquidity.impl.ref.EvelynLiquidityConfiguration
import com.r35157.evelyn.service.liquidity.impl.ref.EvelynLiquidityServiceImpl;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.evelyn.service.status.impl.ref.EvelynStatusServiceImpl;
import com.r35157.evelyn.service.perps.EvelynPerpsService;
import com.r35157.evelyn.service.perps.impl.ref.EvelynPerpsServiceImpl;
import com.fanitas.evelyn.service.burner.EvelynIOUBurnerService;
import com.fanitas.evelyn.service.burner.impl.ref.EvelynIOUBurnerServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -358,6 +360,12 @@ public final class NenjimHubImpl implements NenjimHub {
solanaBlockChain, cis, liquidityConfiguration);
}
private NenjimProcess constructEvelynPerpsProcess() {
EvelynPerpsServiceImpl process = new EvelynPerpsServiceImpl();
evelynPerpsService = process;
return process;
}
private NenjimProcess constructJavaFxRuntimeProcess() {
JavaFxRuntimeServiceImpl process = new JavaFxRuntimeServiceImpl();
javaFxRuntimeService = process;
@@ -374,6 +382,10 @@ public final class NenjimHubImpl implements NenjimHub {
evelynStatusServiceTest,
EVELYN_MISSION_CONTROL_ID,
EVELYN_STATUS_TEST_ID),
requireProcessDependency(
evelynPerpsService,
EVELYN_MISSION_CONTROL_ID,
EVELYN_PERPS_SERVICE_ID),
requireProcessDependency(
javaFxRuntimeService,
EVELYN_MISSION_CONTROL_ID,
@@ -421,6 +433,7 @@ public final class NenjimHubImpl implements NenjimHub {
Map.entry(EVELYN_STATUS_TEST_ID, this::constructEvelynStatusTestProcess),
Map.entry(EVELYN_LIQUIDITY_PROD_ID, this::constructEvelynLiquidityProdProcess),
Map.entry(EVELYN_IOU_BURNER_PROD_ID, this::constructEvelynIOUBurnerProdProcess),
Map.entry(EVELYN_PERPS_SERVICE_ID, this::constructEvelynPerpsProcess),
Map.entry(JAVAFX_RUNTIME_SERVICE_ID, this::constructJavaFxRuntimeProcess),
Map.entry(EVELYN_MISSION_CONTROL_ID, this::constructMissionControlProcess),
Map.entry(PROCESS_MANAGER_ID, this::constructProcessManagerProcess)
@@ -538,6 +551,8 @@ public final class NenjimHubImpl implements NenjimHub {
new NenjimComponentId("evelyn.liquidity.prod");
private static final NenjimComponentId EVELYN_MISSION_CONTROL_ID =
new NenjimComponentId("evelyn.mission-control.default");
private static final NenjimComponentId EVELYN_PERPS_SERVICE_ID =
new NenjimComponentId("evelyn.perps.default");
private static final NenjimComponentId JAVAFX_RUNTIME_SERVICE_ID =
new NenjimComponentId("nenjim.javafx-runtime.service");
private static final NenjimComponentId PROCESS_MANAGER_ID =
@@ -576,6 +591,7 @@ public final class NenjimHubImpl implements NenjimHub {
private JupiterPerpsAlarm jupiterPerpsAlarm;
private EvelynStatusService evelynStatusServiceProd;
private EvelynStatusService evelynStatusServiceTest;
private EvelynPerpsService evelynPerpsService;
private JupiterSwapService jupiterSwapService;
private BoundNotificationService discordAssetAZTokenNotificationService;
private EvelynIOUBurnerService evelynIOUBurnerServiceProd;