Rewrite trader service for V4 P0
This commit is contained in:
@@ -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;
|
||||
}
|
||||
-27
@@ -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;
|
||||
}
|
||||
-33
@@ -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> {
|
||||
}
|
||||
-7
@@ -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
Reference in New Issue
Block a user