package com.quantai.trader.replay; import com.quantai.trader.artifact.TraderArtifactLoader; import com.quantai.trader.domain.*; import com.quantai.trader.enums.TraderActionType; import com.quantai.trader.evidence.EvidenceAppender; import com.quantai.trader.evidence.TraderEvidenceRepository; import com.quantai.trader.model.ArtifactTraderModelService; import com.quantai.trader.outbox.TraderOutboxEvent; import com.quantai.trader.outbox.TraderOutboxRepository; import com.quantai.trader.persistence.TraderDecisionTraceWriter; import com.quantai.trader.position.TraderPositionManager; import com.quantai.trader.replay.state.P0ReplayStateStore; import com.quantai.trader.risk.TraderRiskGate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.util.ArrayList; import java.util.List; import java.io.IOException; import java.math.BigDecimal; import java.nio.file.Path; import static com.quantai.trader.TestFixtures.*; import static org.assertj.core.api.Assertions.assertThat; class TraderP0CycleRunnerTest { @TempDir Path artifactRoot; @Test void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() 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 result = runner.runCycle(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(); assertThat(traceWriter.actions()).containsExactly(result.action()); assertThat(outboxRepository.events()).hasSize(1); assertThat(outboxRepository.events().getFirst().destination()).isEqualTo("SHADOW_RECORDER"); assertThat(evidenceRepository.items()).extracting("stage") .containsExactly("MARKET_SNAPSHOT", "MODEL_OUTPUT", "PM_DECISION", "RISK_DECISION"); } @Test void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() 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 result = runner.runCycle(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(); assertThat(traceWriter.actions()).containsExactly(result.action()); assertThat(outboxRepository.events()).hasSize(1); assertThat(evidenceRepository.items()).hasSize(4); } @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); } private static final class RecordingEvidenceRepository implements TraderEvidenceRepository { private final List items = new ArrayList<>(); @Override public void insert(TraderEvidence evidence) { items.add(evidence); } List items() { return items; } } private static final class RecordingOutboxRepository implements TraderOutboxRepository { private final List events = new ArrayList<>(); @Override public void insert(TraderOutboxEvent event) { events.add(event); } List events() { return events; } } private static final class RecordingTraceWriter implements TraderDecisionTraceWriter { private final List actions = new ArrayList<>(); private final List positionStates = new ArrayList<>(); @Override public void persistCycleTrace(TraderDecisionCycle cycle, TraderMarketSnapshot snapshot, TraderModelOutput modelOutput, TraderPositionState positionState, TraderPositionManagerDecision pmDecision, TraderRiskDecision riskDecision, TraderAction action) { actions.add(action); positionStates.add(positionState); } List actions() { return actions; } List positionStates() { return positionStates; } } }