Persist trader V4 P0 decision trace

This commit is contained in:
Codex
2026-06-26 22:01:25 +08:00
parent 5d210053d0
commit 6bbedda97d
17 changed files with 437 additions and 96 deletions
@@ -3,6 +3,8 @@ package com.quantai.trader.evidence;
import com.quantai.trader.domain.TraderEvidence;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -10,7 +12,8 @@ import static org.assertj.core.api.Assertions.assertThat;
class EvidenceAppenderTest {
@Test
void appendsEvidenceWithStageReasonAndDetails() {
EvidenceAppender appender = new EvidenceAppender();
RecordingEvidenceRepository repository = new RecordingEvidenceRepository();
EvidenceAppender appender = new EvidenceAppender(repository);
TraderEvidence item = appender.append("run-1", "cycle-1", "PM_DECISION", true,
"OPEN_LONG_PM_PASS", null, Map.of("action", "OPEN_LONG"));
@@ -18,6 +21,19 @@ class EvidenceAppenderTest {
assertThat(item.evidenceId()).isEqualTo("evidence_cycle-1_0");
assertThat(item.pass()).isTrue();
assertThat(item.detailsJson()).containsEntry("action", "OPEN_LONG");
assertThat(appender.all()).containsExactly(item);
assertThat(repository.items()).containsExactly(item);
}
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;
}
}
}
@@ -1,40 +0,0 @@
package com.quantai.trader.outbox;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class InMemoryOutboxRepositoryTest {
@Test
void rejectsDuplicateDestinationAndIdempotencyKey() {
InMemoryOutboxRepository repository = new InMemoryOutboxRepository();
TraderOutboxEvent event = event("outbox-1", "SHADOW_RECORDER", "idem-1");
repository.insert(event);
assertThatThrownBy(() -> repository.insert(event("outbox-2", "SHADOW_RECORDER", "idem-1")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("duplicate outbox idempotency key");
assertThat(repository.all()).containsExactly(event);
}
@Test
void allowsSameIdempotencyKeyForDifferentDestination() {
InMemoryOutboxRepository repository = new InMemoryOutboxRepository();
repository.insert(event("outbox-1", "REPLAY_SIM_RECORDER", "idem-1"));
repository.insert(event("outbox-2", "SHADOW_RECORDER", "idem-1"));
assertThat(repository.all()).hasSize(2);
}
private TraderOutboxEvent event(String id, String destination, String idempotencyKey) {
return new TraderOutboxEvent(id, "run-1", "cycle-1", "TRADER_ACTION", "action-1",
"ACTION_CREATED", destination, Map.of("actionType", "OPEN_LONG"), idempotencyKey,
"PENDING", Instant.parse("2026-06-26T00:00:00Z"));
}
}
@@ -0,0 +1,28 @@
package com.quantai.trader.outbox;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import java.time.Instant;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class JdbcTraderOutboxRepositoryTest {
@Test
void insertsOutboxEventThroughJdbcRepository() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
JdbcTraderOutboxRepository repository = new JdbcTraderOutboxRepository(jdbcTemplate, new ObjectMapper());
repository.insert(new TraderOutboxEvent("outbox-1", "run-1", "cycle-1",
"TRADER_ACTION", "action-1", "ACTION_CREATED", "SHADOW_RECORDER",
Map.of("actionType", "OPEN_LONG"), "idem-1", "PENDING",
Instant.parse("2026-06-26T00:00:00Z")));
verify(jdbcTemplate).update(contains("insert into trader_outbox"), any(Object[].class));
}
}
@@ -0,0 +1,34 @@
package com.quantai.trader.persistence;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.quantai.trader.domain.TraderActionFactory;
import com.quantai.trader.domain.TraderRiskDecision;
import com.quantai.trader.enums.PositionSide;
import com.quantai.trader.enums.TraderActionType;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Map;
import static com.quantai.trader.TestFixtures.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
class JdbcTraderDecisionTraceWriterTest {
@Test
void persistsRunCycleModelPmRiskAndActionTrace() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
JdbcTraderDecisionTraceWriter writer = new JdbcTraderDecisionTraceWriter(jdbcTemplate, new ObjectMapper(), properties());
TraderRiskDecision riskDecision = new TraderRiskDecision("risk-1", "run-1", "cycle-1",
"pm-cycle-1", true, TraderActionType.OPEN_LONG, TraderActionType.OPEN_LONG, null, Map.of());
var action = new TraderActionFactory().create(pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), riskDecision, "BTC-USDT-PERP");
writer.persistCycleTrace(cycle(), snapshot(), modelOutput(), flatPosition(),
pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), riskDecision, action);
verify(jdbcTemplate, times(6)).update(anyString(), any(Object[].class));
}
}
@@ -1,15 +1,20 @@
package com.quantai.trader.replay;
import com.quantai.trader.artifact.TraderArtifactLoader;
import com.quantai.trader.domain.TraderActionFactory;
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.DeterministicTraderModelService;
import com.quantai.trader.outbox.InMemoryOutboxRepository;
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.risk.TraderRiskGate;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.math.BigDecimal;
import static com.quantai.trader.TestFixtures.T0;
@@ -19,8 +24,10 @@ import static org.assertj.core.api.Assertions.assertThat;
class TraderP0CycleRunnerTest {
@Test
void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() {
EvidenceAppender evidenceAppender = new EvidenceAppender();
InMemoryOutboxRepository outboxRepository = new InMemoryOutboxRepository();
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
TraderP0CycleRunner runner = new TraderP0CycleRunner(
properties(),
new TraderArtifactLoader(properties()),
@@ -29,6 +36,7 @@ class TraderP0CycleRunnerTest {
new TraderRiskGate(),
new TraderActionFactory(),
evidenceAppender,
traceWriter,
outboxRepository);
TraderCycleResult result = runner.runFlatCycle(new ReplayMarketEvent(
@@ -37,16 +45,19 @@ class TraderP0CycleRunnerTest {
assertThat(result.action().actionType()).isEqualTo(TraderActionType.OPEN_LONG);
assertThat(result.action().reduceOnly()).isFalse();
assertThat(outboxRepository.all()).hasSize(1);
assertThat(outboxRepository.all().getFirst().destination()).isEqualTo("SHADOW_RECORDER");
assertThat(evidenceAppender.all()).extracting("stage")
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() {
EvidenceAppender evidenceAppender = new EvidenceAppender();
InMemoryOutboxRepository outboxRepository = new InMemoryOutboxRepository();
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
TraderP0CycleRunner runner = new TraderP0CycleRunner(
properties(),
new TraderArtifactLoader(properties()),
@@ -55,6 +66,7 @@ class TraderP0CycleRunnerTest {
new TraderRiskGate(),
new TraderActionFactory(),
evidenceAppender,
traceWriter,
outboxRepository);
TraderCycleResult result = runner.runFlatCycle(new ReplayMarketEvent(
@@ -63,6 +75,49 @@ class TraderP0CycleRunnerTest {
assertThat(result.action().actionType()).isEqualTo(TraderActionType.WAIT);
assertThat(result.action().pricePlanId()).isNull();
assertThat(outboxRepository.all()).hasSize(1);
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;
}
}
}