88: Rename Evelyn to EvelynStatusService and expose the calculated reference price

This commit is contained in:
2026-10-04 19:42:21 +02:00
parent 035a01c48c
commit 9c636bc93d
21 changed files with 788 additions and 111 deletions
@@ -1,41 +0,0 @@
package com.fanitas.evelyn.core;
import com.r35157.nenjim.component.NenjimComponent;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Collects and exposes the status-index history for one named Evelyn instance.
*/
public interface Evelyn extends NenjimComponent, AutoCloseable {
/**
* Executes the legacy Evelyn service operation.
*
* @throws Exception when service execution fails
*/
void executeService() throws Exception;
/**
* Restores persisted history and starts status-index sampling.
*/
void start();
/**
* Stops sampling and clears memory while retaining persistent ownership.
*/
void stop();
/**
* Permanently closes this instance and releases its persistent name.
*/
@Override
void close();
/**
* Returns an immutable snapshot containing only durably persisted points.
*
* @return complete current history ordered from oldest to newest
*/
@NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory();
}
@@ -1,6 +1,6 @@
package com.r35157.evelyn.emc.impl.ref;
import com.fanitas.evelyn.core.Evelyn;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.evelyn.emc.EvelynMissionControl;
import com.r35157.nenjim.process.NenjimProcess;
@@ -33,12 +33,12 @@ public final class EvelynMissionControlImpl implements
NenjimProcess {
public EvelynMissionControlImpl(
@NotNull Evelyn evelynServiceProd,
@NotNull Evelyn evelynServiceTest,
@NotNull EvelynStatusService evelynStatusServiceProd,
@NotNull EvelynStatusService evelynStatusServiceTest,
@NotNull JavaFxRuntimeService javaFxRuntimeService
) {
this.evelynProd = Objects.requireNonNull(evelynServiceProd, "evelynServiceProd");
this.evelynTest = Objects.requireNonNull(evelynServiceTest, "evelynServiceTest");
this.evelynStatusProd = Objects.requireNonNull(evelynStatusServiceProd, "evelynStatusServiceProd");
this.evelynStatusTest = Objects.requireNonNull(evelynStatusServiceTest, "evelynStatusServiceTest");
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
@@ -253,8 +253,8 @@ public final class EvelynMissionControlImpl implements
}
private void refreshCharts() {
refreshChart(productionChart, evelynProd.getStatusIndexHistory());
refreshChart(testChart, evelynTest.getStatusIndexHistory());
refreshChart(productionChart, evelynStatusProd.getStatusIndexHistory());
refreshChart(testChart, evelynStatusTest.getStatusIndexHistory());
}
static final class StatusIndexChart {
@@ -313,8 +313,8 @@ public final class EvelynMissionControlImpl implements
}
};
private final Evelyn evelynProd;
private final Evelyn evelynTest;
private final EvelynStatusService evelynStatusProd;
private final EvelynStatusService evelynStatusTest;
private final JavaFxRuntimeService javaFxRuntimeService;
private final Timeline refreshTimeline = new Timeline(new KeyFrame(
Duration.seconds(CHART_REFRESH_SECONDS),
@@ -0,0 +1,59 @@
package com.r35157.evelyn.service.status;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.libs.valuetypes.basic.AssetPrice;
import com.r35157.nenjim.component.NenjimComponent;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.List;
/**
* Collects status-index history and exposes the calculated reference price
* for one named Evelyn instance.
*/
public interface EvelynStatusService extends NenjimComponent, AutoCloseable {
/**
* Executes the legacy Evelyn service operation.
*
* @throws Exception when service execution fails
*/
void executeService() throws Exception;
/**
* Restores persisted history and starts status-index sampling.
*/
void start();
/**
* Stops sampling and clears memory while retaining persistent ownership.
*/
void stop();
/**
* Permanently closes this instance and releases its persistent name.
*/
@Override
void close();
/**
* Returns an immutable snapshot containing only durably persisted points.
*
* @return complete current history ordered from oldest to newest
*/
@NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory();
/**
* Calculates the EVE reference price for the supplied instant, quoted in
* the service's canonical EVE/USDT trading pair. The curve starts at 15.00
* on 2026-07-31T22:00:00Z and grows by a factor of 1.20 per 365-day year.
* This operation works before sampling starts and while sampling is stopped;
* it does not access market prices or history storage or mutate history.
*
* @param timestamp instant for which to calculate the reference price
* @return calculated reference price with its explicit trading pair
* @throws NullPointerException if the timestamp is null
* @throws IllegalArgumentException if the timestamp precedes the curve start
*/
@NotNull AssetPrice getReferencePrice(@NotNull Instant timestamp);
}
@@ -1,4 +1,4 @@
package com.fanitas.evelyn.core.impl.ref;
package com.r35157.evelyn.service.status.impl.ref;
import org.jetbrains.annotations.NotNull;
@@ -1,8 +1,9 @@
package com.fanitas.evelyn.core.impl.ref;
package com.r35157.evelyn.service.status.impl.ref;
import com.fanitas.evelyn.core.*;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.libs.basic.Pair;
import com.r35157.libs.basic.Triblet;
import com.r35157.libs.raydium.Raydium;
@@ -46,9 +47,9 @@ import java.util.concurrent.ConcurrentHashMap;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
public class EvelynImpl implements Evelyn, NenjimProcess {
public class EvelynStatusServiceImpl implements EvelynStatusService, NenjimProcess {
public EvelynImpl(
public EvelynStatusServiceImpl(
@NotNull String instanceName,
@NotNull TickerService tickerService,
@NotNull TradingPair eveUsdtTradingPair,
@@ -65,7 +66,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
);
}
EvelynImpl(
EvelynStatusServiceImpl(
@NotNull String instanceName,
@NotNull TickerService tickerService,
@NotNull TradingPair eveUsdtTradingPair,
@@ -97,7 +98,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
.normalize();
if (!validatedDataRoot.equals(validatedInstancePath.getParent())) {
throw new IllegalArgumentException(
"Evelyn instance name must resolve to a direct child of "
"EvelynStatusService instance name must resolve to a direct child of "
+ validatedDataRoot + ": " + normalizedInstanceName
);
}
@@ -108,7 +109,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
if (!RESERVED_INSTANCE_NAMES.add(reservationKey)) {
throw new IllegalArgumentException(
"Evelyn instance name is already reserved in this JVM: "
"EvelynStatusService instance name is already reserved in this JVM: "
+ normalizedInstanceName
);
}
@@ -135,13 +136,13 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
public synchronized void start() {
if (closeRequested || lifecycleState == LifecycleState.CLOSED) {
throw new IllegalStateException(
"Closing or closed Evelyn instance cannot be started: "
"Closing or closed EvelynStatusService instance cannot be started: "
+ instanceName
);
}
if (lifecycleState != LifecycleState.STOPPED) {
throw new IllegalStateException(
"Evelyn instance cannot start while in state "
"EvelynStatusService instance cannot start while in state "
+ lifecycleState + ": " + instanceName
);
}
@@ -154,7 +155,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
clearStatusIndexHistory();
lifecycleState = LifecycleState.STOPPED;
throw new IllegalStateException(
"Could not start Evelyn instance '" + instanceName
"Could not start EvelynStatusService instance '" + instanceName
+ "' from " + statusHistoryPath + ": "
+ exception.getMessage(),
exception
@@ -177,7 +178,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
clearStatusIndexHistory();
lifecycleState = LifecycleState.STOPPED;
throw new IllegalStateException(
"Could not create Evelyn sampling executor for instance '"
"Could not create EvelynStatusService sampling executor for instance '"
+ instanceName + "'",
exception
);
@@ -203,7 +204,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
clearStatusIndexHistory();
lifecycleState = LifecycleState.STOPPED;
throw new IllegalStateException(
"Could not start Evelyn sampling for instance '"
"Could not start EvelynStatusService sampling for instance '"
+ instanceName + "'",
exception
);
@@ -222,7 +223,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
if (lifecycleState != LifecycleState.STARTED
&& lifecycleState != LifecycleState.TERMINATION_FAILED) {
throw new IllegalStateException(
"Evelyn instance cannot stop while in state "
"EvelynStatusService instance cannot stop while in state "
+ lifecycleState + ": " + instanceName
);
}
@@ -244,7 +245,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
lifecycleState = LifecycleState.TERMINATION_FAILED;
}
throw new IllegalStateException(
"Interrupted while stopping Evelyn instance '"
"Interrupted while stopping EvelynStatusService instance '"
+ instanceName + "'; name reservation retained",
exception
);
@@ -255,7 +256,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
lifecycleState = LifecycleState.TERMINATION_FAILED;
}
throw new IllegalStateException(
"Evelyn sampling thread did not terminate for instance '"
"EvelynStatusService sampling thread did not terminate for instance '"
+ instanceName + "'; name reservation retained"
);
}
@@ -282,7 +283,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
if (lifecycleState != LifecycleState.STOPPED
|| statusIndexScheduler != null) {
throw new IllegalStateException(
"Cannot close Evelyn instance while sampling termination "
"Cannot close EvelynStatusService instance while sampling termination "
+ "is unconfirmed: " + instanceName
);
}
@@ -298,6 +299,14 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
}
}
@Override
public @NotNull AssetPrice getReferencePrice(@NotNull Instant timestamp) {
return new AssetPrice(
EvelynPriceIndexCalculator.calculateExpectedPrice(timestamp),
eveUsdtTradingPair
);
}
private void clearStatusIndexHistory() {
synchronized (statusIndexHistory) {
statusIndexHistory.clear();
@@ -686,7 +695,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
private static String validateInstanceName(String instanceName) {
Objects.requireNonNull(instanceName, "instanceName");
String normalizedName = Normalizer.normalize(instanceName, Normalizer.Form.NFC);
if (normalizedName.codePoints().allMatch(EvelynImpl::isWhitespace)
if (normalizedName.codePoints().allMatch(EvelynStatusServiceImpl::isWhitespace)
|| hasWhitespaceAtBoundary(normalizedName)
|| normalizedName.equals(".")
|| normalizedName.equals("..")
@@ -698,7 +707,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
)
|| isWindowsReservedFilename(normalizedName)) {
throw new IllegalArgumentException(
"Unsafe Evelyn instance name: " + normalizedName
"Unsafe EvelynStatusService instance name: " + normalizedName
);
}
return normalizedName;
@@ -734,7 +743,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
return false;
}
private static final Logger log = LoggerFactory.getLogger(EvelynImpl.class);
private static final Logger log = LoggerFactory.getLogger(EvelynStatusServiceImpl.class);
private static final Path DATA_ROOT = Path.of("data", "evelyn");
private static final String STATUS_HISTORY_FILENAME = "status.log";
private static final String FORMAT_VERSION_KEY = "FORMAT_VERSION";
@@ -3,8 +3,8 @@ package com.r35157.nenjim.hubd.impl.ref;
import com.r35157.assetaz.services.journal.impl.ref.AssetAZJournalServiceImpl;
import com.r35157.assetaz.services.journal.codec.impl.ref.AssetOwnerRegisteredCodec;
import com.r35157.assetaz.services.journal.codec.impl.ref.JournalFormatVersionChangedCodec;
import com.fanitas.evelyn.core.Evelyn;
import com.fanitas.evelyn.core.impl.ref.EvelynImpl;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.evelyn.service.status.impl.ref.EvelynStatusServiceImpl;
import com.fanitas.evelyn.service.burner.EvelynIOUBurnerService;
import com.fanitas.evelyn.service.burner.impl.ref.EvelynIOUBurnerServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -274,29 +274,29 @@ public final class NenjimHubImpl implements NenjimHub {
return process;
}
private NenjimProcess constructEvelynProdProcess() {
EvelynImpl process = new EvelynImpl(
private NenjimProcess constructEvelynStatusProdProcess() {
EvelynStatusServiceImpl process = new EvelynStatusServiceImpl(
"Production",
requireProcessDependency(
tickerService,
EVELYN_SERVICE_PROD_ID,
EVELYN_STATUS_PROD_ID,
TICKER_SERVICE_ID),
eveUSDT,
clockUTC);
evelynServiceProd = process;
evelynStatusServiceProd = process;
return process;
}
private NenjimProcess constructEvelynTestProcess() {
EvelynImpl process = new EvelynImpl(
private NenjimProcess constructEvelynStatusTestProcess() {
EvelynStatusServiceImpl process = new EvelynStatusServiceImpl(
"Test",
requireProcessDependency(
tickerService,
EVELYN_SERVICE_TEST_ID,
EVELYN_STATUS_TEST_ID,
TICKER_SERVICE_ID),
eveUSDT,
clockUTC);
evelynServiceTest = process;
evelynStatusServiceTest = process;
return process;
}
@@ -335,13 +335,13 @@ public final class NenjimHubImpl implements NenjimHub {
private NenjimProcess constructMissionControlProcess() {
EvelynMissionControlImpl process = new EvelynMissionControlImpl(
requireProcessDependency(
evelynServiceProd,
evelynStatusServiceProd,
EVELYN_MISSION_CONTROL_ID,
EVELYN_SERVICE_PROD_ID),
EVELYN_STATUS_PROD_ID),
requireProcessDependency(
evelynServiceTest,
evelynStatusServiceTest,
EVELYN_MISSION_CONTROL_ID,
EVELYN_SERVICE_TEST_ID),
EVELYN_STATUS_TEST_ID),
requireProcessDependency(
javaFxRuntimeService,
EVELYN_MISSION_CONTROL_ID,
@@ -385,8 +385,8 @@ public final class NenjimHubImpl implements NenjimHub {
registryService, Path.of("data/assetaz/journal/events.pde"))),
Map.entry(TICKER_SERVICE_ID, this::constructTickerProcess),
Map.entry(JUPITER_PERPS_ALARM_ID, this::constructJupiterPerpsAlarmProcess),
Map.entry(EVELYN_SERVICE_PROD_ID, this::constructEvelynProdProcess),
Map.entry(EVELYN_SERVICE_TEST_ID, this::constructEvelynTestProcess),
Map.entry(EVELYN_STATUS_PROD_ID, this::constructEvelynStatusProdProcess),
Map.entry(EVELYN_STATUS_TEST_ID, this::constructEvelynStatusTestProcess),
Map.entry(EVELYN_IOU_BURNER_PROD_ID, this::constructEvelynIOUBurnerProdProcess),
Map.entry(JAVAFX_RUNTIME_SERVICE_ID, this::constructJavaFxRuntimeProcess),
Map.entry(EVELYN_MISSION_CONTROL_ID, this::constructMissionControlProcess),
@@ -491,10 +491,10 @@ public final class NenjimHubImpl implements NenjimHub {
new NenjimComponentId("jupiter.perps.evelyn-test");
private static final NenjimComponentId JUPITER_PERPS_ALARM_ID =
new NenjimComponentId("jupiter-perps-alarm.default");
private static final NenjimComponentId EVELYN_SERVICE_PROD_ID =
new NenjimComponentId("evelyn.service.prod");
private static final NenjimComponentId EVELYN_SERVICE_TEST_ID =
new NenjimComponentId("evelyn.service.test");
private static final NenjimComponentId EVELYN_STATUS_PROD_ID =
new NenjimComponentId("evelyn.status.prod");
private static final NenjimComponentId EVELYN_STATUS_TEST_ID =
new NenjimComponentId("evelyn.status.test");
private static final NenjimComponentId JUPITER_SWAP_EVELYN_BURNER_PROD_ID =
new NenjimComponentId("jupiter.swap.evelyn-burner-prod");
private static final NenjimComponentId DISCORD_ASSETAZ_TOKEN_NOTIFICATION_ID =
@@ -539,8 +539,8 @@ public final class NenjimHubImpl implements NenjimHub {
// Services
private TickerService tickerService;
private JupiterPerpsAlarm jupiterPerpsAlarm;
private Evelyn evelynServiceProd;
private Evelyn evelynServiceTest;
private EvelynStatusService evelynStatusServiceProd;
private EvelynStatusService evelynStatusServiceTest;
private JupiterSwapService jupiterSwapService;
private BoundNotificationService discordAssetAZTokenNotificationService;
private EvelynIOUBurnerService evelynIOUBurnerServiceProd;
@@ -0,0 +1,214 @@
package com.r35157.evelyn.service.status.impl.ref;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.assetaz.services.cis.impl.hc.HardcodedCurrencyIdentityService;
import com.r35157.assetaz.services.ticker.PriceObservation;
import com.r35157.assetaz.services.ticker.TickerService;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.libs.valuetypes.basic.AssetPrice;
import com.r35157.libs.valuetypes.basic.TradingPair;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.EVE_ID;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDT_ID;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EvelynStatusServiceImplTest {
@Test
void publicApiCalculatesRequestedCurveWithCanonicalTradingPair(@TempDir Path root) {
try (EvelynStatusService service = service("Curve", UNUSED_TICKER, UNUSED_CLOCK, root)) {
assertPrice("15", service.getReferencePrice(START));
assertPrice("18", service.getReferencePrice(START.plusSeconds(31_536_000)));
AssetPrice fractional = service.getReferencePrice(START.plusSeconds(1_566_494));
assertTrue(fractional.price().subtract(new BigDecimal("15.136464436276"))
.abs().compareTo(new BigDecimal("0.000000000001")) < 0);
assertSame(EVE_USDT, fractional.tradingPair());
assertTrue(service.getStatusIndexHistory().isEmpty());
assertTrue(Files.notExists(root.resolve("Curve")));
}
}
@Test
void publicApiRejectsNullAndPreStartInstantsWithoutSideEffects(@TempDir Path root) {
try (EvelynStatusService service = service("Invalid", UNUSED_TICKER, UNUSED_CLOCK, root)) {
assertThrows(NullPointerException.class, () -> service.getReferencePrice(null));
assertThrows(IllegalArgumentException.class,
() -> service.getReferencePrice(START.minusNanos(1)));
assertTrue(service.getStatusIndexHistory().isEmpty());
assertTrue(Files.notExists(root.resolve("Invalid")));
}
}
@Test
void referencePriceWorksWhileStoppedWithoutProvisionedStorage(@TempDir Path root) {
try (EvelynStatusService service = service("Stopped", UNUSED_TICKER, UNUSED_CLOCK, root)) {
service.stop();
assertPrice("18", service.getReferencePrice(START.plusSeconds(31_536_000)));
assertTrue(service.getStatusIndexHistory().isEmpty());
assertTrue(Files.notExists(root.resolve("Stopped")));
}
}
@Test
void samplingPersistsSharedIndexAndRestoresHistoryAcrossRestart(@TempDir Path root)
throws Exception {
Path history = provision(root, "History", "2");
String original = Files.readString(history);
try (EvelynStatusService service = service("History", ticker("16.5"), FIXED_CLOCK, root)) {
service.start();
List<EvelynStatusIndexPoint> snapshot = awaitHistory(service, 2);
assertDecimal("1", snapshot.get(1).evelynPriceIndex());
assertEquals(START, snapshot.get(1).timestamp());
assertZeroPlaceholders(snapshot.get(1));
assertThrows(UnsupportedOperationException.class, () -> snapshot.add(snapshot.getFirst()));
// Observing a published point must also observe its durable record.
String persisted = Files.readString(history);
assertTrue(persisted.startsWith(original + "\n"));
assertEquals(3, persisted.lines().count());
byte[] beforeRequest = Files.readAllBytes(history);
assertPrice("18", service.getReferencePrice(START.plusSeconds(31_536_000)));
assertEquals(snapshot, service.getStatusIndexHistory());
assertArrayEquals(beforeRequest, Files.readAllBytes(history));
service.stop();
assertTrue(service.getStatusIndexHistory().isEmpty());
assertEquals(2, snapshot.size());
assertArrayEquals(beforeRequest, Files.readAllBytes(history));
Files.delete(history);
assertPrice("15", service.getReferencePrice(START));
assertTrue(Files.notExists(history));
Files.write(history, beforeRequest);
service.start();
List<EvelynStatusIndexPoint> restored = awaitHistory(service, 3);
assertEquals(snapshot, restored.subList(0, 2));
assertDecimal("1", restored.get(2).evelynPriceIndex());
service.stop();
byte[] beforeClose = Files.readAllBytes(history);
service.close();
assertArrayEquals(beforeClose, Files.readAllBytes(history));
assertThrows(IllegalStateException.class, service::start);
}
}
@Test
void productionAndTestKeepIndependentNamedHistories(@TempDir Path root) throws Exception {
Path productionFile = provision(root, "Production", "2");
Path testFile = provision(root, "Test", "-2");
try (EvelynStatusService production = service("Production", ticker("16.5"), FIXED_CLOCK, root);
EvelynStatusService test = service("Test", ticker("13.5"), FIXED_CLOCK, root)) {
production.start();
test.start();
List<EvelynStatusIndexPoint> productionHistory = awaitHistory(production, 2);
List<EvelynStatusIndexPoint> testHistory = awaitHistory(test, 2);
assertDecimal("2", productionHistory.getFirst().evelynPriceIndex());
assertDecimal("-2", testHistory.getFirst().evelynPriceIndex());
assertDecimal("1", productionHistory.get(1).evelynPriceIndex());
assertDecimal("-1", testHistory.get(1).evelynPriceIndex());
assertPrice("15", production.getReferencePrice(START));
assertPrice("15", test.getReferencePrice(START));
byte[] testBytes = Files.readAllBytes(testFile);
production.stop();
assertTrue(production.getStatusIndexHistory().isEmpty());
assertEquals(testHistory, test.getStatusIndexHistory());
assertArrayEquals(testBytes, Files.readAllBytes(testFile));
assertEquals(3, Files.readString(productionFile).lines().count());
}
}
private static EvelynStatusService service(String name, TickerService ticker, Clock clock, Path root) {
return new EvelynStatusServiceImpl(name, ticker, EVE_USDT, clock, 1, TimeUnit.DAYS, root);
}
private static TickerService ticker(String actualPrice) {
return pair -> {
assertSame(EVE_USDT, pair);
return new PriceObservation(new AssetPrice(new BigDecimal(actualPrice), pair), START, "offline");
};
}
private static Path provision(Path root, String name, String existingIndex) throws Exception {
Path history = Files.createDirectory(root.resolve(name)).resolve("status.log");
Files.writeString(history, "FORMAT_VERSION=1\n20260731215959000Z:1:" + existingIndex);
return history;
}
private static List<EvelynStatusIndexPoint> awaitHistory(EvelynStatusService service, int size)
throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
List<EvelynStatusIndexPoint> history;
do {
history = service.getStatusIndexHistory();
if (history.size() == size) {
return history;
}
Thread.sleep(10);
} while (System.nanoTime() < deadline);
assertEquals(size, history.size(), "Expected persisted sample was not published");
return history;
}
private static void assertDecimal(String expected, BigDecimal actual) {
assertEquals(0, new BigDecimal(expected).compareTo(actual));
}
private static void assertPrice(String expected, AssetPrice actual) {
assertDecimal(expected, actual.price());
assertSame(EVE_USDT, actual.tradingPair());
assertEquals(EVE_ID, actual.tradingPair().base().id());
assertEquals(USDT_ID, actual.tradingPair().quote().id());
}
private static void assertZeroPlaceholders(EvelynStatusIndexPoint point) {
assertEquals(BigDecimal.ZERO, point.eveSyrupPoolDepthIndex());
assertEquals(BigDecimal.ZERO, point.eveSyrupPoolBalanceIndex());
assertEquals(BigDecimal.ZERO, point.aazdkkUsdtPoolBalanceIndex());
assertEquals(BigDecimal.ZERO, point.aazdkkUsdtPoolPriceIndex());
assertEquals(BigDecimal.ZERO, point.aazdkkUsdtPoolDepthIndex());
}
private static final Instant START = Instant.parse("2026-07-31T22:00:00Z");
private static final Clock FIXED_CLOCK = Clock.fixed(START, ZoneOffset.UTC);
private static final TradingPair EVE_USDT;
static {
HardcodedCurrencyIdentityService currencies = new HardcodedCurrencyIdentityService();
EVE_USDT = new TradingPair(currencies.resolve(EVE_ID), currencies.resolve(USDT_ID));
}
private static final TickerService UNUSED_TICKER = pair -> {
throw new AssertionError("Reference-price requests must not fetch a market price");
};
private static final Clock UNUSED_CLOCK = new Clock() {
@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}
@Override
public Clock withZone(ZoneId zone) {
return this;
}
@Override
public Instant instant() {
throw new AssertionError("Reference-price requests must use the supplied instant");
}
};
}