64: Implement live Evelyn Price Index in Evelyn Mission Control
This commit is contained in:
@@ -7,5 +7,9 @@ import java.util.List;
|
||||
public interface Evelyn {
|
||||
void executeService() throws Exception;
|
||||
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
|
||||
@NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory();
|
||||
}
|
||||
|
||||
@@ -13,14 +13,21 @@ import com.r35157.libs.solana.SolanaConstants;
|
||||
import com.r35157.libs.valuetypes.basic.AssetPrice;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import com.r35157.libs.valuetypes.basic.TradingPair;
|
||||
import com.r35157.assetaz.services.ticker.PriceObservation;
|
||||
import com.r35157.assetaz.services.ticker.TickerService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
|
||||
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
|
||||
@@ -38,7 +45,37 @@ public class EvelynImpl implements Evelyn {
|
||||
this.raydium = raydium;
|
||||
this.solanaChain = solanaChain;
|
||||
}*/
|
||||
public EvelynImpl() {
|
||||
public EvelynImpl(
|
||||
@NotNull TickerService tickerService,
|
||||
@NotNull TradingPair eveUsdtTradingPair
|
||||
) {
|
||||
this(
|
||||
tickerService,
|
||||
eveUsdtTradingPair,
|
||||
Clock.systemUTC(),
|
||||
SAMPLE_DELAY_MINUTES,
|
||||
TimeUnit.MINUTES
|
||||
);
|
||||
}
|
||||
|
||||
EvelynImpl(
|
||||
@NotNull TickerService tickerService,
|
||||
@NotNull TradingPair eveUsdtTradingPair,
|
||||
@NotNull Clock clock,
|
||||
long sampleDelay,
|
||||
@NotNull TimeUnit sampleDelayUnit
|
||||
) {
|
||||
this.tickerService = Objects.requireNonNull(tickerService, "tickerService");
|
||||
this.eveUsdtTradingPair = Objects.requireNonNull(
|
||||
eveUsdtTradingPair,
|
||||
"eveUsdtTradingPair"
|
||||
);
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
if (sampleDelay <= 0) {
|
||||
throw new IllegalArgumentException("sampleDelay must be positive");
|
||||
}
|
||||
this.sampleDelay = sampleDelay;
|
||||
this.sampleDelayUnit = Objects.requireNonNull(sampleDelayUnit, "sampleDelayUnit");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,36 +84,127 @@ public class EvelynImpl implements Evelyn {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory() {
|
||||
return List.of(
|
||||
statusIndexPoint("2026-08-02T12:00:00Z", "-0.20", "-0.35", "0.15", "0.30", "-0.10", "0.25"),
|
||||
statusIndexPoint("2026-08-03T12:00:00Z", "-0.10", "-0.20", "0.10", "0.20", "-0.05", "0.15"),
|
||||
statusIndexPoint("2026-08-04T12:00:00Z", "0.00", "-0.10", "0.05", "0.10", "0.00", "0.05"),
|
||||
statusIndexPoint("2026-08-05T12:00:00Z", "0.10", "0.05", "-0.05", "0.00", "0.05", "-0.10"),
|
||||
statusIndexPoint("2026-08-06T12:00:00Z", "0.20", "0.15", "-0.10", "-0.10", "0.10", "-0.20")
|
||||
public synchronized void start() {
|
||||
if (statusIndexScheduler != null || stoppingStatusIndexSampling) {
|
||||
throw new IllegalStateException("Evelyn is already started");
|
||||
}
|
||||
|
||||
ScheduledExecutorService newScheduler = Executors.newSingleThreadScheduledExecutor(
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable, "evelyn-price-index-sampler");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
);
|
||||
statusIndexScheduler = newScheduler;
|
||||
newScheduler.scheduleWithFixedDelay(
|
||||
this::sampleStatusIndexSafely,
|
||||
0,
|
||||
sampleDelay,
|
||||
sampleDelayUnit
|
||||
);
|
||||
}
|
||||
|
||||
private static EvelynStatusIndexPoint statusIndexPoint(
|
||||
String timestamp,
|
||||
String evelynPriceIndex,
|
||||
String eveSyrupPoolDepthIndex,
|
||||
String eveSyrupPoolBalanceIndex,
|
||||
String aazdkkUsdtPoolBalanceIndex,
|
||||
String aazdkkUsdtPoolPriceIndex,
|
||||
String aazdkkUsdtPoolDepthIndex
|
||||
) {
|
||||
return new EvelynStatusIndexPoint(
|
||||
Instant.parse(timestamp),
|
||||
new BigDecimal(evelynPriceIndex),
|
||||
new BigDecimal(eveSyrupPoolDepthIndex),
|
||||
new BigDecimal(eveSyrupPoolBalanceIndex),
|
||||
new BigDecimal(aazdkkUsdtPoolBalanceIndex),
|
||||
new BigDecimal(aazdkkUsdtPoolPriceIndex),
|
||||
new BigDecimal(aazdkkUsdtPoolDepthIndex)
|
||||
);
|
||||
@Override
|
||||
public void stop() {
|
||||
ScheduledExecutorService schedulerToStop;
|
||||
synchronized (this) {
|
||||
if (statusIndexScheduler == null) {
|
||||
clearStatusIndexHistory();
|
||||
return;
|
||||
}
|
||||
stoppingStatusIndexSampling = true;
|
||||
schedulerToStop = statusIndexScheduler;
|
||||
}
|
||||
|
||||
schedulerToStop.shutdownNow();
|
||||
boolean terminated = false;
|
||||
try {
|
||||
terminated = schedulerToStop.awaitTermination(
|
||||
TERMINATION_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
if (!terminated) {
|
||||
log.error("Evelyn status-index scheduler did not terminate");
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
schedulerToStop.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
clearStatusIndexHistory();
|
||||
synchronized (this) {
|
||||
if (terminated || schedulerToStop.isTerminated()) {
|
||||
statusIndexScheduler = null;
|
||||
stoppingStatusIndexSampling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory() {
|
||||
synchronized (statusIndexHistory) {
|
||||
return List.copyOf(statusIndexHistory);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearStatusIndexHistory() {
|
||||
synchronized (statusIndexHistory) {
|
||||
statusIndexHistory.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleStatusIndexSafely() {
|
||||
try {
|
||||
Instant samplingInstant = clock.instant();
|
||||
PriceObservation observation = tickerService.getLatestPrice(eveUsdtTradingPair);
|
||||
BigDecimal actualPrice = observation.price().price();
|
||||
BigDecimal evelynPriceIndex = EvelynPriceIndexCalculator.calculateIndex(
|
||||
actualPrice,
|
||||
samplingInstant
|
||||
);
|
||||
EvelynStatusIndexPoint point = new EvelynStatusIndexPoint(
|
||||
samplingInstant,
|
||||
evelynPriceIndex,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO
|
||||
);
|
||||
if (!isStatusIndexSamplingActive()) {
|
||||
return;
|
||||
}
|
||||
synchronized (statusIndexHistory) {
|
||||
statusIndexHistory.add(point);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn(
|
||||
"Could not sample Evelyn Price Index: tradingPair={}",
|
||||
eveUsdtTradingPair,
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized boolean isStatusIndexSamplingActive() {
|
||||
return statusIndexScheduler != null && !stoppingStatusIndexSampling;
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EvelynImpl.class);
|
||||
private static final long SAMPLE_DELAY_MINUTES = 1;
|
||||
private static final long TERMINATION_TIMEOUT_SECONDS = 10;
|
||||
|
||||
private final TickerService tickerService;
|
||||
private final TradingPair eveUsdtTradingPair;
|
||||
private final Clock clock;
|
||||
private final long sampleDelay;
|
||||
private final TimeUnit sampleDelayUnit;
|
||||
private final List<EvelynStatusIndexPoint> statusIndexHistory = new ArrayList<>();
|
||||
|
||||
private ScheduledExecutorService statusIndexScheduler;
|
||||
private boolean stoppingStatusIndexSampling;
|
||||
|
||||
/*
|
||||
private SPLTokenHolding getSPLHolding(ΩSolanaAddressΩ ownerAddress, ΩSPLMintAddressΩ splMintAddress) throws Exception {
|
||||
Map<ΩSPLMintAddressΩ, SPLTokenHolding> holdings = new HashMap<>();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.fanitas.evelyn.core.impl.ref;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
final class EvelynPriceIndexCalculator {
|
||||
static @NotNull BigDecimal calculateExpectedPrice(@NotNull Instant samplingInstant) {
|
||||
Objects.requireNonNull(samplingInstant, "samplingInstant");
|
||||
if (samplingInstant.isBefore(EXPECTED_PRICE_START)) {
|
||||
throw new IllegalArgumentException(
|
||||
"samplingInstant is before the expected-price start: " + samplingInstant
|
||||
);
|
||||
}
|
||||
|
||||
Duration elapsed = Duration.between(EXPECTED_PRICE_START, samplingInstant);
|
||||
BigDecimal elapsedSeconds = BigDecimal.valueOf(elapsed.getSeconds()).add(
|
||||
BigDecimal.valueOf(elapsed.getNano(), 9),
|
||||
MATH_CONTEXT
|
||||
);
|
||||
BigDecimal elapsedYears = elapsedSeconds.divide(SECONDS_PER_YEAR, MATH_CONTEXT);
|
||||
double growthFactor = Math.pow(
|
||||
ANNUAL_GROWTH_FACTOR.doubleValue(),
|
||||
elapsedYears.doubleValue()
|
||||
);
|
||||
|
||||
BigDecimal result = START_PRICE.multiply(BigDecimal.valueOf(growthFactor), MATH_CONTEXT);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static @NotNull BigDecimal calculateIndex(
|
||||
@NotNull BigDecimal actualPrice,
|
||||
@NotNull Instant samplingInstant
|
||||
) {
|
||||
Objects.requireNonNull(actualPrice, "actualPrice");
|
||||
if (actualPrice.signum() < 0) {
|
||||
throw new IllegalArgumentException("actualPrice must not be negative");
|
||||
}
|
||||
|
||||
BigDecimal expectedPrice = calculateExpectedPrice(samplingInstant);
|
||||
BigDecimal result = actualPrice.divide(expectedPrice, MATH_CONTEXT)
|
||||
.subtract(BigDecimal.ONE, MATH_CONTEXT)
|
||||
.multiply(INDEX_SCALE, MATH_CONTEXT);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static final Instant EXPECTED_PRICE_START = Instant.parse("2026-07-31T22:00:00Z");
|
||||
static final MathContext MATH_CONTEXT = MathContext.DECIMAL128;
|
||||
|
||||
private static final BigDecimal START_PRICE = new BigDecimal("15.00");
|
||||
private static final BigDecimal ANNUAL_GROWTH_FACTOR = new BigDecimal("1.20");
|
||||
private static final BigDecimal SECONDS_PER_YEAR = new BigDecimal("31536000");
|
||||
private static final BigDecimal INDEX_SCALE = BigDecimal.TEN;
|
||||
|
||||
private EvelynPriceIndexCalculator() {
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import com.fanitas.evelyn.core.Evelyn;
|
||||
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
|
||||
import com.r35157.evelyn.emc.EvelynMissionControl;
|
||||
import com.r35157.libs.javafx.JavaFxRuntime;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
@@ -13,6 +16,7 @@ import javafx.scene.control.TabPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
@@ -27,6 +31,7 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
public EvelynMissionControlImpl(Evelyn evelynProd, Evelyn evelynTest) {
|
||||
this.evelynProd = Objects.requireNonNull(evelynProd);
|
||||
this.evelynTest = Objects.requireNonNull(evelynTest);
|
||||
refreshTimeline.setCycleCount(Timeline.INDEFINITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,15 +40,26 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
}
|
||||
|
||||
private void showWindow() {
|
||||
Stage window = new Stage();
|
||||
window.setTitle("Evelyn Mission Control");
|
||||
if (window != null && window.isShowing()) {
|
||||
window.toFront();
|
||||
return;
|
||||
}
|
||||
|
||||
if (window == null) {
|
||||
window = new Stage();
|
||||
window.setTitle("Evelyn Mission Control");
|
||||
window.setOnHidden(event -> refreshTimeline.stop());
|
||||
}
|
||||
window.setScene(new Scene(createRootTabs(), 600, 400));
|
||||
|
||||
refreshCharts();
|
||||
window.show();
|
||||
refreshTimeline.playFromStart();
|
||||
}
|
||||
|
||||
private TabPane createMissionControlTabs(String color, List<EvelynStatusIndexPoint> statusIndexHistory) {
|
||||
private TabPane createMissionControlTabs(String color, StatusIndexChart statusIndexChart) {
|
||||
TabPane tabs = new TabPane(
|
||||
createOverviewTab(color, statusIndexHistory),
|
||||
createOverviewTab(color, statusIndexChart),
|
||||
createMissionControlTab("Portfolio", color),
|
||||
createMissionControlTab("Perps", color),
|
||||
createMissionControlTab("Spot", color),
|
||||
@@ -57,8 +73,8 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
private Tab createOverviewTab(String color, List<EvelynStatusIndexPoint> statusIndexHistory) {
|
||||
StackPane content = new StackPane(createStatusIndexChart(statusIndexHistory));
|
||||
private Tab createOverviewTab(String color, StatusIndexChart statusIndexChart) {
|
||||
StackPane content = new StackPane(statusIndexChart.chart());
|
||||
content.setStyle("-fx-background-color: " + color + ";");
|
||||
|
||||
Tab tab = new Tab("Overview", content);
|
||||
@@ -66,24 +82,13 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
return tab;
|
||||
}
|
||||
|
||||
private LineChart<Number, Number> createStatusIndexChart(List<EvelynStatusIndexPoint> history) {
|
||||
long min = history.stream()
|
||||
.mapToLong(point -> point.timestamp().toEpochMilli())
|
||||
.min()
|
||||
.orElse(0L);
|
||||
|
||||
long max = history.stream()
|
||||
.mapToLong(point -> point.timestamp().toEpochMilli())
|
||||
.max()
|
||||
.orElse(0L);
|
||||
|
||||
double padding = 0;
|
||||
NumberAxis xAxis = new NumberAxis(min - padding, max + padding, (max - min) / 4.0);
|
||||
static StatusIndexChart createStatusIndexChart() {
|
||||
NumberAxis xAxis = new NumberAxis(0, 1, 1);
|
||||
xAxis.setLabel("Time");
|
||||
xAxis.setTickLabelFormatter(TIMESTAMP_FORMATTER);
|
||||
xAxis.setForceZeroInRange(false);
|
||||
|
||||
double yBound = calculateYBound(history);
|
||||
double yBound = EMPTY_Y_BOUND;
|
||||
NumberAxis yAxis = new NumberAxis(-yBound, yBound, yBound / 5.0);
|
||||
yAxis.setLabel("Index value");
|
||||
yAxis.setForceZeroInRange(true);
|
||||
@@ -92,16 +97,21 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
chart.setTitle("Evelyn Status Index History");
|
||||
chart.setCreateSymbols(false);
|
||||
chart.setAnimated(false);
|
||||
chart.getData().add(createSeries("Evelyn Price Index", history, EvelynStatusIndexPoint::evelynPriceIndex));
|
||||
chart.getData().add(createSeries("EVE_SYRUP Pool Depth Index", history, EvelynStatusIndexPoint::eveSyrupPoolDepthIndex));
|
||||
chart.getData().add(createSeries("EVE_SYRUP Pool Balance Index", history, EvelynStatusIndexPoint::eveSyrupPoolBalanceIndex));
|
||||
chart.getData().add(createSeries("AAZDKK_USDT Pool Balance Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolBalanceIndex));
|
||||
chart.getData().add(createSeries("AAZDKK_USDT Pool Price Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolPriceIndex));
|
||||
chart.getData().add(createSeries("AAZDKK_USDT Pool Depth Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolDepthIndex));
|
||||
return chart;
|
||||
XYChart.Series<Number, Number> priceIndexSeries = createSeries(
|
||||
"Evelyn Price Index",
|
||||
List.of(),
|
||||
EvelynStatusIndexPoint::evelynPriceIndex
|
||||
);
|
||||
chart.getData().add(priceIndexSeries);
|
||||
//chart.getData().add(createSeries("EVE_SYRUP Pool Depth Index", history, EvelynStatusIndexPoint::eveSyrupPoolDepthIndex));
|
||||
//chart.getData().add(createSeries("EVE_SYRUP Pool Balance Index", history, EvelynStatusIndexPoint::eveSyrupPoolBalanceIndex));
|
||||
//chart.getData().add(createSeries("AAZDKK_USDT Pool Balance Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolBalanceIndex));
|
||||
//chart.getData().add(createSeries("AAZDKK_USDT Pool Price Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolPriceIndex));
|
||||
//chart.getData().add(createSeries("AAZDKK_USDT Pool Depth Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolDepthIndex));
|
||||
return new StatusIndexChart(chart, xAxis, yAxis, priceIndexSeries);
|
||||
}
|
||||
|
||||
private XYChart.Series<Number, Number> createSeries(
|
||||
private static XYChart.Series<Number, Number> createSeries(
|
||||
String name,
|
||||
List<EvelynStatusIndexPoint> history,
|
||||
Function<EvelynStatusIndexPoint, BigDecimal> valueExtractor
|
||||
@@ -117,25 +127,88 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
return series;
|
||||
}
|
||||
|
||||
private double calculateYBound(List<EvelynStatusIndexPoint> history) {
|
||||
static double calculateYBound(List<EvelynStatusIndexPoint> history) {
|
||||
BigDecimal maxAbsoluteValue = BigDecimal.ZERO;
|
||||
for (EvelynStatusIndexPoint point : history) {
|
||||
maxAbsoluteValue = max(maxAbsoluteValue, point.evelynPriceIndex());
|
||||
maxAbsoluteValue = max(maxAbsoluteValue, point.eveSyrupPoolDepthIndex());
|
||||
maxAbsoluteValue = max(maxAbsoluteValue, point.eveSyrupPoolBalanceIndex());
|
||||
maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolBalanceIndex());
|
||||
maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolPriceIndex());
|
||||
maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolDepthIndex());
|
||||
//maxAbsoluteValue = max(maxAbsoluteValue, point.eveSyrupPoolDepthIndex());
|
||||
//maxAbsoluteValue = max(maxAbsoluteValue, point.eveSyrupPoolBalanceIndex());
|
||||
//maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolBalanceIndex());
|
||||
//maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolPriceIndex());
|
||||
//maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolDepthIndex());
|
||||
}
|
||||
|
||||
double bound = maxAbsoluteValue.doubleValue();
|
||||
return bound == 0.0 ? EMPTY_Y_BOUND : bound;
|
||||
}
|
||||
|
||||
private BigDecimal max(BigDecimal currentMaximum, BigDecimal candidate) {
|
||||
private static BigDecimal max(BigDecimal currentMaximum, BigDecimal candidate) {
|
||||
return currentMaximum.max(candidate.abs());
|
||||
}
|
||||
|
||||
static void refreshChart(
|
||||
StatusIndexChart statusIndexChart,
|
||||
List<EvelynStatusIndexPoint> history
|
||||
) {
|
||||
if (!Platform.isFxApplicationThread()) {
|
||||
throw new IllegalStateException("Status-index charts must be updated on the JavaFX Application Thread");
|
||||
}
|
||||
|
||||
int commonPointCount = 0;
|
||||
int maximumCommonPointCount = Math.min(
|
||||
history.size(),
|
||||
statusIndexChart.renderedHistory.size()
|
||||
);
|
||||
while (commonPointCount < maximumCommonPointCount
|
||||
&& history.get(commonPointCount).equals(
|
||||
statusIndexChart.renderedHistory.get(commonPointCount)
|
||||
)) {
|
||||
commonPointCount++;
|
||||
}
|
||||
if (commonPointCount < statusIndexChart.renderedHistory.size()) {
|
||||
statusIndexChart.priceIndexSeries().getData().clear();
|
||||
statusIndexChart.renderedHistory = List.of();
|
||||
commonPointCount = 0;
|
||||
}
|
||||
for (int index = commonPointCount; index < history.size(); index++) {
|
||||
EvelynStatusIndexPoint point = history.get(index);
|
||||
statusIndexChart.priceIndexSeries().getData().add(new XYChart.Data<>(
|
||||
point.timestamp().toEpochMilli(),
|
||||
point.evelynPriceIndex()
|
||||
));
|
||||
}
|
||||
statusIndexChart.renderedHistory = List.copyOf(history);
|
||||
updateAxisBounds(statusIndexChart, history);
|
||||
}
|
||||
|
||||
private static void updateAxisBounds(
|
||||
StatusIndexChart statusIndexChart,
|
||||
List<EvelynStatusIndexPoint> history
|
||||
) {
|
||||
long minimumTimestamp;
|
||||
long maximumTimestamp;
|
||||
if (history.isEmpty()) {
|
||||
minimumTimestamp = 0;
|
||||
maximumTimestamp = 1;
|
||||
} else {
|
||||
minimumTimestamp = history.getFirst().timestamp().toEpochMilli();
|
||||
maximumTimestamp = history.getLast().timestamp().toEpochMilli();
|
||||
if (minimumTimestamp == maximumTimestamp) {
|
||||
minimumTimestamp -= SINGLE_POINT_X_PADDING_MILLIS;
|
||||
maximumTimestamp += SINGLE_POINT_X_PADDING_MILLIS;
|
||||
}
|
||||
}
|
||||
double xTickUnit = Math.max(1, (maximumTimestamp - minimumTimestamp) / 4.0);
|
||||
statusIndexChart.xAxis().setLowerBound(minimumTimestamp);
|
||||
statusIndexChart.xAxis().setUpperBound(maximumTimestamp);
|
||||
statusIndexChart.xAxis().setTickUnit(xTickUnit);
|
||||
|
||||
double yBound = calculateYBound(history);
|
||||
statusIndexChart.yAxis().setLowerBound(-yBound);
|
||||
statusIndexChart.yAxis().setUpperBound(yBound);
|
||||
statusIndexChart.yAxis().setTickUnit(yBound / 5.0);
|
||||
}
|
||||
|
||||
private Tab createMissionControlTab(String title, String color) {
|
||||
StackPane content = new StackPane();
|
||||
content.setStyle("-fx-background-color: " + color + ";");
|
||||
@@ -147,18 +220,18 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
}
|
||||
|
||||
private TabPane createRootTabs() {
|
||||
List<EvelynStatusIndexPoint> statusIndexHistoryProd = List.copyOf(evelynProd.getStatusIndexHistory());
|
||||
List<EvelynStatusIndexPoint> statusIndexHistoryTest = List.copyOf(evelynTest.getStatusIndexHistory());
|
||||
productionChart = createStatusIndexChart();
|
||||
testChart = createStatusIndexChart();
|
||||
|
||||
Tab productionTab = new Tab(
|
||||
"Production",
|
||||
createMissionControlTabs(PRODUCTION_COLOR, statusIndexHistoryProd)
|
||||
createMissionControlTabs(PRODUCTION_COLOR, productionChart)
|
||||
);
|
||||
productionTab.setStyle("-fx-background-color: " + PRODUCTION_COLOR + ";");
|
||||
|
||||
Tab testTab = new Tab(
|
||||
"Test",
|
||||
createMissionControlTabs(TEST_COLOR, statusIndexHistoryTest)
|
||||
createMissionControlTabs(TEST_COLOR, testChart)
|
||||
);
|
||||
testTab.setStyle("-fx-background-color: " + TEST_COLOR + ";");
|
||||
|
||||
@@ -168,9 +241,52 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
return rootTabs;
|
||||
}
|
||||
|
||||
private void refreshCharts() {
|
||||
refreshChart(productionChart, evelynProd.getStatusIndexHistory());
|
||||
refreshChart(testChart, evelynTest.getStatusIndexHistory());
|
||||
}
|
||||
|
||||
static final class StatusIndexChart {
|
||||
private StatusIndexChart(
|
||||
LineChart<Number, Number> chart,
|
||||
NumberAxis xAxis,
|
||||
NumberAxis yAxis,
|
||||
XYChart.Series<Number, Number> priceIndexSeries
|
||||
) {
|
||||
this.chart = chart;
|
||||
this.xAxis = xAxis;
|
||||
this.yAxis = yAxis;
|
||||
this.priceIndexSeries = priceIndexSeries;
|
||||
}
|
||||
|
||||
LineChart<Number, Number> chart() {
|
||||
return chart;
|
||||
}
|
||||
|
||||
NumberAxis xAxis() {
|
||||
return xAxis;
|
||||
}
|
||||
|
||||
NumberAxis yAxis() {
|
||||
return yAxis;
|
||||
}
|
||||
|
||||
XYChart.Series<Number, Number> priceIndexSeries() {
|
||||
return priceIndexSeries;
|
||||
}
|
||||
|
||||
private final LineChart<Number, Number> chart;
|
||||
private final NumberAxis xAxis;
|
||||
private final NumberAxis yAxis;
|
||||
private final XYChart.Series<Number, Number> priceIndexSeries;
|
||||
private List<EvelynStatusIndexPoint> renderedHistory = List.of();
|
||||
}
|
||||
|
||||
private static final String PRODUCTION_COLOR = "#ffd6d6";
|
||||
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 SINGLE_POINT_X_PADDING_MILLIS = 30_000;
|
||||
private static final DateTimeFormatter TIMESTAMP_TICK_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("HH:mm dd/MM/yy")
|
||||
.withZone(ZoneId.of("Europe/Copenhagen"));
|
||||
@@ -188,4 +304,11 @@ public final class EvelynMissionControlImpl implements EvelynMissionControl {
|
||||
|
||||
private final Evelyn evelynProd;
|
||||
private final Evelyn evelynTest;
|
||||
private final Timeline refreshTimeline = new Timeline(new KeyFrame(
|
||||
Duration.seconds(CHART_REFRESH_SECONDS),
|
||||
event -> refreshCharts()
|
||||
));
|
||||
private Stage window;
|
||||
private StatusIndexChart productionChart;
|
||||
private StatusIndexChart testChart;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.r35157.nenjim.hubd.impl.ref;
|
||||
import com.fanitas.evelyn.core.Evelyn;
|
||||
import com.fanitas.evelyn.core.impl.ref.EvelynImpl;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.PriceSource;
|
||||
import com.r35157.assetaz.services.ticker.TickerService;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.impl.hardcoded.HardcodedPriceSource;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.impl.raydiumpool.RaydiumPoolPriceSource;
|
||||
import com.r35157.assetaz.services.ticker.impl.ref.TickerServiceImpl;
|
||||
@@ -78,12 +79,15 @@ public class NenjimHubImpl implements NenjimHub {
|
||||
//PriceSource hardcodedPriceSource = new HardcodedPriceSource(cis);
|
||||
//PriceSource raydiumPoolPriceSource = createEVEUSDTPriceSource(cis, raydium);
|
||||
|
||||
//startAssetAZTickerService(hardcodedPriceSource, raydiumPoolPriceSource);
|
||||
//TickerService tickerService = startAssetAZTickerService(raydiumPoolPriceSource);
|
||||
|
||||
//startJupiterPerpsAlarm(cis);
|
||||
|
||||
//Evelyn evelynProd = new EvelynImpl();
|
||||
//Evelyn evelynTest = new EvelynImpl();
|
||||
//TradingPair eveUsdt = createEVEUSDTTradingPair(cis);
|
||||
//Evelyn evelynProd = new EvelynImpl(tickerService, eveUsdt);
|
||||
//Evelyn evelynTest = new EvelynImpl(tickerService, eveUsdt);
|
||||
//evelynProd.start();
|
||||
//evelynTest.start();
|
||||
//startEvelynMissionControl(evelynProd, evelynTest);
|
||||
//startNenjimComposer();
|
||||
//startNenjimProcessManager();
|
||||
@@ -110,16 +114,20 @@ public class NenjimHubImpl implements NenjimHub {
|
||||
}
|
||||
|
||||
private PriceSource createEVEUSDTPriceSource(CurrencyIdentityService cis, Raydium raydium) {
|
||||
TradingPair eveUsdt = new TradingPair(cis.resolve(EVE_ID), cis.resolve(USDT_ID));
|
||||
TradingPair eveUsdt = createEVEUSDTTradingPair(cis);
|
||||
PriceSource priceSource = new RaydiumPoolPriceSource(raydium, EVE_USDT_RAYDIUM_POOL_ID, eveUsdt);
|
||||
|
||||
return priceSource;
|
||||
}
|
||||
|
||||
private void startAssetAZTickerService(PriceSource... priceSources) {
|
||||
TickerServiceImpl tickerService = new TickerServiceImpl(priceSources);
|
||||
private TradingPair createEVEUSDTTradingPair(CurrencyIdentityService cis) {
|
||||
return new TradingPair(cis.resolve(EVE_ID), cis.resolve(USDT_ID));
|
||||
}
|
||||
|
||||
private TickerService startAssetAZTickerService(PriceSource... priceSources) {
|
||||
TickerService tickerService = new TickerServiceImpl(priceSources);
|
||||
tickerService.start();
|
||||
return tickerService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user