Implement Trader V4 training artifact pipeline
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user