Implement Trader V4 training artifact pipeline

This commit is contained in:
Codex
2026-06-27 16:15:23 +08:00
parent dad6b831b4
commit e58e4a5572
113 changed files with 7959 additions and 477 deletions
@@ -1,15 +1,44 @@
package com.quantai.trader.replay;
import static com.quantai.trader.util.TraderNumbers.nonNegative;
import static com.quantai.trader.util.TraderNumbers.positive;
import static com.quantai.trader.util.TraderNumbers.required;
import static com.quantai.trader.util.TraderNumbers.requiredText;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public record ReplayMarketEvent(
String runId,
String symbol,
Instant eventTime,
String featureVersion,
BigDecimal markPrice,
BigDecimal indexPrice,
BigDecimal spreadBps,
BigDecimal depthNotional5Bps
BigDecimal fundingRateBps,
BigDecimal depthNotional5Bps,
BigDecimal depthNotional10Bps,
BigDecimal depthNotional25Bps,
boolean dataReady,
Map<String, Object> featureJson,
Map<String, Object> dataQualityJson
) {
public ReplayMarketEvent {
runId = requiredText(runId, "runId");
symbol = requiredText(symbol, "symbol");
eventTime = Objects.requireNonNull(eventTime, "eventTime is required");
featureVersion = requiredText(featureVersion, "featureVersion");
markPrice = positive(markPrice, "markPrice");
indexPrice = positive(indexPrice, "indexPrice");
spreadBps = nonNegative(spreadBps, "spreadBps");
fundingRateBps = required(fundingRateBps, "fundingRateBps");
depthNotional5Bps = nonNegative(depthNotional5Bps, "depthNotional5Bps");
depthNotional10Bps = nonNegative(depthNotional10Bps, "depthNotional10Bps");
depthNotional25Bps = nonNegative(depthNotional25Bps, "depthNotional25Bps");
featureJson = Map.copyOf(featureJson == null ? Map.of() : featureJson);
dataQualityJson = Map.copyOf(dataQualityJson == null ? Map.of() : dataQualityJson);
}
}
@@ -1,6 +1,7 @@
package com.quantai.trader.replay;
import com.quantai.trader.artifact.TraderArtifactBundle;
import com.quantai.trader.artifact.TraderArtifactManifestRepository;
import com.quantai.trader.artifact.TraderArtifactLoader;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.domain.*;
@@ -9,6 +10,7 @@ import com.quantai.trader.evidence.EvidenceAppender;
import com.quantai.trader.model.TraderModelService;
import com.quantai.trader.outbox.TraderOutboxRepository;
import com.quantai.trader.outbox.TraderOutboxEvent;
import com.quantai.trader.outbox.TraderOutboxDispatcher;
import com.quantai.trader.persistence.TraderDecisionTraceWriter;
import com.quantai.trader.position.TraderPositionManager;
import com.quantai.trader.replay.state.TraderReplayState;
@@ -16,11 +18,15 @@ import com.quantai.trader.replay.state.TraderReplayStateStore;
import com.quantai.trader.risk.RiskGateInput;
import com.quantai.trader.risk.RiskLimits;
import com.quantai.trader.risk.TraderRiskGate;
import com.quantai.trader.runtime.TraderRuntimeControlDecision;
import com.quantai.trader.runtime.TraderRuntimeControlService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
@@ -36,8 +42,11 @@ public class TraderP0CycleRunner {
private final TraderActionFactory actionFactory;
private final EvidenceAppender evidenceAppender;
private final TraderDecisionTraceWriter traceWriter;
private final TraderArtifactManifestRepository artifactManifestRepository;
private final TraderOutboxRepository outboxRepository;
private final TraderOutboxDispatcher outboxDispatcher;
private final TraderReplayStateStore stateStore;
private final TraderRuntimeControlService runtimeControlService;
public TraderP0CycleRunner(TraderProperties properties,
TraderArtifactLoader artifactLoader,
@@ -47,8 +56,11 @@ public class TraderP0CycleRunner {
TraderActionFactory actionFactory,
EvidenceAppender evidenceAppender,
TraderDecisionTraceWriter traceWriter,
TraderArtifactManifestRepository artifactManifestRepository,
TraderOutboxRepository outboxRepository,
TraderReplayStateStore stateStore) {
TraderOutboxDispatcher outboxDispatcher,
TraderReplayStateStore stateStore,
TraderRuntimeControlService runtimeControlService) {
this.properties = properties;
this.artifactLoader = artifactLoader;
this.modelService = modelService;
@@ -57,47 +69,88 @@ public class TraderP0CycleRunner {
this.actionFactory = actionFactory;
this.evidenceAppender = evidenceAppender;
this.traceWriter = traceWriter;
this.artifactManifestRepository = artifactManifestRepository;
this.outboxRepository = outboxRepository;
this.outboxDispatcher = outboxDispatcher;
this.stateStore = stateStore;
this.runtimeControlService = runtimeControlService;
}
@Transactional
public TraderCycleResult runCycle(ReplayMarketEvent event) {
String cycleId = "cycle_" + event.runId() + "_" + event.eventTime().toEpochMilli();
TraderArtifactBundle bundle = artifactLoader.loadActiveBundle();
artifactManifestRepository.upsertActiveBundle(bundle);
TraderDecisionCycle cycle = new TraderDecisionCycle(event.runId(), cycleId, event.symbol(), event.eventTime(),
properties.runMode(), bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion());
TraderMarketSnapshot snapshot = snapshot(event, cycleId);
TraderReplayState state = stateStore.load(cycle, snapshot);
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "MARKET_SNAPSHOT", snapshot.dataReady(), "SNAPSHOT_BUILT", null, Map.of());
TraderModelOutput modelOutput = modelService.evaluate(snapshot, bundle);
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "MODEL_OUTPUT", true, "MODEL_EVALUATED", null, Map.of("modelOutputId", modelOutput.modelOutputId()));
PositionManagerInput pmInput = new PositionManagerInput(cycle, snapshot, modelOutput,
state.positionState(), state.accountState(), state.executionState(), bundle.pmConfig());
TraderPositionManagerDecision pmDecision = positionManager.decide(pmInput);
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "PM_DECISION", true, pmDecision.reason(), null, Map.of("action", pmDecision.candidateAction().name()));
TraderRiskDecision riskDecision = riskGate.evaluate(new RiskGateInput(pmDecision, pmInput.positionState(), pmInput.accountState(),
pmInput.executionState(), snapshot, riskLimits()));
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "RISK_DECISION", riskDecision.allowAction(), riskDecision.allowAction() ? "RISK_PASS" : riskDecision.blocker(), riskDecision.blocker(), Map.of());
TraderAction action = actionFactory.create(pmDecision, riskDecision, event.symbol());
traceWriter.persistCycleTrace(cycle, snapshot, modelOutput, pmInput.positionState(), pmDecision, riskDecision, action);
outboxRepository.insert(new TraderOutboxEvent("outbox_" + action.actionId(), action.runId(), action.cycleId(),
"TRADER_ACTION", action.actionId(), "ACTION_CREATED", properties.runMode().name() + "_RECORDER",
Map.of("actionType", action.actionType().name()), action.idempotencyKey(), "PENDING", Instant.now()));
stateStore.advance(state, action, snapshot);
log.info("event=trader.cycle.completed runId={} cycleId={} action={} outbox=PENDING", action.runId(), action.cycleId(), action.actionType());
return new TraderCycleResult(cycle.runId(), cycle.cycleId(), pmDecision, riskDecision, action);
runtimeControlService.acquireCycleLock(cycle);
try {
TraderMarketSnapshot snapshot = snapshot(event, cycleId);
TraderReplayState state = stateStore.load(cycle, snapshot);
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "MARKET_SNAPSHOT", snapshot.dataReady(), "SNAPSHOT_BUILT", null, Map.of());
TraderModelOutput modelOutput = modelService.evaluate(snapshot, bundle);
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "MODEL_OUTPUT", true, "MODEL_EVALUATED", null, Map.of("modelOutputId", modelOutput.modelOutputId()));
PositionManagerInput pmInput = new PositionManagerInput(cycle, snapshot, modelOutput,
bundle.pricePlanContext(), state.positionState(), state.accountState(), state.executionState(), bundle.pmConfig());
TraderPositionManagerDecision pmDecision = positionManager.decide(pmInput);
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "PM_DECISION", true, pmDecision.reason(), null, Map.of("action", pmDecision.candidateAction().name()));
TraderRuntimeControlDecision runtimeDecision = runtimeControlService.validateExposureIncrease(cycle, pmDecision);
if (runtimeDecision.allowed()
&& pmDecision.candidateAction().increasesExposure()
&& outboxRepository.hasUnsentExposureIncrease(cycle.runId(), cycle.symbol(), pmDecision.side())) {
runtimeDecision = TraderRuntimeControlDecision.block("OUTBOX_PENDING_OPEN_ADD");
}
TraderRiskDecision riskDecision = riskGate.evaluate(new RiskGateInput(pmDecision, pmInput.positionState(), pmInput.accountState(),
pmInput.executionState(), snapshot, riskLimits(runtimeDecision)));
evidenceAppender.append(cycle.runId(), cycle.cycleId(), "RISK_DECISION", riskDecision.allowAction(), riskDecision.allowAction() ? "RISK_PASS" : riskDecision.blocker(), riskDecision.blocker(), Map.of());
TraderAction action = actionFactory.create(pmDecision, riskDecision, event.symbol());
traceWriter.persistCycleTrace(cycle, snapshot, modelOutput, pmInput.positionState(),
pmInput.accountState(), pmInput.executionState(), pmDecision, riskDecision, action);
String destination = outboxDestination();
outboxRepository.insert(new TraderOutboxEvent("outbox_" + action.actionId(), action.runId(), action.cycleId(),
"TRADER_ACTION", action.actionId(), "ACTION_CREATED", destination,
Map.of("actionType", action.actionType().name()), action.idempotencyKey(), "PENDING", Instant.now()));
dispatchAfterCommit(action, state, snapshot, destination);
log.info("event=trader.cycle.completed runId={} cycleId={} action={} outbox=PENDING runtimeBlocker={}",
action.runId(), action.cycleId(), action.actionType(), runtimeDecision.blocker());
return new TraderCycleResult(cycle.runId(), cycle.cycleId(), pmDecision, riskDecision, action);
} finally {
runtimeControlService.releaseCycleLock(cycle);
}
}
private TraderMarketSnapshot snapshot(ReplayMarketEvent event, String cycleId) {
return new TraderMarketSnapshot("snapshot_" + cycleId, event.runId(), cycleId, event.symbol(), event.eventTime(),
"feature-v4-p0", event.markPrice(), event.indexPrice(), event.spreadBps(), BigDecimal.ZERO,
event.depthNotional5Bps(), event.depthNotional5Bps(), event.depthNotional5Bps(),
event.depthNotional5Bps().compareTo(BigDecimal.ZERO) > 0, Map.of(), Map.of());
event.featureVersion(), event.markPrice(), event.indexPrice(), event.spreadBps(), event.fundingRateBps(),
event.depthNotional5Bps(), event.depthNotional10Bps(), event.depthNotional25Bps(),
event.dataReady(), event.featureJson(), event.dataQualityJson());
}
private RiskLimits riskLimits() {
private RiskLimits riskLimits(TraderRuntimeControlDecision runtimeDecision) {
return new RiskLimits(properties.risk().maxDailyLossBps(), properties.risk().maxTotalExposureRatio(),
properties.risk().minLiquidationBufferBps(), properties.execution().maxApiErrorCount(),
properties.execution().maxExchangeLatencyMs(), false, false);
properties.execution().maxExchangeLatencyMs(), false, !runtimeDecision.allowed(), runtimeDecision.blocker());
}
private String outboxDestination() {
return switch (properties.execution().mode()) {
case REPLAY_SIM -> "REPLAY_SIM_EXECUTION";
case SHADOW -> "SHADOW_RECORDER";
case PAPER, REAL -> throw new IllegalStateException("P0 runtime guard must reject PAPER/REAL execution mode");
};
}
private void dispatchAfterCommit(TraderAction action, TraderReplayState state,
TraderMarketSnapshot snapshot, String destination) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
outboxDispatcher.dispatch(action, state, snapshot, destination);
return;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
outboxDispatcher.dispatch(action, state, snapshot, destination);
}
});
}
}
@@ -0,0 +1,70 @@
package com.quantai.trader.replay.state;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.quantai.trader.domain.TraderAccountState;
import com.quantai.trader.domain.TraderExecutionState;
import com.quantai.trader.domain.TraderPositionState;
import com.quantai.trader.persistence.TraderJsonCodec;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.sql.Timestamp;
@Repository
public class JdbcTraderPostActionStateRepository implements TraderPostActionStateRepository {
private final JdbcTemplate jdbcTemplate;
private final TraderJsonCodec jsonCodec;
public JdbcTraderPostActionStateRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.jsonCodec = new TraderJsonCodec(objectMapper);
}
@Override
public void insertPostActionState(TraderReplayState state) {
insertPositionState(state.positionState());
insertAccountState(state.accountState());
insertExecutionState(state.executionState());
}
private void insertPositionState(TraderPositionState state) {
jdbcTemplate.update("""
insert into trader_position_state
(run_id, cycle_id, position_state_id, state_role, symbol, side, position_ratio,
average_entry_price, current_price, unrealized_pnl_bps, liquidation_buffer_bps,
add_count, remaining_add_capacity, last_add_time)
values (?, ?, ?, 'POST_ACTION', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
state.runId(), state.cycleId(), state.positionStateId() + "_post", state.symbol(),
state.side().name(), state.positionRatio(), state.averageEntryPrice(), state.currentPrice(),
state.unrealizedPnlBps(), state.liquidationBufferBps(), state.addCount(),
state.remainingAddCapacity(), state.lastAddTime() == null ? null : Timestamp.from(state.lastAddTime()));
}
private void insertAccountState(TraderAccountState state) {
jdbcTemplate.update("""
insert into trader_account_state
(run_id, cycle_id, account_state_id, state_role, daily_drawdown_bps,
portfolio_exposure_ratio, remaining_symbol_capacity_ratio, consecutive_losses)
values (?, ?, ?, 'POST_ACTION', ?, ?, ?, ?)
""",
state.runId(), state.cycleId(), state.accountStateId() + "_post", state.dailyDrawdownBps(),
state.portfolioExposureRatio(), state.remainingSymbolCapacityRatio(), state.consecutiveLosses());
}
private void insertExecutionState(TraderExecutionState state) {
jdbcTemplate.update("""
insert into trader_execution_state
(run_id, cycle_id, execution_state_id, state_role, symbol, open_orders_json,
expected_slippage_bps, exchange_latency_ms, api_error_count, maker_fee_bps,
taker_fee_bps, min_notional, price_tick_size, lot_size_step_size,
market_lot_size_step_size, liquidity_capacity_ratio)
values (?, ?, ?, 'POST_ACTION', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
state.runId(), state.cycleId(), state.executionStateId() + "_post", state.symbol(),
jsonCodec.toJson(state.openOrders()), state.expectedSlippageBps(), state.exchangeLatencyMs(),
state.apiErrorCount(), state.makerFeeBps(), state.takerFeeBps(), state.minNotional(),
state.priceTickSize(), state.lotSizeStepSize(), state.marketLotSizeStepSize(),
state.liquidityCapacityRatio());
}
}
@@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -88,7 +87,7 @@ public class P0ReplayStateStore implements TraderReplayStateStore {
BigDecimal weightedEntry = weightedEntry(current.averageEntryPrice(), current.positionRatio(), snapshot.markPrice(), addRatio);
return new TraderPositionState("position_state_" + action.cycleId(), action.runId(), action.cycleId(), action.symbol(),
current.side(), newRatio, weightedEntry, snapshot.markPrice(), pnlBps(current.side(), weightedEntry, snapshot.markPrice()),
current.liquidationBufferBps(), current.addCount() + 1, remainingCapacity(newRatio), Instant.now());
current.liquidationBufferBps(), current.addCount() + 1, remainingCapacity(newRatio), snapshot.snapshotTime());
}
private TraderPositionState reducePosition(TraderPositionState current, TraderAction action, TraderMarketSnapshot snapshot) {
@@ -0,0 +1,5 @@
package com.quantai.trader.replay.state;
public interface TraderPostActionStateRepository {
void insertPostActionState(TraderReplayState state);
}