Persist trader V4 P0 decision trace
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user