Rewrite trader service for V4 P0

This commit is contained in:
Codex
2026-06-26 21:53:22 +08:00
parent 2fe4077164
commit 5d210053d0
184 changed files with 2780 additions and 6945 deletions
+44 -14
View File
@@ -15,11 +15,11 @@
<artifactId>quant-trader-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>quant-trader-service</name>
<description>Clean P0 rebuild of the Trader-style strategy service.</description>
<description>Trader V4 P0 decision service: REPLAY_SIM and SHADOW only.</description>
<properties>
<java.version>21</java.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<jacoco.version>0.8.13</jacoco.version>
</properties>
<dependencies>
@@ -35,15 +35,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
@@ -53,9 +44,8 @@
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.10.0</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -99,6 +89,46 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check-line-coverage</id>
<phase>test</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,11 +1,9 @@
package com.quantai.trader;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@MapperScan("com.quantai.trader.infrastructure.mapper")
@SpringBootApplication
@ConfigurationPropertiesScan
public class QuantTraderServiceApplication {
@@ -0,0 +1,21 @@
package com.quantai.trader.artifact;
import com.quantai.trader.domain.TraderPmConfig;
import java.util.Set;
public record TraderArtifactBundle(
String modelBundleVersion,
String calibrationBundleVersion,
String pmConfigVersion,
String bundleHashSha256,
Set<String> providedModels,
TraderPmConfig pmConfig
) {
public TraderArtifactBundle {
if (providedModels == null || !providedModels.containsAll(Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"))) {
throw new IllegalArgumentException("artifact bundle must provide all five V4 models");
}
pmConfig = java.util.Objects.requireNonNull(pmConfig, "pmConfig is required");
}
}
@@ -0,0 +1,71 @@
package com.quantai.trader.artifact;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.domain.TraderException;
import com.quantai.trader.domain.TraderPmConfig;
import com.quantai.trader.enums.TraderErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Set;
@Component
public class TraderArtifactLoader {
private static final Logger log = LoggerFactory.getLogger(TraderArtifactLoader.class);
private final TraderProperties properties;
public TraderArtifactLoader(TraderProperties properties) {
this.properties = properties;
}
public TraderArtifactBundle loadActiveBundle() {
TraderProperties.Artifact artifact = properties.artifact();
if (artifact.modelBundleVersion().isBlank()
|| artifact.calibrationBundleVersion().isBlank()
|| artifact.pmConfigVersion().isBlank()) {
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
"model/calibration/pm version is required");
}
TraderArtifactBundle bundle = deterministicP0Bundle(artifact);
log.info("event=trader.artifact.loaded modelBundleVersion={} calibrationBundleVersion={} pmConfigVersion={} providedModels={}",
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), bundle.providedModels());
return bundle;
}
private TraderArtifactBundle deterministicP0Bundle(TraderProperties.Artifact artifact) {
TraderPmConfig pmConfig = new TraderPmConfig(
artifact.pmConfigVersion(),
new TraderPmConfig.OpenRuleConfig(
new BigDecimal("0.58"), new BigDecimal("0.58"),
new BigDecimal("0.55"), new BigDecimal("0.55"),
new BigDecimal("0.45"), new BigDecimal("1.0"),
new BigDecimal("0.03"), new BigDecimal("0.10"), new BigDecimal("0.80")),
new TraderPmConfig.AddRuleConfig(
new BigDecimal("0.60"), new BigDecimal("0.60"),
new BigDecimal("0.58"), new BigDecimal("0.55"), new BigDecimal("0.45"),
new BigDecimal("0.45"), new BigDecimal("0.50"),
new BigDecimal("1.0"), BigDecimal.ZERO, new BigDecimal("0.10"),
new BigDecimal("500"), 3, 5),
new TraderPmConfig.ExitRuleConfig(
new BigDecimal("0.70"), new BigDecimal("0.70"), new BigDecimal("0.70"),
new BigDecimal("0.25"), new BigDecimal("0.62"),
new BigDecimal("0.35"), new BigDecimal("0.70"),
new BigDecimal("5.0"), new BigDecimal("80")),
new TraderPmConfig.SizingConfig(
new BigDecimal("0.80"), new BigDecimal("0.05"), BigDecimal.ONE,
new BigDecimal("0.02"), new BigDecimal("0.25"), BigDecimal.ONE,
new BigDecimal("1.0"), new BigDecimal("80"),
new BigDecimal("0.20"), new BigDecimal("0.50"), new BigDecimal("500"))
);
return new TraderArtifactBundle(
artifact.modelBundleVersion(),
artifact.calibrationBundleVersion(),
artifact.pmConfigVersion(),
"deterministic-p0-fixture",
Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"),
pmConfig);
}
}
@@ -1,27 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.ManagementDecision;
import com.quantai.trader.domain.PlaybookCandidate;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.domain.TraderPositionPath;
import com.quantai.trader.enums.TraderActionType;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class LifecycleActionValueService {
public ManagementDecision decide(
TraderDecisionCycle cycle,
PlaybookCandidate candidate,
TraderPositionPath path,
TraderMarketSnapshot snapshot
) {
if (path != null && path.fullSize()) {
return new ManagementDecision(TraderActionType.CLOSE, "P0_PROXY_CLOSE_AFTER_FULL_PATH", Map.of("proxyOnly", true));
}
return new ManagementDecision(TraderActionType.HOLD, "P0_PROXY_HOLD", Map.of("proxyOnly", true));
}
}
@@ -1,85 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.PlaybookCandidate;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderException;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.domain.TraderPricePlan;
import com.quantai.trader.enums.TraderErrorCode;
import com.quantai.trader.enums.TraderSide;
import com.quantai.trader.playbook.TraderPlaybookCatalog;
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
import com.quantai.trader.util.Ids;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Component
public class PlaybookCandidateEngine {
private final TraderPlaybookCatalog catalog;
public PlaybookCandidateEngine(TraderPlaybookCatalog catalog) {
this.catalog = catalog;
}
public List<PlaybookCandidate> generate(TraderMarketSnapshot snapshot, TraderDecisionCycle cycle) {
if (!Boolean.TRUE.equals(snapshot.setupFeatures().get("setupPass"))) {
return List.of();
}
TraderPlaybookDefinitionSnapshot playbook = catalog.require(cycle.playbookId(), cycle.playbookVersion());
BigDecimal entry = requiredDecimal(snapshot.setupFeatures(), "entryPrice");
TraderPricePlan pricePlan = new TraderPricePlan(
entry,
requiredDecimal(snapshot.setupFeatures(), "invalidPrice"),
requiredDecimal(snapshot.setupFeatures(), "stopPrice"),
requiredDecimal(snapshot.setupFeatures(), "targetPrice"),
null,
300_000,
7_200_000
);
return List.of(new PlaybookCandidate(
cycle.runId(),
cycle.cycleId(),
Ids.candidateId(cycle, playbook.playbookId()),
playbook.playbookId(),
playbook.playbookVersion(),
requiredSide(snapshot.setupFeatures(), "side"),
playbook.variant(),
snapshot.snapshotTime(),
pricePlan,
playbook.definition().plannedEntryLegRule().maxPlannedEntryLegs(),
snapshot.setupFeatures()
));
}
private TraderSide requiredSide(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof TraderSide side) {
return side;
}
if (value instanceof String text && !text.isBlank()) {
return TraderSide.valueOf(text.trim().toUpperCase());
}
throw new TraderException(
TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE,
"setup feature is required when setupPass=true: " + key
);
}
private BigDecimal requiredDecimal(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number number) {
return BigDecimal.valueOf(number.doubleValue());
}
if (value instanceof String text && !text.isBlank()) {
return new BigDecimal(text);
}
throw new TraderException(
TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE,
"setup feature is required when setupPass=true: " + key
);
}
}
@@ -1,26 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.StageDecision;
import com.quantai.trader.domain.TraderMarketSnapshot;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class TraderContextGate {
public StageDecision evaluate(TraderMarketSnapshot snapshot) {
Object missing = snapshot.dataQuality().get("missing_features");
if (missing instanceof List<?> list && !list.isEmpty()) {
return new StageDecision(false, "DATA_MISSING", "TRADER_DATA_QUALITY_FAILED", Map.of(
"missingFeatures", list
));
}
Object pass = snapshot.contextFeatures().get("contextPass");
if (Boolean.FALSE.equals(pass)) {
return StageDecision.block("CONTEXT_BLOCKED", "TRADER_RISK_BLOCKED");
}
return StageDecision.pass("CONTEXT_PASS");
}
}
@@ -1,14 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.TraderAction;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderPositionPath;
import com.quantai.trader.domain.TraderTrainingSample;
public record TraderCycleResult(
TraderDecisionCycle cycle,
TraderAction action,
TraderPositionPath path,
TraderTrainingSample sample
) {
}
@@ -1,165 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.ExecutionDecision;
import com.quantai.trader.domain.ManagementDecision;
import com.quantai.trader.domain.PlaybookCandidate;
import com.quantai.trader.domain.RiskDecision;
import com.quantai.trader.domain.StageDecision;
import com.quantai.trader.domain.TraderAction;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderEntryPlan;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.domain.TraderPositionPath;
import com.quantai.trader.domain.TraderTrainingSample;
import com.quantai.trader.evidence.EvidenceAppender;
import com.quantai.trader.enums.TraderState;
import com.quantai.trader.execution.ExecutionQualityGate;
import com.quantai.trader.execution.TraderEntryPlanner;
import com.quantai.trader.market.SnapshotBuilder;
import com.quantai.trader.position.TraderPositionManager;
import com.quantai.trader.replay.ReplayClockTick;
import com.quantai.trader.risk.TraderRiskGate;
import com.quantai.trader.sample.TrainingSampleExporter;
import com.quantai.trader.state.TraderDecisionCycleFactory;
import com.quantai.trader.state.TraderRuntimeState;
import com.quantai.trader.state.TraderStateMachine;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
public class TraderDecisionCycleRunner {
private final SnapshotBuilder snapshotBuilder;
private final TraderContextGate contextGate;
private final PlaybookCandidateEngine playbookCandidateEngine;
private final TriggerMarkoutService triggerMarkoutService;
private final TraderEntryPlanner entryPlanner;
private final ExecutionQualityGate executionQualityGate;
private final TraderRiskGate riskGate;
private final TraderStateMachine stateMachine;
private final TraderPositionManager positionManager;
private final LifecycleActionValueService actionValueService;
private final EvidenceAppender evidenceAppender;
private final TrainingSampleExporter sampleExporter;
public TraderDecisionCycleRunner(
SnapshotBuilder snapshotBuilder,
TraderContextGate contextGate,
PlaybookCandidateEngine playbookCandidateEngine,
TriggerMarkoutService triggerMarkoutService,
TraderEntryPlanner entryPlanner,
ExecutionQualityGate executionQualityGate,
TraderRiskGate riskGate,
TraderStateMachine stateMachine,
TraderPositionManager positionManager,
LifecycleActionValueService actionValueService,
EvidenceAppender evidenceAppender,
TrainingSampleExporter sampleExporter
) {
this.snapshotBuilder = snapshotBuilder;
this.contextGate = contextGate;
this.playbookCandidateEngine = playbookCandidateEngine;
this.triggerMarkoutService = triggerMarkoutService;
this.entryPlanner = entryPlanner;
this.executionQualityGate = executionQualityGate;
this.riskGate = riskGate;
this.stateMachine = stateMachine;
this.positionManager = positionManager;
this.actionValueService = actionValueService;
this.evidenceAppender = evidenceAppender;
this.sampleExporter = sampleExporter;
}
public TraderCycleResult runReplayTick(ReplayClockTick tick, TraderRuntimeState runtimeState) {
TraderMarketSnapshot snapshot = snapshotBuilder.build(tick, runtimeState);
TraderDecisionCycle cycle = TraderDecisionCycleFactory.create(snapshot, runtimeState);
StageDecision context = contextGate.evaluate(snapshot);
evidenceAppender.append(cycle, "CONTEXT_GATE", context);
if (context.blocked()) {
TraderTrainingSample sample = sampleExporter.export(cycle.withState(TraderState.BLOCKED, "BLOCKED", context.blocker()), snapshot, null, null, null);
return new TraderCycleResult(cycle, null, null, sample);
}
List<PlaybookCandidate> candidates = playbookCandidateEngine.generate(snapshot, cycle);
if (candidates.isEmpty()) {
evidenceAppender.append(cycle, "PLAYBOOK_CANDIDATE", StageDecision.block("NO_PLAYBOOK_CANDIDATE", "NO_PLAYBOOK_CANDIDATE"));
TraderTrainingSample sample = sampleExporter.export(cycle, snapshot, null, null, null);
return new TraderCycleResult(cycle, null, null, sample);
}
PlaybookCandidate selected = candidates.getFirst();
var trigger = triggerMarkoutService.evaluate(snapshot, selected);
evidenceAppender.append(cycle, "TRIGGER_MARKOUT", new StageDecision(trigger.pass(), trigger.reason(), trigger.blocker(), trigger.details()));
if (trigger.blocked()) {
TraderTrainingSample sample = sampleExporter.export(cycle.withState(TraderState.TRIGGER_WAIT, "WAIT", trigger.blocker()), snapshot, selected, null, null);
return new TraderCycleResult(cycle, null, null, sample);
}
TraderDecisionCycle entryCycle = cycle.withState(TraderState.ENTRY_PLANNED, "RUNNING", null);
TraderEntryPlan entryPlan = entryPlanner.planInitialEntry(entryCycle, selected, trigger);
ExecutionDecision execution = executionQualityGate.evaluate(snapshot, entryPlan);
evidenceAppender.append(entryCycle, "EXECUTION_QUALITY", new StageDecision(execution.pass(), execution.reason(), execution.blocker(), execution.details()));
RiskDecision risk = riskGate.evaluate(entryCycle, entryPlan, execution);
evidenceAppender.append(entryCycle, "RISK_GATE", new StageDecision(risk.allowAction(), risk.allowAction() ? "RISK_PASS" : "RISK_BLOCKED", risk.blocker(), risk.details()));
if (execution.blocked() || risk.blocked()) {
TraderTrainingSample sample = sampleExporter.export(entryCycle.withState(TraderState.BLOCKED, "BLOCKED", risk.blocker()), snapshot, selected, null, null);
return new TraderCycleResult(entryCycle, null, null, sample);
}
TraderAction action = stateMachine.toInitialEntryAction(entryCycle, selected, entryPlan);
TraderPositionPath path = positionManager.simulateOrUpdate(entryCycle, action, snapshot);
evidenceAppender.append(entryCycle, "OPEN_INITIAL", StageDecision.pass(action.reason()));
TraderLifecycleResult lifecycle = runPositionLifecycle(entryCycle, selected, action, path, snapshot);
TraderTrainingSample sample = sampleExporter.export(
lifecycle.finalCycle(),
snapshot,
selected,
lifecycle.lastAction(),
lifecycle.finalPath()
);
return new TraderCycleResult(lifecycle.finalCycle(), lifecycle.lastAction(), lifecycle.finalPath(), sample);
}
private TraderLifecycleResult runPositionLifecycle(
TraderDecisionCycle initialCycle,
PlaybookCandidate candidate,
TraderAction initialAction,
TraderPositionPath initialPath,
TraderMarketSnapshot snapshot
) {
TraderDecisionCycle cycle = initialCycle;
TraderPositionPath path = initialPath;
TraderAction lastAction = initialAction;
if (!path.fullSize()) {
cycle = cycle.withState(TraderState.PLANNED_LEG_WAIT, "RUNNING", null);
Optional<TraderEntryPlan> plannedLeg = entryPlanner.planNextDeclaredLeg(cycle, candidate, path);
if (plannedLeg.isPresent()) {
ExecutionDecision execution = executionQualityGate.evaluate(snapshot, plannedLeg.get());
RiskDecision risk = riskGate.evaluate(cycle, plannedLeg.get(), execution);
evidenceAppender.append(cycle, "PLANNED_LEG_EXECUTION", new StageDecision(execution.pass(), execution.reason(), execution.blocker(), execution.details()));
evidenceAppender.append(cycle, "PLANNED_LEG_RISK", new StageDecision(risk.allowAction(), risk.allowAction() ? "RISK_PASS" : "RISK_BLOCKED", risk.blocker(), risk.details()));
if (execution.pass() && risk.allowAction()) {
TraderAction legAction = stateMachine.toPlannedLegAction(cycle, plannedLeg.get(), path);
path = positionManager.simulateOrUpdate(cycle, legAction, snapshot);
evidenceAppender.append(cycle, "OPEN_PLANNED_LEG", StageDecision.pass(legAction.reason()));
lastAction = legAction;
}
}
}
cycle = cycle.withState(TraderState.MANAGING, "RUNNING", null);
ManagementDecision management = actionValueService.decide(cycle, candidate, path, snapshot);
evidenceAppender.append(cycle, "ACTION_VALUE", new StageDecision(true, management.reason(), null, management.details()));
TraderAction managementAction = stateMachine.toManagementAction(cycle, path, management.actionType());
riskGate.evaluateManagement(cycle, managementAction, path, management);
path = positionManager.simulateOrUpdate(cycle, managementAction, snapshot);
evidenceAppender.append(cycle, "MANAGEMENT_ACTION", StageDecision.pass(managementAction.reason()));
lastAction = managementAction;
return new TraderLifecycleResult(cycle.withState(TraderState.SAMPLE_EXPORTED, "COMPLETED", null), path, lastAction);
}
}
@@ -1,12 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.TraderAction;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderPositionPath;
public record TraderLifecycleResult(
TraderDecisionCycle finalCycle,
TraderPositionPath finalPath,
TraderAction lastAction
) {
}
@@ -1,39 +0,0 @@
package com.quantai.trader.brain;
import com.quantai.trader.domain.PlaybookCandidate;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.domain.TriggerDecision;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Map;
@Component
public class TriggerMarkoutService {
public TriggerDecision evaluate(TraderMarketSnapshot snapshot, PlaybookCandidate candidate) {
BigDecimal score = readScore(snapshot.triggerFeatures().get("triggerScore"));
if (score == null) {
return new TriggerDecision(false, BigDecimal.ZERO, "TRIGGER_SCORE_MISSING", "NO_TRIGGER_MARKOUT", Map.of());
}
if (score.compareTo(new BigDecimal("0.50")) < 0) {
return new TriggerDecision(false, score, "TRIGGER_WAIT", "NO_TRIGGER_MARKOUT", Map.of(
"triggerScore", score
));
}
return new TriggerDecision(true, score, "TRIGGER_ACCEPTED", null, Map.of(
"triggerScore", score,
"proxyOnly", true
));
}
private BigDecimal readScore(Object value) {
if (value instanceof Number number) {
return BigDecimal.valueOf(number.doubleValue());
}
if (value instanceof String text && !text.isBlank()) {
return new BigDecimal(text);
}
return null;
}
}
@@ -1,299 +1,92 @@
package com.quantai.trader.config;
import com.quantai.trader.enums.TraderExecutionMode;
import com.quantai.trader.enums.TraderRunMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.math.BigDecimal;
import static com.quantai.trader.util.TraderNumbers.requiredText;
@ConfigurationProperties(prefix = "trader")
public class TraderProperties {
private String serviceName = "quant-trader-service";
private TraderRunMode runMode = TraderRunMode.REPLAY;
private String symbol = "BTCUSDT";
private String featureVersion = "trader_feature_v0";
private String labelVersion = "trader_label_v0";
private Playbook playbook = new Playbook();
private Replay replay = new Replay();
private Integration integration = new Integration();
private Risk risk = new Risk();
private Sizing sizing = new Sizing();
private DataSource dataSource = new DataSource();
public String getServiceName() {
return serviceName;
public record TraderProperties(
String serviceName,
TraderRunMode runMode,
String symbol,
Artifact artifact,
Feedback feedback,
Execution execution,
Runtime runtime,
Outbox outbox,
Release release,
Risk risk,
PositionManager positionManager
) {
public TraderProperties {
serviceName = defaultText(serviceName, "quant-trader-service");
runMode = runMode == null ? TraderRunMode.SHADOW : runMode;
symbol = defaultText(symbol, "BTC-USDT-PERP");
artifact = artifact == null ? new Artifact("trader-v4-btc-p0", "cal-v4-btc-p0", "pm-v4-btc-p0", ".") : artifact;
feedback = feedback == null ? new Feedback(false) : feedback;
execution = execution == null ? new Execution(TraderExecutionMode.SHADOW, 3, 1500) : execution;
runtime = runtime == null ? new Runtime("trader:v4", true, false) : runtime;
outbox = outbox == null ? new Outbox(true, 5) : outbox;
release = release == null ? new Release(true, true, true) : release;
risk = risk == null ? new Risk(new BigDecimal("200"), BigDecimal.ONE, new BigDecimal("500")) : risk;
positionManager = positionManager == null ? new PositionManager(BigDecimal.ONE, BigDecimal.ONE) : positionManager;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
private static String defaultText(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value;
}
public TraderRunMode getRunMode() {
return runMode;
}
public void setRunMode(TraderRunMode runMode) {
this.runMode = runMode;
}
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public String getFeatureVersion() {
return featureVersion;
}
public void setFeatureVersion(String featureVersion) {
this.featureVersion = featureVersion;
}
public String getLabelVersion() {
return labelVersion;
}
public void setLabelVersion(String labelVersion) {
this.labelVersion = labelVersion;
}
public Playbook getPlaybook() {
return playbook;
}
public void setPlaybook(Playbook playbook) {
this.playbook = playbook;
}
public Replay getReplay() {
return replay;
}
public void setReplay(Replay replay) {
this.replay = replay;
}
public Integration getIntegration() {
return integration;
}
public void setIntegration(Integration integration) {
this.integration = integration;
}
public Risk getRisk() {
return risk;
}
public void setRisk(Risk risk) {
this.risk = risk;
}
public Sizing getSizing() {
return sizing;
}
public void setSizing(Sizing sizing) {
this.sizing = sizing;
}
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public static class Playbook {
private String locationPattern = "classpath:/playbooks/*.yml";
public String getLocationPattern() {
return locationPattern;
}
public void setLocationPattern(String locationPattern) {
this.locationPattern = locationPattern;
public record Artifact(
String modelBundleVersion,
String calibrationBundleVersion,
String pmConfigVersion,
String artifactRoot
) {
public Artifact {
modelBundleVersion = requiredText(modelBundleVersion, "artifact.modelBundleVersion");
calibrationBundleVersion = requiredText(calibrationBundleVersion, "artifact.calibrationBundleVersion");
pmConfigVersion = requiredText(pmConfigVersion, "artifact.pmConfigVersion");
artifactRoot = requiredText(artifactRoot, "artifact.artifactRoot");
}
}
public static class Replay {
private String outputDir = "/Users/zach/Desktop/app/trader/replay-output";
private boolean failOnDataMissing = true;
public record Feedback(boolean httpEnabled) {
}
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public boolean isFailOnDataMissing() {
return failOnDataMissing;
}
public void setFailOnDataMissing(boolean failOnDataMissing) {
this.failOnDataMissing = failOnDataMissing;
public record Execution(TraderExecutionMode mode, int maxApiErrorCount, long maxExchangeLatencyMs) {
public Execution {
mode = mode == null ? TraderExecutionMode.SHADOW : mode;
}
}
public static class Integration {
private String appActionChannel = "JAR_FUTURE";
private boolean httpFeedbackEnabled = false;
public String getAppActionChannel() {
return appActionChannel;
}
public void setAppActionChannel(String appActionChannel) {
this.appActionChannel = appActionChannel;
}
public boolean isHttpFeedbackEnabled() {
return httpFeedbackEnabled;
}
public void setHttpFeedbackEnabled(boolean httpFeedbackEnabled) {
this.httpFeedbackEnabled = httpFeedbackEnabled;
public record Runtime(String redisKeyPrefix, boolean requireRedisForOpenAdd, boolean tradingEnabled) {
public Runtime {
redisKeyPrefix = defaultText(redisKeyPrefix, "trader:v4");
}
}
public static class Risk {
private BigDecimal leverageScreen = BigDecimal.TEN;
private boolean requireOneXNotNegative = true;
private int maxPlannedEntryLegs = 3;
private boolean allowFreeScaleIn = false;
private boolean allowReduceThenAdd = false;
private boolean requireStop = true;
private boolean requireTarget = true;
private boolean requireInvalid = true;
public record Outbox(boolean enabled, int maxRetryCount) {
}
public BigDecimal getLeverageScreen() {
return leverageScreen;
}
public record Release(boolean requireReviewForPaper, boolean requireReviewForLiveProbe, boolean activePointerCheckEnabled) {
}
public void setLeverageScreen(BigDecimal leverageScreen) {
this.leverageScreen = leverageScreen;
}
public boolean isRequireOneXNotNegative() {
return requireOneXNotNegative;
}
public void setRequireOneXNotNegative(boolean requireOneXNotNegative) {
this.requireOneXNotNegative = requireOneXNotNegative;
}
public int getMaxPlannedEntryLegs() {
return maxPlannedEntryLegs;
}
public void setMaxPlannedEntryLegs(int maxPlannedEntryLegs) {
this.maxPlannedEntryLegs = maxPlannedEntryLegs;
}
public boolean isAllowFreeScaleIn() {
return allowFreeScaleIn;
}
public void setAllowFreeScaleIn(boolean allowFreeScaleIn) {
this.allowFreeScaleIn = allowFreeScaleIn;
}
public boolean isAllowReduceThenAdd() {
return allowReduceThenAdd;
}
public void setAllowReduceThenAdd(boolean allowReduceThenAdd) {
this.allowReduceThenAdd = allowReduceThenAdd;
}
public boolean isRequireStop() {
return requireStop;
}
public void setRequireStop(boolean requireStop) {
this.requireStop = requireStop;
}
public boolean isRequireTarget() {
return requireTarget;
}
public void setRequireTarget(boolean requireTarget) {
this.requireTarget = requireTarget;
}
public boolean isRequireInvalid() {
return requireInvalid;
}
public void setRequireInvalid(boolean requireInvalid) {
this.requireInvalid = requireInvalid;
public record Risk(BigDecimal maxDailyLossBps, BigDecimal maxTotalExposureRatio, BigDecimal minLiquidationBufferBps) {
public Risk {
maxDailyLossBps = maxDailyLossBps == null ? new BigDecimal("200") : maxDailyLossBps;
maxTotalExposureRatio = maxTotalExposureRatio == null ? BigDecimal.ONE : maxTotalExposureRatio;
minLiquidationBufferBps = minLiquidationBufferBps == null ? new BigDecimal("500") : minLiquidationBufferBps;
}
}
public static class Sizing {
private String method = "SIGNAL_EXECUTION_RISK_DYNAMIC";
private boolean allowFullInitialEntry = true;
private int maxPlannedEntryLegs = 3;
private BigDecimal maxTotalPositionRatio = BigDecimal.ONE;
private BigDecimal maxSingleLegRatio = BigDecimal.ONE;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public boolean isAllowFullInitialEntry() {
return allowFullInitialEntry;
}
public void setAllowFullInitialEntry(boolean allowFullInitialEntry) {
this.allowFullInitialEntry = allowFullInitialEntry;
}
public int getMaxPlannedEntryLegs() {
return maxPlannedEntryLegs;
}
public void setMaxPlannedEntryLegs(int maxPlannedEntryLegs) {
this.maxPlannedEntryLegs = maxPlannedEntryLegs;
}
public BigDecimal getMaxTotalPositionRatio() {
return maxTotalPositionRatio;
}
public void setMaxTotalPositionRatio(BigDecimal maxTotalPositionRatio) {
this.maxTotalPositionRatio = maxTotalPositionRatio;
}
public BigDecimal getMaxSingleLegRatio() {
return maxSingleLegRatio;
}
public void setMaxSingleLegRatio(BigDecimal maxSingleLegRatio) {
this.maxSingleLegRatio = maxSingleLegRatio;
}
}
public static class DataSource {
private String hashMode = "FULL_HASH_OR_SCHEMA_ROW_TIME_MISSING_SUMMARY";
public String getHashMode() {
return hashMode;
}
public void setHashMode(String hashMode) {
this.hashMode = hashMode;
public record PositionManager(BigDecimal maxSingleLegRatio, BigDecimal maxTotalPositionRatio) {
public PositionManager {
maxSingleLegRatio = maxSingleLegRatio == null ? BigDecimal.ONE : maxSingleLegRatio;
maxTotalPositionRatio = maxTotalPositionRatio == null ? BigDecimal.ONE : maxTotalPositionRatio;
}
}
}
@@ -1,7 +1,6 @@
package com.quantai.trader.controller;
public record TraderApiError(
String code,
String message
) {
import com.quantai.trader.enums.TraderErrorCode;
public record TraderApiError(TraderErrorCode code, String message) {
}
@@ -1,21 +1,19 @@
package com.quantai.trader.controller;
import com.quantai.trader.domain.TraderException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class TraderApiExceptionHandler {
@ExceptionHandler(TraderException.class)
public ResponseEntity<TraderApiError> handleTraderException(TraderException ex) {
return ResponseEntity.badRequest().body(new TraderApiError(ex.errorCode().name(), ex.getMessage()));
ResponseEntity<TraderApiError> traderException(TraderException exception) {
return ResponseEntity.badRequest().body(new TraderApiError(exception.code(), exception.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<TraderApiError> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new TraderApiError("TRADER_NOT_FOUND", ex.getMessage()));
ResponseEntity<TraderApiError> illegalArgument(IllegalArgumentException exception) {
return ResponseEntity.badRequest().body(new TraderApiError(com.quantai.trader.enums.TraderErrorCode.TRADER_MODEL_OUTPUT_INVALID, exception.getMessage()));
}
}
@@ -1,48 +1,37 @@
package com.quantai.trader.controller;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.domain.FeedbackValidator;
import com.quantai.trader.domain.TraderAppFeedback;
import com.quantai.trader.domain.TraderException;
import com.quantai.trader.enums.TraderErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/trader/feedback")
public class TraderFeedbackController {
private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class);
private final TraderProperties properties;
private final FeedbackValidator feedbackValidator;
public TraderFeedbackController(TraderProperties properties) {
public TraderFeedbackController(TraderProperties properties, FeedbackValidator feedbackValidator) {
this.properties = properties;
this.feedbackValidator = feedbackValidator;
}
@PostMapping
public ResponseEntity<?> feedback(@RequestBody TraderFeedbackRequest request) {
if (!properties.getIntegration().isHttpFeedbackEnabled()) {
log.info(
"event=trader.feedback.rejected runId={} cycleId={} actionId={} reason={}",
request.runId(),
request.cycleId(),
request.actionId(),
TraderErrorCode.TRADER_FEEDBACK_DISABLED
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new TraderApiError(
TraderErrorCode.TRADER_FEEDBACK_DISABLED.name(),
"P0 feedback endpoint is disabled; future App main channel is trader-core jar"
));
@PostMapping("/api/trader/feedback")
public Map<String, Object> feedback(@RequestBody TraderAppFeedback feedback) {
if (!properties.feedback().httpEnabled()) {
throw new TraderException(TraderErrorCode.TRADER_FEEDBACK_INVALID, "HTTP feedback is disabled in P0");
}
return ResponseEntity.accepted().body(Map.of(
"status", "ACCEPTED_CONTRACT_ONLY",
"runId", request.runId(),
"actionId", request.actionId()
));
feedbackValidator.validateP0(feedback);
log.info("event=trader.feedback.accepted runId={} cycleId={} actionId={} source={}",
feedback.runId(), feedback.cycleId(), feedback.actionId(), feedback.feedbackSource());
return Map.of("accepted", true, "feedbackId", feedback.feedbackId());
}
}
@@ -1,26 +0,0 @@
package com.quantai.trader.controller;
import com.quantai.trader.enums.TraderFeedbackSource;
import com.quantai.trader.enums.TraderFeedbackType;
import java.math.BigDecimal;
import java.util.Map;
public record TraderFeedbackRequest(
String runId,
String cycleId,
String actionId,
TraderFeedbackType feedbackType,
TraderFeedbackSource feedbackSource,
boolean realFill,
String proxyMethod,
String simulatorVersion,
String orderId,
String positionId,
BigDecimal filledPrice,
BigDecimal filledQuantity,
BigDecimal fee,
BigDecimal slippageBps,
Map<String, Object> rawFeedback
) {
}
@@ -1,33 +1,28 @@
package com.quantai.trader.controller;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.playbook.TraderPlaybookCatalog;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/trader")
public class TraderHealthController {
private final TraderProperties properties;
private final TraderPlaybookCatalog catalog;
public TraderHealthController(TraderProperties properties, TraderPlaybookCatalog catalog) {
public TraderHealthController(TraderProperties properties) {
this.properties = properties;
this.catalog = catalog;
}
@GetMapping("/health")
@GetMapping("/api/trader/health")
public Map<String, Object> health() {
return Map.of(
"service", properties.getServiceName(),
"runMode", properties.getRunMode(),
"symbol", properties.getSymbol(),
"playbookCount", catalog.list().size(),
"httpFeedbackEnabled", properties.getIntegration().isHttpFeedbackEnabled()
);
"status", "UP",
"runMode", properties.runMode(),
"executionMode", properties.execution().mode(),
"modelBundleVersion", properties.artifact().modelBundleVersion(),
"calibrationBundleVersion", properties.artifact().calibrationBundleVersion(),
"pmConfigVersion", properties.artifact().pmConfigVersion(),
"tradingEnabled", properties.runtime().tradingEnabled());
}
}
@@ -1,52 +0,0 @@
package com.quantai.trader.controller;
import com.quantai.trader.playbook.TraderPlaybookCatalog;
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/trader/playbooks")
public class TraderPlaybookController {
private final TraderPlaybookCatalog catalog;
public TraderPlaybookController(TraderPlaybookCatalog catalog) {
this.catalog = catalog;
}
@GetMapping
public List<PlaybookResponse> list() {
return catalog.list().stream().map(PlaybookResponse::from).toList();
}
@GetMapping("/{playbookId}")
public PlaybookResponse get(@PathVariable String playbookId) {
return PlaybookResponse.from(catalog.require(playbookId));
}
public record PlaybookResponse(
String playbookId,
String playbookVersion,
String family,
String variant,
String definitionHashSha256,
List<String> outputActions
) {
static PlaybookResponse from(TraderPlaybookDefinitionSnapshot snapshot) {
return new PlaybookResponse(
snapshot.playbookId(),
snapshot.playbookVersion(),
snapshot.family(),
snapshot.variant(),
snapshot.definitionHashSha256(),
snapshot.definition().outputActions()
);
}
}
}
@@ -1,52 +1,22 @@
package com.quantai.trader.controller;
import com.quantai.trader.domain.TraderReplayReport;
import com.quantai.trader.persistence.ReplayReportRepository;
import com.quantai.trader.replay.ReplayRun;
import com.quantai.trader.replay.ReplayRunConfig;
import com.quantai.trader.replay.ReplayRunResponse;
import com.quantai.trader.replay.ReplayRunService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.quantai.trader.replay.ReplayMarketEvent;
import com.quantai.trader.replay.TraderCycleResult;
import com.quantai.trader.replay.TraderP0CycleRunner;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/trader/replay/runs")
public class TraderReplayController {
private final TraderP0CycleRunner runner;
private final ReplayRunService replayRunService;
private final ReplayReportRepository reportRepository;
public TraderReplayController(ReplayRunService replayRunService, ReplayReportRepository reportRepository) {
this.replayRunService = replayRunService;
this.reportRepository = reportRepository;
public TraderReplayController(TraderP0CycleRunner runner) {
this.runner = runner;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ReplayRunResponse create(@RequestBody ReplayRunConfig config) {
return replayRunService.createRun(config);
}
@GetMapping("/{runId}")
public ReplayRun get(@PathVariable String runId) {
return replayRunService.find(runId)
.orElseThrow(() -> new IllegalArgumentException("replay run not found: " + runId));
}
@PostMapping("/{runId}/cancel")
public ReplayRun cancel(@PathVariable String runId) {
return replayRunService.cancel(runId);
}
@GetMapping("/{runId}/report")
public TraderReplayReport report(@PathVariable String runId) {
return reportRepository.findByRunId(runId)
.orElseThrow(() -> new IllegalArgumentException("replay report not found: " + runId));
@PostMapping("/api/trader/replay/cycles")
public TraderCycleResult runOneCycle(@RequestBody ReplayMarketEvent event) {
return runner.runFlatCycle(event);
}
}
@@ -0,0 +1,23 @@
package com.quantai.trader.core;
import com.quantai.trader.domain.*;
import java.time.Instant;
import java.util.Map;
public record TraderCoreDecisionRequest(
String requestId,
String runId,
String cycleId,
String symbol,
Instant eventTime,
TraderMarketSnapshot snapshot,
TraderPositionState positionState,
TraderAccountState accountState,
TraderExecutionState executionState,
String modelBundleVersion,
String calibrationBundleVersion,
String pmConfigVersion,
Map<String, Object> requestContextJson
) {
}
@@ -0,0 +1,19 @@
package com.quantai.trader.core;
import com.quantai.trader.domain.TraderAction;
import java.util.Map;
public record TraderCoreDecisionResponse(
String responseId,
String requestId,
String cycleId,
String modelOutputId,
String pmDecisionId,
String riskDecisionId,
boolean actionAllowed,
TraderAction action,
String blocker,
Map<String, Object> responseContextJson
) {
}
@@ -0,0 +1,24 @@
package com.quantai.trader.core;
import com.quantai.trader.enums.FeedbackSource;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
public record TraderCoreFeedbackEvent(
String feedbackId,
String actionId,
FeedbackSource feedbackSource,
boolean realFill,
String orderId,
String orderStatus,
BigDecimal filledPrice,
BigDecimal filledQuantity,
BigDecimal fee,
BigDecimal slippageBps,
String rejectReason,
Instant eventTime,
Map<String, Object> rawFeedbackJson
) {
}
@@ -0,0 +1,11 @@
package com.quantai.trader.core;
public record TraderCoreHealth(
boolean ready,
String runMode,
String modelBundleVersion,
String calibrationBundleVersion,
String pmConfigVersion,
String blocker
) {
}
@@ -0,0 +1,28 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.util.Map;
public record ContinueOutput(
BigDecimal longContinueProb,
BigDecimal shortContinueProb,
BigDecimal trendPersistenceProb,
BigDecimal holdEdgeBps,
BigDecimal continueVsExitEdgeBps,
String modelVersion,
String calibrationVersion,
Map<String, Object> explanation
) {
public ContinueOutput {
longContinueProb = probability(longContinueProb, "continue.longContinueProb");
shortContinueProb = probability(shortContinueProb, "continue.shortContinueProb");
trendPersistenceProb = probability(trendPersistenceProb, "continue.trendPersistenceProb");
holdEdgeBps = required(holdEdgeBps, "continue.holdEdgeBps");
continueVsExitEdgeBps = required(continueVsExitEdgeBps, "continue.continueVsExitEdgeBps");
modelVersion = requiredText(modelVersion, "continue.modelVersion");
calibrationVersion = requiredText(calibrationVersion, "continue.calibrationVersion");
explanation = Map.copyOf(explanation == null ? Map.of() : explanation);
}
}
@@ -0,0 +1,34 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.util.Map;
public record DirectionOutput(
BigDecimal longProb,
BigDecimal shortProb,
BigDecimal neutralProb,
BigDecimal directionConfidence,
BigDecimal directionMargin,
BigDecimal expectedReturnBps,
Integer horizonMinutes,
String modelVersion,
String calibrationVersion,
Map<String, Object> explanation
) {
public DirectionOutput {
longProb = probability(longProb, "direction.longProb");
shortProb = probability(shortProb, "direction.shortProb");
neutralProb = probability(neutralProb, "direction.neutralProb");
directionConfidence = probability(directionConfidence, "direction.directionConfidence");
directionMargin = nonNegative(directionMargin, "direction.directionMargin");
expectedReturnBps = required(expectedReturnBps, "direction.expectedReturnBps");
if (horizonMinutes == null || horizonMinutes <= 0) {
throw new IllegalArgumentException("direction.horizonMinutes must be > 0");
}
modelVersion = requiredText(modelVersion, "direction.modelVersion");
calibrationVersion = requiredText(calibrationVersion, "direction.calibrationVersion");
explanation = Map.copyOf(explanation == null ? Map.of() : explanation);
}
}
@@ -0,0 +1,40 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.util.Map;
public record EntryOutput(
BigDecimal longEntryProb,
BigDecimal shortEntryProb,
BigDecimal entryQualityScore,
BigDecimal expectedEdgeBps,
String pricePlanId,
String pricePlanConfigHash,
BigDecimal stopDistanceBps,
BigDecimal targetDistanceBps,
Integer maxHoldMinutes,
BigDecimal costBps,
String modelVersion,
String calibrationVersion,
Map<String, Object> explanation
) {
public EntryOutput {
longEntryProb = probability(longEntryProb, "entry.longEntryProb");
shortEntryProb = probability(shortEntryProb, "entry.shortEntryProb");
entryQualityScore = probability(entryQualityScore, "entry.entryQualityScore");
expectedEdgeBps = required(expectedEdgeBps, "entry.expectedEdgeBps");
pricePlanId = requiredText(pricePlanId, "entry.pricePlanId");
pricePlanConfigHash = requiredText(pricePlanConfigHash, "entry.pricePlanConfigHash");
stopDistanceBps = positive(stopDistanceBps, "entry.stopDistanceBps");
targetDistanceBps = positive(targetDistanceBps, "entry.targetDistanceBps");
if (maxHoldMinutes == null || maxHoldMinutes <= 0) {
throw new IllegalArgumentException("entry.maxHoldMinutes must be > 0");
}
costBps = nonNegative(costBps, "entry.costBps");
modelVersion = requiredText(modelVersion, "entry.modelVersion");
calibrationVersion = requiredText(calibrationVersion, "entry.calibrationVersion");
explanation = Map.copyOf(explanation == null ? Map.of() : explanation);
}
}
@@ -1,21 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
import java.util.Map;
public record ExecutionDecision(
boolean pass,
BigDecimal executionQualityScore,
String reason,
String blocker,
Map<String, Object> details
) {
public ExecutionDecision {
details = Maps.immutable(details);
}
public boolean blocked() {
return !pass;
}
}
@@ -0,0 +1,32 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.util.Map;
public record ExitOutput(
BigDecimal longExitProb,
BigDecimal shortExitProb,
BigDecimal profitGivebackProb,
BigDecimal reversalProb,
BigDecimal stopRiskProb,
BigDecimal stagnationProb,
BigDecimal expectedGivebackBps,
String modelVersion,
String calibrationVersion,
Map<String, Object> explanation
) {
public ExitOutput {
longExitProb = probability(longExitProb, "exit.longExitProb");
shortExitProb = probability(shortExitProb, "exit.shortExitProb");
profitGivebackProb = probability(profitGivebackProb, "exit.profitGivebackProb");
reversalProb = probability(reversalProb, "exit.reversalProb");
stopRiskProb = probability(stopRiskProb, "exit.stopRiskProb");
stagnationProb = probability(stagnationProb, "exit.stagnationProb");
expectedGivebackBps = nonNegative(expectedGivebackBps, "exit.expectedGivebackBps");
modelVersion = requiredText(modelVersion, "exit.modelVersion");
calibrationVersion = requiredText(calibrationVersion, "exit.calibrationVersion");
explanation = Map.copyOf(explanation == null ? Map.of() : explanation);
}
}
@@ -0,0 +1,14 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderErrorCode;
import org.springframework.stereotype.Component;
@Component
public class FeedbackValidator {
public void validateP0(TraderAppFeedback feedback) {
if (!feedback.feedbackSource().p0Allowed() || feedback.realFill()) {
throw new TraderException(TraderErrorCode.TRADER_FEEDBACK_INVALID,
"P0 rejects PAPER_APP/REAL_APP and any realFill feedback");
}
}
}
@@ -1,16 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderActionType;
import java.util.Map;
public record ManagementDecision(
TraderActionType actionType,
String reason,
Map<String, Object> details
) {
public ManagementDecision {
details = Maps.immutable(details);
}
}
@@ -1,17 +0,0 @@
package com.quantai.trader.domain;
import java.util.LinkedHashMap;
import java.util.Map;
final class Maps {
private Maps() {
}
static Map<String, Object> immutable(Map<String, Object> value) {
if (value == null || value.isEmpty()) {
return Map.of();
}
return Map.copyOf(new LinkedHashMap<>(value));
}
}
@@ -0,0 +1,10 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.requiredText;
public record OpenOrderState(String orderId, String status) {
public OpenOrderState {
orderId = requiredText(orderId, "orderId");
status = requiredText(status, "status");
}
}
@@ -1,25 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderSide;
import java.time.Instant;
import java.util.Map;
public record PlaybookCandidate(
String runId,
String cycleId,
String candidateId,
String playbookId,
String playbookVersion,
TraderSide side,
String variant,
Instant candidateTime,
TraderPricePlan pricePlan,
int maxPlannedEntryLegs,
Map<String, Object> setupEvidence
) {
public PlaybookCandidate {
setupEvidence = Maps.immutable(setupEvidence);
}
}
@@ -0,0 +1,23 @@
package com.quantai.trader.domain;
import java.util.Objects;
public record PositionManagerInput(
TraderDecisionCycle cycle,
TraderMarketSnapshot snapshot,
TraderModelOutput modelOutput,
TraderPositionState positionState,
TraderAccountState accountState,
TraderExecutionState executionState,
TraderPmConfig pmConfig
) {
public PositionManagerInput {
cycle = Objects.requireNonNull(cycle, "cycle is required");
snapshot = Objects.requireNonNull(snapshot, "snapshot is required");
modelOutput = Objects.requireNonNull(modelOutput, "modelOutput is required");
positionState = Objects.requireNonNull(positionState, "positionState is required");
accountState = Objects.requireNonNull(accountState, "accountState is required");
executionState = Objects.requireNonNull(executionState, "executionState is required");
pmConfig = Objects.requireNonNull(pmConfig, "pmConfig is required");
}
}
@@ -1,14 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
public record PositionSizingPlan(
int plannedLegCount,
BigDecimal initialLegRatio,
BigDecimal nextLegRatio,
String sizingMethod,
BigDecimal signalStrengthScore,
BigDecimal executionQualityScore,
BigDecimal riskGateScore
) {
}
@@ -1,20 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
import java.util.Map;
public record RiskDecision(
boolean allowAction,
String blocker,
BigDecimal riskGateScore,
Map<String, Object> details
) {
public RiskDecision {
details = Maps.immutable(details);
}
public boolean blocked() {
return !allowAction;
}
}
@@ -0,0 +1,38 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.util.Map;
public record RiskOutput(
BigDecimal marketRiskProb,
BigDecimal positionRiskProb,
BigDecimal marketRiskSeverityBps,
BigDecimal positionRiskSeverityBps,
BigDecimal drawdownProb,
BigDecimal expectedShortfallBps,
BigDecimal volatilityExpansionProb,
BigDecimal spikeProb,
BigDecimal liquidityRiskProb,
BigDecimal liquidityCapacityRatio,
String modelVersion,
String calibrationVersion,
Map<String, Object> explanation
) {
public RiskOutput {
marketRiskProb = probability(marketRiskProb, "risk.marketRiskProb");
positionRiskProb = probability(positionRiskProb, "risk.positionRiskProb");
marketRiskSeverityBps = nonNegative(marketRiskSeverityBps, "risk.marketRiskSeverityBps");
positionRiskSeverityBps = nonNegative(positionRiskSeverityBps, "risk.positionRiskSeverityBps");
drawdownProb = probability(drawdownProb, "risk.drawdownProb");
expectedShortfallBps = nonNegative(expectedShortfallBps, "risk.expectedShortfallBps");
volatilityExpansionProb = probability(volatilityExpansionProb, "risk.volatilityExpansionProb");
spikeProb = probability(spikeProb, "risk.spikeProb");
liquidityRiskProb = probability(liquidityRiskProb, "risk.liquidityRiskProb");
liquidityCapacityRatio = nonNegative(liquidityCapacityRatio, "risk.liquidityCapacityRatio");
modelVersion = requiredText(modelVersion, "risk.modelVersion");
calibrationVersion = requiredText(calibrationVersion, "risk.calibrationVersion");
explanation = Map.copyOf(explanation == null ? Map.of() : explanation);
}
}
@@ -1,27 +0,0 @@
package com.quantai.trader.domain;
import java.util.Map;
public record StageDecision(
boolean pass,
String reason,
String blocker,
Map<String, Object> details
) {
public StageDecision {
details = Maps.immutable(details);
}
public boolean blocked() {
return !pass;
}
public static StageDecision pass(String reason) {
return new StageDecision(true, reason, null, Map.of());
}
public static StageDecision block(String reason, String blocker) {
return new StageDecision(false, reason, blocker, Map.of());
}
}
@@ -0,0 +1,27 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
public record TraderAccountState(
String accountStateId,
String runId,
String cycleId,
BigDecimal dailyDrawdownBps,
BigDecimal portfolioExposureRatio,
BigDecimal remainingSymbolCapacityRatio,
int consecutiveLosses
) {
public TraderAccountState {
accountStateId = requiredText(accountStateId, "accountStateId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
dailyDrawdownBps = nonNegative(dailyDrawdownBps, "dailyDrawdownBps");
portfolioExposureRatio = nonNegative(portfolioExposureRatio, "portfolioExposureRatio");
remainingSymbolCapacityRatio = nonNegative(remainingSymbolCapacityRatio, "remainingSymbolCapacityRatio");
if (consecutiveLosses < 0) {
throw new IllegalArgumentException("consecutiveLosses must be >= 0");
}
}
}
@@ -1,30 +1,54 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderActionType;
import com.quantai.trader.enums.TraderSide;
import static com.quantai.trader.util.TraderNumbers.*;
import com.quantai.trader.enums.PositionSide;
import com.quantai.trader.enums.TraderActionType;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public record TraderAction(
String actionId,
String runId,
String cycleId,
String actionId,
String modelOutputId,
String pmDecisionId,
String riskDecisionId,
TraderActionType actionType,
String playbookId,
String playbookVersion,
String symbol,
TraderSide side,
BigDecimal price,
PositionSide side,
String pricePlanId,
String pricePlanConfigHash,
BigDecimal positionRatio,
BigDecimal quantity,
Instant actionTime,
BigDecimal stopPrice,
BigDecimal targetPrice,
boolean reduceOnly,
String idempotencyKey,
String reason,
Map<String, Object> actionContext,
String sendStatus
Map<String, Object> actionContextJson
) {
public TraderAction {
actionContext = Maps.immutable(actionContext);
actionId = requiredText(actionId, "actionId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
modelOutputId = requiredText(modelOutputId, "modelOutputId");
pmDecisionId = requiredText(pmDecisionId, "pmDecisionId");
riskDecisionId = requiredText(riskDecisionId, "riskDecisionId");
actionType = Objects.requireNonNull(actionType, "actionType is required");
symbol = requiredText(symbol, "symbol");
side = Objects.requireNonNull(side, "side is required");
idempotencyKey = requiredText(idempotencyKey, "idempotencyKey");
reason = requiredText(reason, "reason");
actionContextJson = Map.copyOf(actionContextJson == null ? Map.of() : actionContextJson);
if (actionType.increasesExposure()) {
pricePlanId = requiredText(pricePlanId, "pricePlanId");
pricePlanConfigHash = requiredText(pricePlanConfigHash, "pricePlanConfigHash");
positionRatio = positive(positionRatio, "positionRatio");
}
if (actionType.reducesExposure() && !reduceOnly) {
throw new IllegalArgumentException("reduce/close action must be reduceOnly");
}
}
}
@@ -0,0 +1,44 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.PositionSide;
import com.quantai.trader.enums.TraderActionType;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class TraderActionFactory {
public TraderAction create(TraderPositionManagerDecision pmDecision, TraderRiskDecision riskDecision, String symbol) {
TraderActionType finalAction = riskDecision.finalAction();
PositionSide side = sideFor(finalAction, pmDecision.side());
return new TraderAction(
"action_" + pmDecision.cycleId(),
pmDecision.runId(),
pmDecision.cycleId(),
pmDecision.modelOutputId(),
pmDecision.pmDecisionId(),
riskDecision.riskDecisionId(),
finalAction,
symbol,
side,
finalAction.increasesExposure() ? pmDecision.pricePlanId() : null,
finalAction.increasesExposure() ? pmDecision.pricePlanConfigHash() : null,
finalAction == TraderActionType.OPEN_LONG || finalAction == TraderActionType.OPEN_SHORT ? pmDecision.targetPositionRatio() : pmDecision.addRatio(),
null,
pmDecision.stopPrice(),
pmDecision.targetPrice(),
finalAction.reducesExposure(),
"idem_" + pmDecision.runId() + "_" + pmDecision.cycleId() + "_" + finalAction,
riskDecision.allowAction() ? pmDecision.reason() : riskDecision.blocker(),
Map.of("riskAllowed", riskDecision.allowAction()));
}
private PositionSide sideFor(TraderActionType action, PositionSide pmSide) {
return switch (action) {
case OPEN_LONG, ADD_LONG, REDUCE_LONG, CLOSE_LONG -> PositionSide.LONG;
case OPEN_SHORT, ADD_SHORT, REDUCE_SHORT, CLOSE_SHORT -> PositionSide.SHORT;
case WAIT, CANCEL -> PositionSide.NONE;
case HOLD, MOVE_STOP -> pmSide;
};
}
}
@@ -1,23 +1,21 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderFeedbackSource;
import com.quantai.trader.enums.TraderFeedbackType;
import static com.quantai.trader.util.TraderNumbers.*;
import com.quantai.trader.enums.FeedbackSource;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public record TraderAppFeedback(
String feedbackId,
String runId,
String cycleId,
String actionId,
TraderFeedbackType feedbackType,
TraderFeedbackSource feedbackSource,
String proxyMethod,
String simulatorVersion,
FeedbackSource feedbackSource,
boolean realFill,
String orderId,
String positionId,
String orderStatus,
Instant appReceivedTime,
Instant exchangeAckTime,
@@ -26,19 +24,24 @@ public record TraderAppFeedback(
BigDecimal filledQuantity,
BigDecimal fee,
BigDecimal slippageBps,
String closeReason,
String closeSignalSource,
String exchangeErrorCode,
String platformErrorCode,
Map<String, Object> rawFeedback
String rejectReason,
Map<String, Object> rawFeedbackJson
) {
public TraderAppFeedback {
rawFeedback = Maps.immutable(rawFeedback);
boolean sourceCanBeReal = feedbackSource == TraderFeedbackSource.PAPER_APP
|| feedbackSource == TraderFeedbackSource.REAL_APP;
if (realFill != sourceCanBeReal) {
throw new IllegalArgumentException("feedback_source and realFill are inconsistent");
feedbackId = requiredText(feedbackId, "feedbackId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
actionId = requiredText(actionId, "actionId");
feedbackSource = Objects.requireNonNull(feedbackSource, "feedbackSource is required");
if (realFill && !feedbackSource.canBeRealFill()) {
throw new IllegalArgumentException("realFill requires PAPER_APP or REAL_APP");
}
if (!realFill && feedbackSource.canBeRealFill()) {
throw new IllegalArgumentException("PAPER_APP/REAL_APP feedback must be realFill");
}
if (filledQuantity != null && filledQuantity.compareTo(ZERO) > 0) {
filledPrice = positive(filledPrice, "filledPrice");
}
rawFeedbackJson = Map.copyOf(rawFeedbackJson == null ? Map.of() : rawFeedbackJson);
}
}
@@ -1,48 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderErrorCode;
import java.time.Instant;
import java.util.Map;
public record TraderDataSourceManifest(
String sourceId,
String symbol,
String sourceType,
String exchange,
String granularity,
String sourcePath,
String contentHashSha256,
String schemaHashSha256,
Instant dataFrom,
Instant dataTo,
Instant minEventTime,
Instant maxEventTime,
String timezone,
Long rowCount,
Map<String, Object> missingSummary,
String qualityStatus
) {
public TraderDataSourceManifest {
missingSummary = Maps.immutable(missingSummary);
boolean hasFullHash = contentHashSha256 != null && !contentHashSha256.isBlank();
boolean hasSchemaTrace = schemaHashSha256 != null
&& !schemaHashSha256.isBlank()
&& rowCount != null
&& minEventTime != null
&& maxEventTime != null;
if (sourceId == null || sourceId.isBlank() || sourcePath == null || sourcePath.isBlank()) {
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source id and path are required");
}
if (timezone == null || timezone.isBlank()) {
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source timezone is required");
}
if (!hasFullHash && !hasSchemaTrace) {
throw new TraderException(
TraderErrorCode.TRADER_DATA_SOURCE_MISSING,
"data source requires content hash or schema/row/time lineage"
);
}
}
}
@@ -1,37 +1,29 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderRunMode;
import com.quantai.trader.enums.TraderState;
import static com.quantai.trader.util.TraderNumbers.requiredText;
import com.quantai.trader.enums.TraderRunMode;
import java.time.Instant;
import java.util.Objects;
public record TraderDecisionCycle(
String runId,
String cycleId,
String snapshotId,
String symbol,
String playbookId,
String playbookVersion,
TraderState state,
Instant cycleTime,
TraderRunMode runMode,
String decisionStatus,
String blocker
String modelBundleVersion,
String calibrationBundleVersion,
String pmConfigVersion
) {
public TraderDecisionCycle withState(TraderState nextState, String nextStatus, String nextBlocker) {
return new TraderDecisionCycle(
runId,
cycleId,
snapshotId,
symbol,
playbookId,
playbookVersion,
nextState,
cycleTime,
runMode,
nextStatus,
nextBlocker
);
public TraderDecisionCycle {
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
symbol = requiredText(symbol, "symbol");
cycleTime = Objects.requireNonNull(cycleTime, "cycleTime is required");
runMode = Objects.requireNonNull(runMode, "runMode is required");
modelBundleVersion = requiredText(modelBundleVersion, "modelBundleVersion");
calibrationBundleVersion = requiredText(calibrationBundleVersion, "calibrationBundleVersion");
pmConfigVersion = requiredText(pmConfigVersion, "pmConfigVersion");
}
}
@@ -1,39 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderActionType;
import java.math.BigDecimal;
public record TraderEntryPlan(
String runId,
String cycleId,
String actionId,
String entryLegId,
String candidateId,
TraderActionType entryAction,
int plannedLegIndex,
int plannedLegCount,
BigDecimal plannedLegRatio,
String sizingMethod,
BigDecimal signalStrengthScore,
BigDecimal executionQualityScore,
BigDecimal riskGateScore,
BigDecimal entryPrice,
BigDecimal invalidPrice,
BigDecimal stopPrice,
BigDecimal targetPrice,
BigDecimal partialTakeProfitPrice,
long maxEntryWaitMs,
long maxHoldMs,
String reason
) {
public boolean completeForEntry() {
return entryPrice != null
&& invalidPrice != null
&& stopPrice != null
&& targetPrice != null
&& maxEntryWaitMs > 0
&& maxHoldMs > 0;
}
}
@@ -1,21 +1,29 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.requiredText;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public record TraderEvidence(
String evidenceId,
String runId,
String cycleId,
String evidenceId,
String stage,
boolean pass,
String reason,
String blocker,
Instant evidenceTime,
Map<String, Object> details
Map<String, Object> detailsJson
) {
public TraderEvidence {
details = Maps.immutable(details);
evidenceId = requiredText(evidenceId, "evidenceId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
stage = requiredText(stage, "stage");
reason = requiredText(reason, "reason");
evidenceTime = Objects.requireNonNull(evidenceTime, "evidenceTime is required");
detailsJson = Map.copyOf(detailsJson == null ? Map.of() : detailsJson);
}
}
@@ -3,15 +3,14 @@ package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderErrorCode;
public class TraderException extends RuntimeException {
private final TraderErrorCode code;
private final TraderErrorCode errorCode;
public TraderException(TraderErrorCode errorCode, String message) {
public TraderException(TraderErrorCode code, String message) {
super(message);
this.errorCode = errorCode;
this.code = code;
}
public TraderErrorCode errorCode() {
return errorCode;
public TraderErrorCode code() {
return code;
}
}
@@ -0,0 +1,46 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.util.List;
public record TraderExecutionState(
String executionStateId,
String runId,
String cycleId,
String symbol,
List<OpenOrderState> openOrders,
BigDecimal expectedSlippageBps,
long exchangeLatencyMs,
int apiErrorCount,
BigDecimal makerFeeBps,
BigDecimal takerFeeBps,
BigDecimal minNotional,
BigDecimal priceTickSize,
BigDecimal lotSizeStepSize,
BigDecimal marketLotSizeStepSize,
BigDecimal liquidityCapacityRatio
) {
public TraderExecutionState {
executionStateId = requiredText(executionStateId, "executionStateId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
symbol = requiredText(symbol, "symbol");
openOrders = List.copyOf(openOrders == null ? List.of() : openOrders);
expectedSlippageBps = nonNegative(expectedSlippageBps, "expectedSlippageBps");
if (exchangeLatencyMs < 0) {
throw new IllegalArgumentException("exchangeLatencyMs must be >= 0");
}
if (apiErrorCount < 0) {
throw new IllegalArgumentException("apiErrorCount must be >= 0");
}
makerFeeBps = nonNegative(makerFeeBps, "makerFeeBps");
takerFeeBps = nonNegative(takerFeeBps, "takerFeeBps");
minNotional = positive(minNotional, "minNotional");
priceTickSize = positive(priceTickSize, "priceTickSize");
lotSizeStepSize = positive(lotSizeStepSize, "lotSizeStepSize");
marketLotSizeStepSize = positive(marketLotSizeStepSize, "marketLotSizeStepSize");
liquidityCapacityRatio = nonNegative(liquidityCapacityRatio, "liquidityCapacityRatio");
}
}
@@ -1,26 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderActionType;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
public record TraderManagementAction(
String runId,
String cycleId,
String actionId,
String managementActionId,
String positionId,
TraderActionType managementActionType,
Instant actionTime,
BigDecimal beforeRiskBps,
BigDecimal afterRiskBps,
String reason,
Map<String, Object> details
) {
public TraderManagementAction {
details = Maps.immutable(details);
}
}
@@ -1,19 +0,0 @@
package com.quantai.trader.domain;
import java.time.Instant;
import java.util.Map;
public record TraderMarketEvent(
String runId,
String eventId,
String symbol,
Instant eventTime,
String source,
String sourcePath,
Map<String, Object> payload
) {
public TraderMarketEvent {
payload = Maps.immutable(payload);
}
}
@@ -1,29 +1,44 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
public record TraderMarketSnapshot(
String snapshotId,
String runId,
String cycleId,
String snapshotId,
String symbol,
Instant snapshotTime,
String featureVersion,
Map<String, Object> contextFeatures,
Map<String, Object> setupFeatures,
Map<String, Object> triggerFeatures,
Map<String, Object> executionFeatures,
Map<String, Object> dataQuality,
Map<String, Object> labelInputs
BigDecimal markPrice,
BigDecimal indexPrice,
BigDecimal spreadBps,
BigDecimal fundingRateBps,
BigDecimal depthNotional5Bps,
BigDecimal depthNotional10Bps,
BigDecimal depthNotional25Bps,
boolean dataReady,
Map<String, Object> featureJson,
Map<String, Object> dataQualityJson
) {
public TraderMarketSnapshot {
contextFeatures = Maps.immutable(contextFeatures);
setupFeatures = Maps.immutable(setupFeatures);
triggerFeatures = Maps.immutable(triggerFeatures);
executionFeatures = Maps.immutable(executionFeatures);
dataQuality = Maps.immutable(dataQuality);
labelInputs = Maps.immutable(labelInputs);
snapshotId = requiredText(snapshotId, "snapshotId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
symbol = requiredText(symbol, "symbol");
snapshotTime = java.util.Objects.requireNonNull(snapshotTime, "snapshotTime 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,20 +0,0 @@
package com.quantai.trader.domain;
import java.time.Instant;
import java.util.Map;
public record TraderModelManifest(
String modelName,
String modelVersion,
String featureVersion,
String labelVersion,
String artifactPath,
Instant trainedAt,
Map<String, Object> metrics,
String status
) {
public TraderModelManifest {
metrics = Maps.immutable(metrics);
}
}
@@ -1,20 +1,39 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public record TraderModelOutput(
String modelName,
String modelVersion,
BigDecimal score,
String modelOutputId,
String runId,
String cycleId,
String modelBundleVersion,
String calibrationBundleVersion,
DirectionOutput direction,
EntryOutput entry,
ContinueOutput continuation,
ExitOutput exit,
RiskOutput risk,
BigDecimal uncertainty,
BigDecimal oodScore,
Instant predictedAt,
Map<String, Object> details
Map<String, Object> explanation
) {
public TraderModelOutput {
details = Maps.immutable(details);
modelOutputId = requiredText(modelOutputId, "modelOutputId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
modelBundleVersion = requiredText(modelBundleVersion, "modelBundleVersion");
calibrationBundleVersion = requiredText(calibrationBundleVersion, "calibrationBundleVersion");
direction = Objects.requireNonNull(direction, "direction is required");
entry = Objects.requireNonNull(entry, "entry is required");
continuation = Objects.requireNonNull(continuation, "continuation is required");
exit = Objects.requireNonNull(exit, "exit is required");
risk = Objects.requireNonNull(risk, "risk is required");
uncertainty = probability(uncertainty, "uncertainty");
oodScore = probability(oodScore, "oodScore");
explanation = Map.copyOf(explanation == null ? Map.of() : explanation);
}
}
@@ -0,0 +1,130 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import java.math.BigDecimal;
public record TraderPmConfig(
String pmConfigVersion,
OpenRuleConfig open,
AddRuleConfig add,
ExitRuleConfig exit,
SizingConfig sizing
) {
public TraderPmConfig {
pmConfigVersion = requiredText(pmConfigVersion, "pmConfigVersion");
open = java.util.Objects.requireNonNull(open, "open config is required");
add = java.util.Objects.requireNonNull(add, "add config is required");
exit = java.util.Objects.requireNonNull(exit, "exit config is required");
sizing = java.util.Objects.requireNonNull(sizing, "sizing config is required");
}
public record OpenRuleConfig(
BigDecimal longOpenProb,
BigDecimal shortOpenProb,
BigDecimal minLongEntryProb,
BigDecimal minShortEntryProb,
BigDecimal maxMarketRiskProb,
BigDecimal minExpectedEdgeBps,
BigDecimal minDirectionMargin,
BigDecimal minLiquidityCapacityRatio,
BigDecimal maxOodScore
) {
public OpenRuleConfig {
longOpenProb = probability(longOpenProb, "open.longOpenProb");
shortOpenProb = probability(shortOpenProb, "open.shortOpenProb");
minLongEntryProb = probability(minLongEntryProb, "open.minLongEntryProb");
minShortEntryProb = probability(minShortEntryProb, "open.minShortEntryProb");
maxMarketRiskProb = probability(maxMarketRiskProb, "open.maxMarketRiskProb");
minExpectedEdgeBps = required(minExpectedEdgeBps, "open.minExpectedEdgeBps");
minDirectionMargin = nonNegative(minDirectionMargin, "open.minDirectionMargin");
minLiquidityCapacityRatio = nonNegative(minLiquidityCapacityRatio, "open.minLiquidityCapacityRatio");
maxOodScore = probability(maxOodScore, "open.maxOodScore");
}
}
public record AddRuleConfig(
BigDecimal minLongProb,
BigDecimal minShortProb,
BigDecimal minContinueProb,
BigDecimal minEntryProb,
BigDecimal maxExitProb,
BigDecimal maxMarketRiskProb,
BigDecimal maxPositionRiskProb,
BigDecimal minExpectedEdgeBps,
BigDecimal minContinueVsExitEdgeBps,
BigDecimal minLiquidityCapacityRatio,
BigDecimal minPostTradeLiquidationBufferBps,
int maxAddCount,
long cooldownMinutes
) {
public AddRuleConfig {
minLongProb = probability(minLongProb, "add.minLongProb");
minShortProb = probability(minShortProb, "add.minShortProb");
minContinueProb = probability(minContinueProb, "add.minContinueProb");
minEntryProb = probability(minEntryProb, "add.minEntryProb");
maxExitProb = probability(maxExitProb, "add.maxExitProb");
maxMarketRiskProb = probability(maxMarketRiskProb, "add.maxMarketRiskProb");
maxPositionRiskProb = probability(maxPositionRiskProb, "add.maxPositionRiskProb");
minExpectedEdgeBps = required(minExpectedEdgeBps, "add.minExpectedEdgeBps");
minContinueVsExitEdgeBps = required(minContinueVsExitEdgeBps, "add.minContinueVsExitEdgeBps");
minLiquidityCapacityRatio = nonNegative(minLiquidityCapacityRatio, "add.minLiquidityCapacityRatio");
minPostTradeLiquidationBufferBps = nonNegative(minPostTradeLiquidationBufferBps, "add.minPostTradeLiquidationBufferBps");
if (maxAddCount < 0 || cooldownMinutes < 0) {
throw new IllegalArgumentException("add count and cooldown must be >= 0");
}
}
}
public record ExitRuleConfig(
BigDecimal closeExitProb,
BigDecimal closePositionRiskProb,
BigDecimal closeMarketRiskProb,
BigDecimal closeContinueMax,
BigDecimal reduceGivebackProb,
BigDecimal reduceContinueMin,
BigDecimal reduceContinueMax,
BigDecimal minProfitForReduceBps,
BigDecimal maxExpectedShortfallBps
) {
public ExitRuleConfig {
closeExitProb = probability(closeExitProb, "exit.closeExitProb");
closePositionRiskProb = probability(closePositionRiskProb, "exit.closePositionRiskProb");
closeMarketRiskProb = probability(closeMarketRiskProb, "exit.closeMarketRiskProb");
closeContinueMax = probability(closeContinueMax, "exit.closeContinueMax");
reduceGivebackProb = probability(reduceGivebackProb, "exit.reduceGivebackProb");
reduceContinueMin = probability(reduceContinueMin, "exit.reduceContinueMin");
reduceContinueMax = probability(reduceContinueMax, "exit.reduceContinueMax");
minProfitForReduceBps = nonNegative(minProfitForReduceBps, "exit.minProfitForReduceBps");
maxExpectedShortfallBps = nonNegative(maxExpectedShortfallBps, "exit.maxExpectedShortfallBps");
}
}
public record SizingConfig(
BigDecimal baseRatio,
BigDecimal minInitialRatio,
BigDecimal maxSingleLegRatio,
BigDecimal minAddRatio,
BigDecimal maxAddRatio,
BigDecimal maxTotalPositionRatio,
BigDecimal minEdgeBps,
BigDecimal maxLossPerTradeBps,
BigDecimal maxLiquidityUsageRatio,
BigDecimal uncertaintyPenaltyMultiplier,
BigDecimal minPostTradeLiquidationBufferBps
) {
public SizingConfig {
baseRatio = positive(baseRatio, "sizing.baseRatio");
minInitialRatio = nonNegative(minInitialRatio, "sizing.minInitialRatio");
maxSingleLegRatio = positive(maxSingleLegRatio, "sizing.maxSingleLegRatio");
minAddRatio = nonNegative(minAddRatio, "sizing.minAddRatio");
maxAddRatio = nonNegative(maxAddRatio, "sizing.maxAddRatio");
maxTotalPositionRatio = positive(maxTotalPositionRatio, "sizing.maxTotalPositionRatio");
minEdgeBps = required(minEdgeBps, "sizing.minEdgeBps");
maxLossPerTradeBps = positive(maxLossPerTradeBps, "sizing.maxLossPerTradeBps");
maxLiquidityUsageRatio = positive(maxLiquidityUsageRatio, "sizing.maxLiquidityUsageRatio");
uncertaintyPenaltyMultiplier = nonNegative(uncertaintyPenaltyMultiplier, "sizing.uncertaintyPenaltyMultiplier");
minPostTradeLiquidationBufferBps = nonNegative(minPostTradeLiquidationBufferBps, "sizing.minPostTradeLiquidationBufferBps");
}
}
}
@@ -1,21 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderActionType;
import java.math.BigDecimal;
import java.time.Instant;
public record TraderPositionLeg(
String runId,
String cycleId,
String positionId,
String legId,
TraderActionType actionType,
BigDecimal quantity,
BigDecimal price,
BigDecimal legRatio,
BigDecimal riskDeltaBps,
Instant actionTime,
String reason
) {
}
@@ -0,0 +1,48 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import com.quantai.trader.enums.PositionSide;
import com.quantai.trader.enums.TraderActionType;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Objects;
public record TraderPositionManagerDecision(
String pmDecisionId,
String runId,
String cycleId,
String modelOutputId,
String positionStateId,
String accountStateId,
String executionStateId,
TraderActionType candidateAction,
PositionSide side,
String pricePlanId,
String pricePlanConfigHash,
BigDecimal targetPositionRatio,
BigDecimal addRatio,
BigDecimal reduceRatio,
BigDecimal stopPrice,
BigDecimal targetPrice,
String reason,
Map<String, Object> decisionJson
) {
public TraderPositionManagerDecision {
pmDecisionId = requiredText(pmDecisionId, "pmDecisionId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
modelOutputId = requiredText(modelOutputId, "modelOutputId");
positionStateId = requiredText(positionStateId, "positionStateId");
accountStateId = requiredText(accountStateId, "accountStateId");
executionStateId = requiredText(executionStateId, "executionStateId");
candidateAction = Objects.requireNonNull(candidateAction, "candidateAction is required");
side = Objects.requireNonNull(side, "side is required");
reason = requiredText(reason, "reason");
decisionJson = Map.copyOf(decisionJson == null ? Map.of() : decisionJson);
if (candidateAction.increasesExposure()) {
pricePlanId = requiredText(pricePlanId, "pricePlanId");
pricePlanConfigHash = requiredText(pricePlanConfigHash, "pricePlanConfigHash");
}
}
}
@@ -1,43 +0,0 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderSide;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
public record TraderPositionPath(
String runId,
String cycleId,
String actionId,
String positionId,
TraderSide side,
Instant entryTime,
Instant lastEventTime,
BigDecimal entryPrice,
BigDecimal currentPrice,
BigDecimal mfeBps,
BigDecimal maeBps,
Long timeToTargetMs,
Long timeToInvalidMs,
boolean targetBeforeStop,
boolean stagnationTimeoutHit,
boolean proxyOnly,
boolean reduceSeen,
int filledLegCount,
BigDecimal totalPositionRatio,
Map<String, Object> pathSummary
) {
public TraderPositionPath {
pathSummary = Maps.immutable(pathSummary);
}
public boolean opened() {
return positionId != null && filledLegCount > 0;
}
public boolean fullSize() {
return totalPositionRatio != null && totalPositionRatio.compareTo(BigDecimal.ONE) >= 0;
}
}
@@ -0,0 +1,47 @@
package com.quantai.trader.domain;
import static com.quantai.trader.util.TraderNumbers.*;
import com.quantai.trader.enums.PositionSide;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Objects;
public record TraderPositionState(
String positionStateId,
String runId,
String cycleId,
String symbol,
PositionSide side,
BigDecimal positionRatio,
BigDecimal averageEntryPrice,
BigDecimal currentPrice,
BigDecimal unrealizedPnlBps,
BigDecimal liquidationBufferBps,
int addCount,
BigDecimal remainingAddCapacity,
Instant lastAddTime
) {
public TraderPositionState {
positionStateId = requiredText(positionStateId, "positionStateId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
symbol = requiredText(symbol, "symbol");
side = Objects.requireNonNull(side, "side is required");
positionRatio = nonNegative(positionRatio, "positionRatio");
currentPrice = positive(currentPrice, "currentPrice");
if (side == PositionSide.NONE && positionRatio.compareTo(ZERO) != 0) {
throw new IllegalArgumentException("flat position must have zero ratio");
}
if (side != PositionSide.NONE) {
averageEntryPrice = positive(averageEntryPrice, "averageEntryPrice");
}
unrealizedPnlBps = required(unrealizedPnlBps, "unrealizedPnlBps");
liquidationBufferBps = nonNegative(liquidationBufferBps, "liquidationBufferBps");
remainingAddCapacity = nonNegative(remainingAddCapacity, "remainingAddCapacity");
}
public boolean isFlat() {
return side == PositionSide.NONE;
}
}
@@ -1,23 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
public record TraderPricePlan(
BigDecimal entryPrice,
BigDecimal invalidPrice,
BigDecimal stopPrice,
BigDecimal targetPrice,
BigDecimal partialTakeProfitPrice,
long maxEntryWaitMs,
long maxHoldMs
) {
public boolean completeForEntry() {
return entryPrice != null
&& invalidPrice != null
&& stopPrice != null
&& targetPrice != null
&& maxEntryWaitMs > 0
&& maxHoldMs > 0;
}
}
@@ -1,30 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public record TraderReplayReport(
String runId,
String reportId,
String symbol,
String playbookId,
String playbookVersion,
int candidateEvents,
int monthsCovered,
BigDecimal baseNetReturnBps1x,
BigDecimal leveragedNetReturnBps10x,
BigDecimal holdoutReturnBps10x,
Map<String, Object> strictVsLoose,
List<String> failureRisks,
String conclusion,
String reportPath,
Instant createdAt
) {
public TraderReplayReport {
strictVsLoose = Maps.immutable(strictVsLoose);
failureRisks = failureRisks == null ? List.of() : List.copyOf(failureRisks);
}
}
@@ -1,32 +1,32 @@
package com.quantai.trader.domain;
import com.quantai.trader.enums.TraderActionType;
import static com.quantai.trader.util.TraderNumbers.requiredText;
import java.math.BigDecimal;
import java.time.Instant;
import com.quantai.trader.enums.TraderActionType;
import java.util.Map;
import java.util.Objects;
public record TraderRiskDecision(
String riskDecisionId,
String runId,
String cycleId,
String actionId,
String accountStateId,
TraderActionType actionType,
BigDecimal leverageScreen,
BigDecimal plannedTotalPositionRatio,
BigDecimal maxLossBps,
BigDecimal liquidationBufferBps,
BigDecimal expectedValueBps1x,
BigDecimal expectedValueBps10x,
BigDecimal uncertainty,
BigDecimal oodScore,
String pmDecisionId,
boolean allowAction,
TraderActionType originalAction,
TraderActionType finalAction,
String blocker,
Map<String, Object> decision,
Instant createdAt
Map<String, Object> decisionJson
) {
public TraderRiskDecision {
decision = Maps.immutable(decision);
riskDecisionId = requiredText(riskDecisionId, "riskDecisionId");
runId = requiredText(runId, "runId");
cycleId = requiredText(cycleId, "cycleId");
pmDecisionId = requiredText(pmDecisionId, "pmDecisionId");
originalAction = Objects.requireNonNull(originalAction, "originalAction is required");
finalAction = Objects.requireNonNull(finalAction, "finalAction is required");
if (!allowAction) {
blocker = requiredText(blocker, "blocker");
}
decisionJson = Map.copyOf(decisionJson == null ? Map.of() : decisionJson);
}
}
@@ -1,27 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
public record TraderTrainingSample(
String runId,
String cycleId,
String sampleId,
String actionId,
String positionId,
String featureVersion,
String labelVersion,
Instant sampleTime,
Map<String, Object> features,
Map<String, Object> labels,
BigDecimal netReturnBps1x,
BigDecimal netReturnBps10x,
boolean proxyOnly
) {
public TraderTrainingSample {
features = Maps.immutable(features);
labels = Maps.immutable(labels);
}
}
@@ -1,21 +0,0 @@
package com.quantai.trader.domain;
import java.math.BigDecimal;
import java.util.Map;
public record TriggerDecision(
boolean pass,
BigDecimal signalStrengthScore,
String reason,
String blocker,
Map<String, Object> details
) {
public TriggerDecision {
details = Maps.immutable(details);
}
public boolean blocked() {
return !pass;
}
}
@@ -1,22 +0,0 @@
package com.quantai.trader.domain;
import java.time.Instant;
import java.util.Map;
public record TriggerEvent(
String runId,
String cycleId,
String candidateId,
String triggerId,
Instant triggerTime,
String triggerTimeframe,
String featureVersion,
Map<String, Object> triggerEvidence,
Map<String, Object> markout
) {
public TriggerEvent {
triggerEvidence = Maps.immutable(triggerEvidence);
markout = Maps.immutable(markout);
}
}
@@ -0,0 +1,16 @@
package com.quantai.trader.enums;
public enum FeedbackSource {
REPLAY_SIMULATOR,
SHADOW_APP,
PAPER_APP,
REAL_APP;
public boolean p0Allowed() {
return this == REPLAY_SIMULATOR || this == SHADOW_APP;
}
public boolean canBeRealFill() {
return this == PAPER_APP || this == REAL_APP;
}
}
@@ -0,0 +1,15 @@
package com.quantai.trader.enums;
public enum PositionSide {
NONE,
LONG,
SHORT;
public boolean isLong() {
return this == LONG;
}
public boolean isShort() {
return this == SHORT;
}
}
@@ -1,10 +0,0 @@
package com.quantai.trader.enums;
public enum ReplayRunStatus {
CREATED,
RUNNING,
CANCEL_REQUESTED,
CANCELLED,
COMPLETED,
FAILED
}
@@ -2,12 +2,23 @@ package com.quantai.trader.enums;
public enum TraderActionType {
WAIT,
OPEN_INITIAL,
OPEN_PLANNED_LEG,
OPEN_LONG,
OPEN_SHORT,
ADD_LONG,
ADD_SHORT,
HOLD,
REDUCE,
REDUCE_LONG,
REDUCE_SHORT,
MOVE_STOP,
CLOSE,
CANCEL,
REQUOTE
CLOSE_LONG,
CLOSE_SHORT,
CANCEL;
public boolean increasesExposure() {
return this == OPEN_LONG || this == OPEN_SHORT || this == ADD_LONG || this == ADD_SHORT;
}
public boolean reducesExposure() {
return this == REDUCE_LONG || this == REDUCE_SHORT || this == CLOSE_LONG || this == CLOSE_SHORT;
}
}
@@ -1,13 +1,15 @@
package com.quantai.trader.enums;
public enum TraderErrorCode {
TRADER_PLAYBOOK_VERSION_CONFLICT,
TRADER_DATA_SOURCE_MISSING,
TRADER_DATA_QUALITY_FAILED,
TRADER_ENTRY_PLAN_INCOMPLETE,
TRADER_ILLEGAL_ACTION_TRANSITION,
TRADER_PLANNED_LEG_AFTER_REDUCE,
TRADER_DATA_NOT_READY,
TRADER_MODEL_ARTIFACT_MISSING,
TRADER_CALIBRATION_MISMATCH,
TRADER_PM_CONFIG_MISMATCH,
TRADER_MODEL_OUTPUT_INVALID,
TRADER_RISK_BLOCKED,
TRADER_FEEDBACK_DISABLED,
TRADER_SAMPLE_EXPORT_FAILED
TRADER_EXECUTION_BLOCKED,
TRADER_FEEDBACK_INVALID,
TRADER_P0_MODE_BLOCKED,
TRADER_KILL_SWITCH_ACTIVE,
TRADER_ACTIVE_POINTER_MISMATCH
}
@@ -0,0 +1,12 @@
package com.quantai.trader.enums;
public enum TraderExecutionMode {
REPLAY_SIM,
SHADOW,
PAPER,
REAL;
public boolean p0Allowed() {
return this == REPLAY_SIM || this == SHADOW;
}
}
@@ -1,8 +0,0 @@
package com.quantai.trader.enums;
public enum TraderFeedbackSource {
MARKET_PROXY,
SHADOW_APP,
PAPER_APP,
REAL_APP
}
@@ -1,8 +0,0 @@
package com.quantai.trader.enums;
public enum TraderFeedbackType {
FILL_EVENT,
CANCEL_EVENT,
CLOSE_EVENT,
REJECT_EVENT
}
@@ -1,7 +0,0 @@
package com.quantai.trader.enums;
public enum TraderPlaybookId {
BREAKOUT_RETEST_CONTINUATION,
SUPPORT_PULLBACK_CONTINUATION,
FALSE_BREAK_RECLAIM
}
@@ -1,7 +1,12 @@
package com.quantai.trader.enums;
public enum TraderRunMode {
REPLAY,
REPLAY_SIM,
SHADOW,
PAPER
PAPER,
REAL;
public boolean p0Allowed() {
return this == REPLAY_SIM || this == SHADOW;
}
}
@@ -1,6 +0,0 @@
package com.quantai.trader.enums;
public enum TraderSide {
LONG,
SHORT
}
@@ -1,18 +0,0 @@
package com.quantai.trader.enums;
public enum TraderState {
CONTEXT_CHECK,
SETUP_ARMED,
TRIGGER_WAIT,
ENTRY_PLANNED,
ENTRY_SENT_SHADOW,
ENTRY_FILLED_PROXY,
ENTRY_MISSED_PROXY,
PLANNED_LEG_WAIT,
PLANNED_LEG_SENT_SHADOW,
PLANNED_LEG_FILLED_PROXY,
PLANNED_LEG_MISSED_PROXY,
MANAGING,
SAMPLE_EXPORTED,
BLOCKED
}
@@ -1,50 +1,31 @@
package com.quantai.trader.evidence;
import com.quantai.trader.domain.StageDecision;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderEvidence;
import com.quantai.trader.persistence.TraderEvidenceRepository;
import com.quantai.trader.util.Ids;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
@Component
public class EvidenceAppender {
private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class);
private final TraderEvidenceRepository repository;
private final CopyOnWriteArrayList<TraderEvidence> evidence = new CopyOnWriteArrayList<>();
public EvidenceAppender(TraderEvidenceRepository repository) {
this.repository = repository;
public TraderEvidence append(String runId, String cycleId, String stage, boolean pass, String reason, String blocker, Map<String, Object> details) {
TraderEvidence item = new TraderEvidence("evidence_" + cycleId + "_" + evidence.size(), runId, cycleId,
stage, pass, reason, blocker, Instant.now(), details);
evidence.add(item);
log.info("event=trader.evidence.appended runId={} cycleId={} stage={} pass={} reason={} blocker={}",
runId, cycleId, stage, pass, reason, blocker);
return item;
}
public TraderEvidence append(TraderDecisionCycle cycle, String stage, StageDecision decision) {
TraderEvidence evidence = new TraderEvidence(
cycle.runId(),
cycle.cycleId(),
Ids.evidenceId(cycle, stage),
stage,
decision.pass(),
decision.reason(),
decision.blocker(),
cycle.cycleTime(),
decision.details()
);
repository.insert(evidence);
log.info(
"event=trader.evidence runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} stage={} pass={} reason={} blocker={}",
cycle.runId(),
cycle.cycleId(),
cycle.symbol(),
cycle.playbookId(),
cycle.playbookVersion(),
cycle.state(),
stage,
decision.pass(),
decision.reason(),
decision.blocker()
);
return evidence;
public List<TraderEvidence> all() {
return new ArrayList<>(evidence);
}
}
@@ -1,31 +0,0 @@
package com.quantai.trader.execution;
import com.quantai.trader.domain.ExecutionDecision;
import com.quantai.trader.domain.TraderEntryPlan;
import com.quantai.trader.domain.TraderMarketSnapshot;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Map;
@Component
public class ExecutionQualityGate {
public ExecutionDecision evaluate(TraderMarketSnapshot snapshot, TraderEntryPlan entryPlan) {
BigDecimal score = entryPlan.executionQualityScore() == null
? new BigDecimal("0.50")
: entryPlan.executionQualityScore();
if (!entryPlan.completeForEntry()) {
return new ExecutionDecision(false, score, "ENTRY_PLAN_INCOMPLETE", "TRADER_ENTRY_PLAN_INCOMPLETE", Map.of());
}
if (score.compareTo(new BigDecimal("0.20")) < 0) {
return new ExecutionDecision(false, score, "EXECUTION_QUALITY_TOO_LOW", "TRADER_RISK_BLOCKED", Map.of(
"executionQualityScore", score
));
}
return new ExecutionDecision(true, score, "EXECUTION_PROXY_PASS", null, Map.of(
"executionQualityScore", score,
"proxyOnly", true
));
}
}
@@ -1,113 +0,0 @@
package com.quantai.trader.execution;
import com.quantai.trader.domain.PlaybookCandidate;
import com.quantai.trader.domain.PositionSizingPlan;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderEntryPlan;
import com.quantai.trader.domain.TraderException;
import com.quantai.trader.domain.TraderPositionPath;
import com.quantai.trader.domain.TraderPricePlan;
import com.quantai.trader.domain.TriggerDecision;
import com.quantai.trader.enums.TraderActionType;
import com.quantai.trader.enums.TraderErrorCode;
import com.quantai.trader.risk.TraderPositionSizer;
import com.quantai.trader.util.Ids;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Optional;
@Component
public class TraderEntryPlanner {
private final TraderPositionSizer positionSizer;
public TraderEntryPlanner(TraderPositionSizer positionSizer) {
this.positionSizer = positionSizer;
}
public TraderEntryPlan planInitialEntry(
TraderDecisionCycle cycle,
PlaybookCandidate candidate,
TriggerDecision trigger
) {
TraderPricePlan pricePlan = candidate.pricePlan();
validatePricePlan(pricePlan);
PositionSizingPlan sizingPlan = positionSizer.sizeInitialPlan(cycle, candidate, trigger, pricePlan);
return new TraderEntryPlan(
cycle.runId(),
cycle.cycleId(),
Ids.actionId(cycle, 1),
Ids.entryLegId(cycle, 0),
candidate.candidateId(),
TraderActionType.OPEN_INITIAL,
0,
sizingPlan.plannedLegCount(),
sizingPlan.initialLegRatio(),
sizingPlan.sizingMethod(),
sizingPlan.signalStrengthScore(),
sizingPlan.executionQualityScore(),
sizingPlan.riskGateScore(),
pricePlan.entryPrice(),
pricePlan.invalidPrice(),
pricePlan.stopPrice(),
pricePlan.targetPrice(),
pricePlan.partialTakeProfitPrice(),
pricePlan.maxEntryWaitMs(),
pricePlan.maxHoldMs(),
"INITIAL_ENTRY_FROM_PLAYBOOK_TRIGGER"
);
}
public Optional<TraderEntryPlan> planNextDeclaredLeg(
TraderDecisionCycle cycle,
PlaybookCandidate candidate,
TraderPositionPath path
) {
if (path == null || !path.opened() || path.reduceSeen() || path.fullSize()) {
return Optional.empty();
}
int nextIndex = path.filledLegCount();
if (nextIndex >= candidate.maxPlannedEntryLegs()) {
return Optional.empty();
}
TraderPricePlan pricePlan = candidate.pricePlan();
validatePricePlan(pricePlan);
PositionSizingPlan sizingPlan = positionSizer.sizeNextPlannedLeg(cycle, candidate, path, pricePlan, nextIndex);
if (sizingPlan.nextLegRatio().compareTo(BigDecimal.ZERO) <= 0) {
return Optional.empty();
}
return Optional.of(new TraderEntryPlan(
cycle.runId(),
cycle.cycleId(),
Ids.actionId(cycle, nextIndex + 1),
Ids.entryLegId(cycle, nextIndex),
candidate.candidateId(),
TraderActionType.OPEN_PLANNED_LEG,
nextIndex,
sizingPlan.plannedLegCount(),
sizingPlan.nextLegRatio(),
sizingPlan.sizingMethod(),
sizingPlan.signalStrengthScore(),
sizingPlan.executionQualityScore(),
sizingPlan.riskGateScore(),
pricePlan.entryPrice(),
pricePlan.invalidPrice(),
pricePlan.stopPrice(),
pricePlan.targetPrice(),
pricePlan.partialTakeProfitPrice(),
pricePlan.maxEntryWaitMs(),
pricePlan.maxHoldMs(),
"DECLARED_PLANNED_ENTRY_LEG"
));
}
private void validatePricePlan(TraderPricePlan pricePlan) {
if (pricePlan == null || !pricePlan.completeForEntry()) {
throw new TraderException(
TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE,
"entry/invalid/stop/target/maxHold are required before an entry action"
);
}
}
}
@@ -1,25 +0,0 @@
package com.quantai.trader.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("trader_evidence")
public class TraderEvidenceEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String runId;
private String cycleId;
private String evidenceId;
private String stage;
private Boolean pass;
private String reason;
private String blocker;
private LocalDateTime evidenceTime;
private String detailsJson;
private LocalDateTime createdAt;
}
@@ -1,27 +0,0 @@
package com.quantai.trader.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("trader_playbook_definition")
public class TraderPlaybookDefinitionEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String playbookId;
private String playbookVersion;
private String family;
private String variant;
private String sideMode;
private String sourcePath;
private String definitionHashSha256;
private String definitionJson;
private LocalDateTime loadedAt;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,35 +0,0 @@
package com.quantai.trader.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("trader_replay_report")
public class TraderReplayReportEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String runId;
private String reportId;
private String symbol;
private String playbookId;
private String playbookVersion;
private Integer candidateEvents;
private Integer monthsCovered;
@TableField("base_net_return_bps_1x")
private BigDecimal baseNetReturnBps1x;
@TableField("leveraged_net_return_bps_10x")
private BigDecimal leveragedNetReturnBps10x;
@TableField("holdout_return_bps_10x")
private BigDecimal holdoutReturnBps10x;
private String strictVsLooseJson;
private String failureRisksJson;
private String conclusion;
private String reportPath;
private LocalDateTime createdAt;
}
@@ -1,33 +0,0 @@
package com.quantai.trader.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("trader_replay_run")
public class TraderReplayRunEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String runId;
private String runMode;
private String symbol;
private String playbookId;
private String playbookVersion;
private String playbookDefinitionHash;
private LocalDateTime dataFrom;
private LocalDateTime dataTo;
private String featureVersion;
private String labelVersion;
private String dataSourceManifestJson;
private String status;
private String configJson;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
private String failureReason;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,36 +0,0 @@
package com.quantai.trader.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("trader_risk_decision")
public class TraderRiskDecisionEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String runId;
private String cycleId;
private String actionId;
private String accountStateId;
private String actionType;
private BigDecimal leverageScreen;
private BigDecimal plannedTotalPositionRatio;
private BigDecimal maxLossBps;
private BigDecimal liquidationBufferBps;
@TableField("expected_value_bps_1x")
private BigDecimal expectedValueBps1x;
@TableField("expected_value_bps_10x")
private BigDecimal expectedValueBps10x;
private BigDecimal uncertainty;
private BigDecimal oodScore;
private Boolean allowAction;
private String blocker;
private String decisionJson;
private LocalDateTime createdAt;
}
@@ -1,33 +0,0 @@
package com.quantai.trader.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("trader_training_sample")
public class TraderTrainingSampleEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String runId;
private String cycleId;
private String sampleId;
private String actionId;
private String positionId;
private String featureVersion;
private String labelVersion;
private LocalDateTime sampleTime;
private String featuresJson;
private String labelsJson;
@TableField("net_return_bps_1x")
private BigDecimal netReturnBps1x;
@TableField("net_return_bps_10x")
private BigDecimal netReturnBps10x;
private Boolean proxyOnly;
private LocalDateTime createdAt;
}
@@ -1,7 +0,0 @@
package com.quantai.trader.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.quantai.trader.infrastructure.entity.TraderEvidenceEntity;
public interface TraderEvidenceMapper extends BaseMapper<TraderEvidenceEntity> {
}
@@ -1,7 +0,0 @@
package com.quantai.trader.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.quantai.trader.infrastructure.entity.TraderPlaybookDefinitionEntity;
public interface TraderPlaybookDefinitionMapper extends BaseMapper<TraderPlaybookDefinitionEntity> {
}
@@ -1,7 +0,0 @@
package com.quantai.trader.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.quantai.trader.infrastructure.entity.TraderReplayReportEntity;
public interface TraderReplayReportMapper extends BaseMapper<TraderReplayReportEntity> {
}
@@ -1,7 +0,0 @@
package com.quantai.trader.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.quantai.trader.infrastructure.entity.TraderReplayRunEntity;
public interface TraderReplayRunMapper extends BaseMapper<TraderReplayRunEntity> {
}
@@ -1,7 +0,0 @@
package com.quantai.trader.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.quantai.trader.infrastructure.entity.TraderRiskDecisionEntity;
public interface TraderRiskDecisionMapper extends BaseMapper<TraderRiskDecisionEntity> {
}
@@ -1,7 +0,0 @@
package com.quantai.trader.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.quantai.trader.infrastructure.entity.TraderTrainingSampleEntity;
public interface TraderTrainingSampleMapper extends BaseMapper<TraderTrainingSampleEntity> {
}
@@ -1,46 +0,0 @@
package com.quantai.trader.market;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.replay.ReplayClockTick;
import com.quantai.trader.state.TraderRuntimeState;
import com.quantai.trader.util.Ids;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class SnapshotBuilder {
private final TraderProperties properties;
public SnapshotBuilder(TraderProperties properties) {
this.properties = properties;
}
public TraderMarketSnapshot build(ReplayClockTick tick, TraderRuntimeState runtimeState) {
String cycleId = Ids.cycleId(runtimeState.runId(), tick.symbol(), tick.eventTime());
return new TraderMarketSnapshot(
runtimeState.runId(),
cycleId,
Ids.snapshotId(cycleId),
tick.symbol(),
tick.eventTime(),
properties.getFeatureVersion(),
Objects.requireNonNull(tick.contextFeatures(), "contextFeatures is required"),
Objects.requireNonNull(tick.setupFeatures(), "setupFeatures is required"),
Objects.requireNonNull(tick.triggerFeatures(), "triggerFeatures is required"),
Objects.requireNonNull(tick.executionFeatures(), "executionFeatures is required"),
Objects.requireNonNull(tick.dataQuality(), "dataQuality is required"),
Objects.requireNonNull(tick.labelInputs(), "labelInputs is required")
);
}
public TraderMarketSnapshot buildNextManagementSnapshot(
TraderMarketSnapshot previous,
ReplayClockTick tick,
TraderRuntimeState runtimeState
) {
return build(tick, runtimeState);
}
}
@@ -0,0 +1,50 @@
package com.quantai.trader.model;
import com.quantai.trader.artifact.TraderArtifactBundle;
import com.quantai.trader.domain.*;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Map;
@Service
public class DeterministicTraderModelService implements TraderModelService {
@Override
public TraderModelOutput evaluate(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle) {
BigDecimal directionalBias = snapshot.markPrice().compareTo(snapshot.indexPrice()) >= 0
? new BigDecimal("0.62")
: new BigDecimal("0.32");
BigDecimal shortBias = BigDecimal.ONE.subtract(directionalBias).subtract(new BigDecimal("0.10"));
if (shortBias.compareTo(BigDecimal.ZERO) < 0) {
shortBias = BigDecimal.ZERO;
}
BigDecimal neutral = BigDecimal.ONE.subtract(directionalBias).subtract(shortBias);
EntryOutput entry = new EntryOutput(
new BigDecimal("0.63"), new BigDecimal("0.42"), new BigDecimal("0.64"),
new BigDecimal("12.0"), "p0-plan-atr-2r", "p0-price-plan-hash",
new BigDecimal("35"), new BigDecimal("70"), 45, new BigDecimal("4.0"),
"entry-p0", bundle.calibrationBundleVersion(), Map.of("source", "deterministic_fixture"));
return new TraderModelOutput(
"model_output_" + snapshot.cycleId(),
snapshot.runId(),
snapshot.cycleId(),
bundle.modelBundleVersion(),
bundle.calibrationBundleVersion(),
new DirectionOutput(directionalBias, shortBias, neutral, directionalBias, directionalBias.subtract(shortBias).abs(),
new BigDecimal("8.0"), 45, "direction-p0", bundle.calibrationBundleVersion(), Map.of()),
entry,
new ContinueOutput(new BigDecimal("0.61"), new BigDecimal("0.39"), new BigDecimal("0.58"),
new BigDecimal("5.0"), new BigDecimal("3.0"), "continue-p0", bundle.calibrationBundleVersion(), Map.of()),
new ExitOutput(new BigDecimal("0.24"), new BigDecimal("0.48"), new BigDecimal("0.20"),
new BigDecimal("0.25"), new BigDecimal("0.22"), new BigDecimal("0.20"),
new BigDecimal("10"), "exit-p0", bundle.calibrationBundleVersion(), Map.of()),
new RiskOutput(new BigDecimal("0.20"), new BigDecimal("0.18"),
new BigDecimal("20"), new BigDecimal("18"), new BigDecimal("0.15"),
new BigDecimal("20"), new BigDecimal("0.20"), new BigDecimal("0.10"),
new BigDecimal("0.12"), snapshot.depthNotional5Bps().compareTo(BigDecimal.ZERO) > 0 ? new BigDecimal("1.0") : BigDecimal.ZERO,
"risk-p0", bundle.calibrationBundleVersion(), Map.of()),
new BigDecimal("0.10"),
new BigDecimal("0.05"),
Map.of("source", "deterministic_p0_fixture"));
}
}
@@ -1,9 +0,0 @@
package com.quantai.trader.model;
import com.quantai.trader.domain.TraderModelManifest;
import com.quantai.trader.domain.TraderModelOutput;
public interface TraderModel<TInput> {
TraderModelOutput predict(TInput input, TraderModelManifest manifest);
}
@@ -0,0 +1,9 @@
package com.quantai.trader.model;
import com.quantai.trader.artifact.TraderArtifactBundle;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.domain.TraderModelOutput;
public interface TraderModelService {
TraderModelOutput evaluate(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle);
}
@@ -0,0 +1,30 @@
package com.quantai.trader.outbox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Repository
public class InMemoryOutboxRepository {
private static final Logger log = LoggerFactory.getLogger(InMemoryOutboxRepository.class);
private final CopyOnWriteArrayList<TraderOutboxEvent> events = new CopyOnWriteArrayList<>();
public void insert(TraderOutboxEvent event) {
boolean duplicate = events.stream().anyMatch(existing -> existing.destination().equals(event.destination())
&& existing.idempotencyKey().equals(event.idempotencyKey()));
if (duplicate) {
throw new IllegalArgumentException("duplicate outbox idempotency key: " + event.idempotencyKey());
}
events.add(event);
log.info("event=trader.outbox.inserted runId={} cycleId={} destination={} aggregateType={} aggregateId={} status={}",
event.runId(), event.cycleId(), event.destination(), event.aggregateType(), event.aggregateId(), event.status());
}
public List<TraderOutboxEvent> all() {
return new ArrayList<>(events);
}
}
@@ -0,0 +1,34 @@
package com.quantai.trader.outbox;
import static com.quantai.trader.util.TraderNumbers.requiredText;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public record TraderOutboxEvent(
String outboxId,
String runId,
String cycleId,
String aggregateType,
String aggregateId,
String eventType,
String destination,
Map<String, Object> payloadJson,
String idempotencyKey,
String status,
Instant createdAt
) {
public TraderOutboxEvent {
outboxId = requiredText(outboxId, "outboxId");
runId = requiredText(runId, "runId");
aggregateType = requiredText(aggregateType, "aggregateType");
aggregateId = requiredText(aggregateId, "aggregateId");
eventType = requiredText(eventType, "eventType");
destination = requiredText(destination, "destination");
idempotencyKey = requiredText(idempotencyKey, "idempotencyKey");
status = requiredText(status, "status");
createdAt = Objects.requireNonNull(createdAt, "createdAt is required");
payloadJson = Map.copyOf(payloadJson == null ? Map.of() : payloadJson);
}
}
@@ -1,75 +0,0 @@
package com.quantai.trader.persistence;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.quantai.trader.domain.TraderReplayReport;
import com.quantai.trader.infrastructure.entity.TraderReplayReportEntity;
import com.quantai.trader.infrastructure.mapper.TraderReplayReportMapper;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public class MybatisReplayReportRepository implements ReplayReportRepository {
private final TraderReplayReportMapper mapper;
public MybatisReplayReportRepository(TraderReplayReportMapper mapper) {
this.mapper = mapper;
}
@Override
public void insert(TraderReplayReport report) {
mapper.insert(toEntity(report));
}
@Override
public Optional<TraderReplayReport> findByRunId(String runId) {
TraderReplayReportEntity entity = mapper.selectOne(
Wrappers.<TraderReplayReportEntity>lambdaQuery()
.eq(TraderReplayReportEntity::getRunId, runId)
.orderByDesc(TraderReplayReportEntity::getCreatedAt)
.last("limit 1")
);
return Optional.ofNullable(entity).map(this::toDomain);
}
private TraderReplayReportEntity toEntity(TraderReplayReport report) {
TraderReplayReportEntity entity = new TraderReplayReportEntity();
entity.setRunId(report.runId());
entity.setReportId(report.reportId());
entity.setSymbol(report.symbol());
entity.setPlaybookId(report.playbookId());
entity.setPlaybookVersion(report.playbookVersion());
entity.setCandidateEvents(report.candidateEvents());
entity.setMonthsCovered(report.monthsCovered());
entity.setBaseNetReturnBps1x(report.baseNetReturnBps1x());
entity.setLeveragedNetReturnBps10x(report.leveragedNetReturnBps10x());
entity.setHoldoutReturnBps10x(report.holdoutReturnBps10x());
entity.setStrictVsLooseJson(TraderPersistenceCodec.json(report.strictVsLoose()));
entity.setFailureRisksJson(TraderPersistenceCodec.json(report.failureRisks()));
entity.setConclusion(report.conclusion());
entity.setReportPath(report.reportPath());
entity.setCreatedAt(TraderPersistenceCodec.requiredLocal("report.createdAt", report.createdAt()));
return entity;
}
private TraderReplayReport toDomain(TraderReplayReportEntity entity) {
return new TraderReplayReport(
entity.getRunId(),
entity.getReportId(),
entity.getSymbol(),
entity.getPlaybookId(),
entity.getPlaybookVersion(),
entity.getCandidateEvents(),
entity.getMonthsCovered(),
entity.getBaseNetReturnBps1x(),
entity.getLeveragedNetReturnBps10x(),
entity.getHoldoutReturnBps10x(),
TraderPersistenceCodec.map(entity.getStrictVsLooseJson()),
TraderPersistenceCodec.stringList(entity.getFailureRisksJson()),
entity.getConclusion(),
entity.getReportPath(),
TraderPersistenceCodec.instant(entity.getCreatedAt())
);
}
}
@@ -1,88 +0,0 @@
package com.quantai.trader.persistence;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.quantai.trader.enums.ReplayRunStatus;
import com.quantai.trader.infrastructure.entity.TraderReplayRunEntity;
import com.quantai.trader.infrastructure.mapper.TraderReplayRunMapper;
import com.quantai.trader.replay.ReplayRun;
import com.quantai.trader.replay.ReplayRunConfig;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public class MybatisReplayRunRepository implements ReplayRunRepository {
private final TraderReplayRunMapper mapper;
public MybatisReplayRunRepository(TraderReplayRunMapper mapper) {
this.mapper = mapper;
}
@Override
public void insert(ReplayRun run) {
mapper.insert(toEntity(run));
}
@Override
public void update(ReplayRun run) {
int updated = mapper.update(
null,
Wrappers.<TraderReplayRunEntity>lambdaUpdate()
.set(TraderReplayRunEntity::getStatus, run.status().name())
.set(TraderReplayRunEntity::getConfigJson, TraderPersistenceCodec.json(run.config()))
.set(TraderReplayRunEntity::getStartedAt, TraderPersistenceCodec.local(run.startedAt()))
.set(TraderReplayRunEntity::getFinishedAt, TraderPersistenceCodec.local(run.finishedAt()))
.set(TraderReplayRunEntity::getFailureReason, run.failureReason())
.eq(TraderReplayRunEntity::getRunId, run.runId())
);
if (updated == 0) {
throw new IllegalStateException("replay run not found for update: " + run.runId());
}
}
@Override
public Optional<ReplayRun> findByRunId(String runId) {
TraderReplayRunEntity entity = mapper.selectOne(
Wrappers.<TraderReplayRunEntity>lambdaQuery()
.eq(TraderReplayRunEntity::getRunId, runId)
);
return Optional.ofNullable(entity).map(this::toDomain);
}
private TraderReplayRunEntity toEntity(ReplayRun run) {
ReplayRunConfig config = run.config();
TraderReplayRunEntity entity = new TraderReplayRunEntity();
entity.setRunId(run.runId());
entity.setRunMode("REPLAY");
entity.setSymbol(config.symbol());
entity.setPlaybookId(config.playbookId());
entity.setPlaybookVersion(config.playbookVersion());
entity.setPlaybookDefinitionHash(run.playbookDefinitionHash());
entity.setDataFrom(TraderPersistenceCodec.local(config.from()));
entity.setDataTo(TraderPersistenceCodec.local(config.to()));
entity.setFeatureVersion(config.featureVersion());
entity.setLabelVersion(config.labelVersion());
entity.setDataSourceManifestJson(TraderPersistenceCodec.json(config.dataSources()));
entity.setStatus(run.status().name());
entity.setConfigJson(TraderPersistenceCodec.json(config));
entity.setStartedAt(TraderPersistenceCodec.local(run.startedAt()));
entity.setFinishedAt(TraderPersistenceCodec.local(run.finishedAt()));
entity.setFailureReason(run.failureReason());
entity.setCreatedAt(TraderPersistenceCodec.requiredLocal("run.createdAt", run.createdAt()));
return entity;
}
private ReplayRun toDomain(TraderReplayRunEntity entity) {
return new ReplayRun(
entity.getRunId(),
ReplayRunStatus.valueOf(entity.getStatus()),
TraderPersistenceCodec.replayRunConfig(entity.getConfigJson()),
entity.getPlaybookDefinitionHash(),
TraderPersistenceCodec.instant(entity.getCreatedAt()),
TraderPersistenceCodec.instant(entity.getStartedAt()),
TraderPersistenceCodec.instant(entity.getFinishedAt()),
entity.getFailureReason()
);
}
}
@@ -1,61 +0,0 @@
package com.quantai.trader.persistence;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.quantai.trader.domain.TraderEvidence;
import com.quantai.trader.infrastructure.entity.TraderEvidenceEntity;
import com.quantai.trader.infrastructure.mapper.TraderEvidenceMapper;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.List;
@Repository
public class MybatisTraderEvidenceRepository implements TraderEvidenceRepository {
private final TraderEvidenceMapper mapper;
public MybatisTraderEvidenceRepository(TraderEvidenceMapper mapper) {
this.mapper = mapper;
}
@Override
public void insert(TraderEvidence evidence) {
TraderEvidenceEntity entity = new TraderEvidenceEntity();
entity.setRunId(evidence.runId());
entity.setCycleId(evidence.cycleId());
entity.setEvidenceId(evidence.evidenceId());
entity.setStage(evidence.stage());
entity.setPass(evidence.pass());
entity.setReason(evidence.reason());
entity.setBlocker(evidence.blocker());
entity.setEvidenceTime(TraderPersistenceCodec.local(evidence.evidenceTime()));
entity.setDetailsJson(TraderPersistenceCodec.json(evidence.details()));
entity.setCreatedAt(TraderPersistenceCodec.local(Instant.now()));
mapper.insert(entity);
}
@Override
public List<TraderEvidence> findByCycleId(String runId, String cycleId) {
return mapper.selectList(
Wrappers.<TraderEvidenceEntity>lambdaQuery()
.eq(TraderEvidenceEntity::getRunId, runId)
.eq(TraderEvidenceEntity::getCycleId, cycleId)
.orderByAsc(TraderEvidenceEntity::getEvidenceTime)
.orderByAsc(TraderEvidenceEntity::getId)
).stream().map(this::toDomain).toList();
}
private TraderEvidence toDomain(TraderEvidenceEntity entity) {
return new TraderEvidence(
entity.getRunId(),
entity.getCycleId(),
entity.getEvidenceId(),
entity.getStage(),
Boolean.TRUE.equals(entity.getPass()),
entity.getReason(),
entity.getBlocker(),
TraderPersistenceCodec.instant(entity.getEvidenceTime()),
TraderPersistenceCodec.map(entity.getDetailsJson())
);
}
}

Some files were not shown because too many files have changed in this diff Show More