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;
|
|
|
|
|
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 21:53:22 +08:00
|
|
|
TraderP0CycleRunner runner = new TraderP0CycleRunner(
|
2026-06-26 22:07:43 +08:00
|
|
|
propertiesWithArtifactRoot(artifactRoot),
|
|
|
|
|
new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()),
|
|
|
|
|
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 21:53:22 +08:00
|
|
|
outboxRepository);
|
|
|
|
|
|
|
|
|
|
TraderCycleResult result = runner.runFlatCycle(new ReplayMarketEvent(
|
|
|
|
|
"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 21:53:22 +08:00
|
|
|
TraderP0CycleRunner runner = new TraderP0CycleRunner(
|
2026-06-26 22:07:43 +08:00
|
|
|
propertiesWithArtifactRoot(artifactRoot),
|
|
|
|
|
new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()),
|
|
|
|
|
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 21:53:22 +08:00
|
|
|
outboxRepository);
|
|
|
|
|
|
|
|
|
|
TraderCycleResult result = runner.runFlatCycle(new ReplayMarketEvent(
|
|
|
|
|
"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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<>();
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void persistCycleTrace(TraderDecisionCycle cycle, TraderMarketSnapshot snapshot, TraderModelOutput modelOutput,
|
|
|
|
|
TraderPositionState positionState, TraderPositionManagerDecision pmDecision,
|
|
|
|
|
TraderRiskDecision riskDecision, TraderAction action) {
|
|
|
|
|
actions.add(action);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<TraderAction> actions() {
|
|
|
|
|
return actions;
|
|
|
|
|
}
|
2026-06-26 21:53:22 +08:00
|
|
|
}
|
|
|
|
|
}
|