Files
quant-trader-service/src/test/java/com/quantai/trader/replay/TraderP0CycleRunnerTest.java
T

176 lines
7.8 KiB
Java
Raw Normal View History

2026-06-26 21:53:22 +08:00
package com.quantai.trader.replay;
import com.quantai.trader.artifact.TraderArtifactLoader;
2026-06-26 22:01:25 +08:00
import com.quantai.trader.domain.*;
2026-06-26 21:53:22 +08:00
import com.quantai.trader.enums.TraderActionType;
import com.quantai.trader.evidence.EvidenceAppender;
2026-06-26 22:01:25 +08:00
import com.quantai.trader.evidence.TraderEvidenceRepository;
2026-06-26 22:07:43 +08:00
import com.quantai.trader.model.ArtifactTraderModelService;
2026-06-26 22:01:25 +08:00
import com.quantai.trader.outbox.TraderOutboxEvent;
import com.quantai.trader.outbox.TraderOutboxRepository;
import com.quantai.trader.persistence.TraderDecisionTraceWriter;
2026-06-26 21:53:22 +08:00
import com.quantai.trader.position.TraderPositionManager;
2026-06-26 22:17:48 +08:00
import com.quantai.trader.replay.state.P0ReplayStateStore;
2026-06-26 21:53:22 +08:00
import com.quantai.trader.risk.TraderRiskGate;
import org.junit.jupiter.api.Test;
2026-06-26 22:07:43 +08:00
import org.junit.jupiter.api.io.TempDir;
2026-06-26 21:53:22 +08:00
2026-06-26 22:01:25 +08:00
import java.util.ArrayList;
import java.util.List;
2026-06-26 22:07:43 +08:00
import java.io.IOException;
2026-06-26 21:53:22 +08:00
import java.math.BigDecimal;
2026-06-26 22:07:43 +08:00
import java.nio.file.Path;
2026-06-26 21:53:22 +08:00
2026-06-26 22:07:43 +08:00
import static com.quantai.trader.TestFixtures.*;
2026-06-26 21:53:22 +08:00
import static org.assertj.core.api.Assertions.assertThat;
class TraderP0CycleRunnerTest {
2026-06-26 22:07:43 +08:00
@TempDir
Path artifactRoot;
2026-06-26 21:53:22 +08:00
@Test
2026-06-26 22:07:43 +08:00
void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() throws IOException {
writeArtifactBundle(artifactRoot);
2026-06-26 22:01:25 +08:00
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
2026-06-26 22:17:48 +08:00
var properties = propertiesWithArtifactRoot(artifactRoot);
2026-06-26 21:53:22 +08:00
TraderP0CycleRunner runner = new TraderP0CycleRunner(
2026-06-26 22:17:48 +08:00
properties,
new TraderArtifactLoader(properties, objectMapper()),
2026-06-26 22:07:43 +08:00
new ArtifactTraderModelService(),
2026-06-26 21:53:22 +08:00
new TraderPositionManager(),
new TraderRiskGate(),
new TraderActionFactory(),
evidenceAppender,
2026-06-26 22:01:25 +08:00
traceWriter,
2026-06-26 22:17:48 +08:00
outboxRepository,
new P0ReplayStateStore(properties));
2026-06-26 21:53:22 +08:00
2026-06-26 22:17:48 +08:00
TraderCycleResult result = runner.runCycle(new ReplayMarketEvent(
2026-06-26 21:53:22 +08:00
"run-1", "BTC-USDT-PERP", T0, new BigDecimal("100"), new BigDecimal("99.5"),
new BigDecimal("1.2"), new BigDecimal("1000")));
assertThat(result.action().actionType()).isEqualTo(TraderActionType.OPEN_LONG);
assertThat(result.action().reduceOnly()).isFalse();
2026-06-26 22:01:25 +08:00
assertThat(traceWriter.actions()).containsExactly(result.action());
assertThat(outboxRepository.events()).hasSize(1);
assertThat(outboxRepository.events().getFirst().destination()).isEqualTo("SHADOW_RECORDER");
assertThat(evidenceRepository.items()).extracting("stage")
2026-06-26 21:53:22 +08:00
.containsExactly("MARKET_SNAPSHOT", "MODEL_OUTPUT", "PM_DECISION", "RISK_DECISION");
}
@Test
2026-06-26 22:07:43 +08:00
void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() throws IOException {
writeArtifactBundle(artifactRoot);
2026-06-26 22:01:25 +08:00
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
2026-06-26 22:17:48 +08:00
var properties = propertiesWithArtifactRoot(artifactRoot);
2026-06-26 21:53:22 +08:00
TraderP0CycleRunner runner = new TraderP0CycleRunner(
2026-06-26 22:17:48 +08:00
properties,
new TraderArtifactLoader(properties, objectMapper()),
2026-06-26 22:07:43 +08:00
new ArtifactTraderModelService(),
2026-06-26 21:53:22 +08:00
new TraderPositionManager(),
new TraderRiskGate(),
new TraderActionFactory(),
evidenceAppender,
2026-06-26 22:01:25 +08:00
traceWriter,
2026-06-26 22:17:48 +08:00
outboxRepository,
new P0ReplayStateStore(properties));
2026-06-26 21:53:22 +08:00
2026-06-26 22:17:48 +08:00
TraderCycleResult result = runner.runCycle(new ReplayMarketEvent(
2026-06-26 21:53:22 +08:00
"run-1", "BTC-USDT-PERP", T0.plusSeconds(60), new BigDecimal("100"), new BigDecimal("99.5"),
new BigDecimal("1.2"), BigDecimal.ZERO));
assertThat(result.action().actionType()).isEqualTo(TraderActionType.WAIT);
assertThat(result.action().pricePlanId()).isNull();
2026-06-26 22:01:25 +08:00
assertThat(traceWriter.actions()).containsExactly(result.action());
assertThat(outboxRepository.events()).hasSize(1);
assertThat(evidenceRepository.items()).hasSize(4);
}
2026-06-26 22:17:48 +08:00
@Test
void laterCycleUsesPositionStateFromEarlierOpen() throws IOException {
writeArtifactBundle(artifactRoot);
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
var properties = propertiesWithArtifactRoot(artifactRoot);
TraderP0CycleRunner runner = new TraderP0CycleRunner(
properties,
new TraderArtifactLoader(properties, objectMapper()),
new ArtifactTraderModelService(),
new TraderPositionManager(),
new TraderRiskGate(),
new TraderActionFactory(),
evidenceAppender,
traceWriter,
outboxRepository,
new P0ReplayStateStore(properties));
TraderCycleResult first = runner.runCycle(new ReplayMarketEvent(
"run-state-1", "BTC-USDT-PERP", T0, new BigDecimal("100"), new BigDecimal("99.5"),
new BigDecimal("1.2"), new BigDecimal("1000")));
TraderCycleResult second = runner.runCycle(new ReplayMarketEvent(
"run-state-1", "BTC-USDT-PERP", T0.plusSeconds(60), new BigDecimal("101"), new BigDecimal("100.5"),
new BigDecimal("1.2"), new BigDecimal("1000")));
assertThat(first.action().actionType()).isEqualTo(TraderActionType.OPEN_LONG);
assertThat(second.action().actionType()).isEqualTo(TraderActionType.ADD_LONG);
assertThat(traceWriter.positionStates()).extracting("side").containsExactly(
com.quantai.trader.enums.PositionSide.NONE,
com.quantai.trader.enums.PositionSide.LONG);
}
2026-06-26 22:01:25 +08:00
private static final class RecordingEvidenceRepository implements TraderEvidenceRepository {
private final List<TraderEvidence> items = new ArrayList<>();
@Override
public void insert(TraderEvidence evidence) {
items.add(evidence);
}
List<TraderEvidence> items() {
return items;
}
}
private static final class RecordingOutboxRepository implements TraderOutboxRepository {
private final List<TraderOutboxEvent> events = new ArrayList<>();
@Override
public void insert(TraderOutboxEvent event) {
events.add(event);
}
List<TraderOutboxEvent> events() {
return events;
}
}
private static final class RecordingTraceWriter implements TraderDecisionTraceWriter {
private final List<TraderAction> actions = new ArrayList<>();
2026-06-26 22:17:48 +08:00
private final List<TraderPositionState> positionStates = new ArrayList<>();
2026-06-26 22:01:25 +08:00
@Override
public void persistCycleTrace(TraderDecisionCycle cycle, TraderMarketSnapshot snapshot, TraderModelOutput modelOutput,
TraderPositionState positionState, TraderPositionManagerDecision pmDecision,
TraderRiskDecision riskDecision, TraderAction action) {
actions.add(action);
2026-06-26 22:17:48 +08:00
positionStates.add(positionState);
2026-06-26 22:01:25 +08:00
}
List<TraderAction> actions() {
return actions;
}
2026-06-26 22:17:48 +08:00
List<TraderPositionState> positionStates() {
return positionStates;
}
2026-06-26 21:53:22 +08:00
}
}