88: Rename Evelyn to EvelynStatusService and expose the calculated reference price
This commit is contained in:
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package com.fanitas.evelyn.core.impl.ref;
|
||||
package com.r35157.evelyn.service.status.impl.ref;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
+28
-19
@@ -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;
|
||||
|
||||
+214
@@ -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");
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user