Rewrite trader service for V4 P0
This commit is contained in:
@@ -15,11 +15,11 @@
|
|||||||
<artifactId>quant-trader-service</artifactId>
|
<artifactId>quant-trader-service</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>quant-trader-service</name>
|
<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>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
<jacoco.version>0.8.13</jacoco.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -35,15 +35,6 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||||
@@ -53,9 +44,8 @@
|
|||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>commons-csv</artifactId>
|
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||||
<version>1.10.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -99,6 +89,46 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</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>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.quantai.trader;
|
package com.quantai.trader;
|
||||||
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
|
||||||
@MapperScan("com.quantai.trader.infrastructure.mapper")
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@ConfigurationPropertiesScan
|
@ConfigurationPropertiesScan
|
||||||
public class QuantTraderServiceApplication {
|
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;
|
package com.quantai.trader.config;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.TraderExecutionMode;
|
||||||
import com.quantai.trader.enums.TraderRunMode;
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "trader")
|
@ConfigurationProperties(prefix = "trader")
|
||||||
public class TraderProperties {
|
public record TraderProperties(
|
||||||
|
String serviceName,
|
||||||
private String serviceName = "quant-trader-service";
|
TraderRunMode runMode,
|
||||||
private TraderRunMode runMode = TraderRunMode.REPLAY;
|
String symbol,
|
||||||
private String symbol = "BTCUSDT";
|
Artifact artifact,
|
||||||
private String featureVersion = "trader_feature_v0";
|
Feedback feedback,
|
||||||
private String labelVersion = "trader_label_v0";
|
Execution execution,
|
||||||
private Playbook playbook = new Playbook();
|
Runtime runtime,
|
||||||
private Replay replay = new Replay();
|
Outbox outbox,
|
||||||
private Integration integration = new Integration();
|
Release release,
|
||||||
private Risk risk = new Risk();
|
Risk risk,
|
||||||
private Sizing sizing = new Sizing();
|
PositionManager positionManager
|
||||||
private DataSource dataSource = new DataSource();
|
) {
|
||||||
|
public TraderProperties {
|
||||||
public String getServiceName() {
|
serviceName = defaultText(serviceName, "quant-trader-service");
|
||||||
return serviceName;
|
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;
|
||||||
public void setServiceName(String serviceName) {
|
feedback = feedback == null ? new Feedback(false) : feedback;
|
||||||
this.serviceName = serviceName;
|
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;
|
||||||
public TraderRunMode getRunMode() {
|
release = release == null ? new Release(true, true, true) : release;
|
||||||
return runMode;
|
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 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) {
|
private static String defaultText(String value, String defaultValue) {
|
||||||
this.featureVersion = featureVersion;
|
return value == null || value.isBlank() ? defaultValue : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLabelVersion() {
|
public record Artifact(
|
||||||
return labelVersion;
|
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 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) {
|
public record Feedback(boolean httpEnabled) {
|
||||||
this.replay = replay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integration getIntegration() {
|
public record Execution(TraderExecutionMode mode, int maxApiErrorCount, long maxExchangeLatencyMs) {
|
||||||
return integration;
|
public Execution {
|
||||||
|
mode = mode == null ? TraderExecutionMode.SHADOW : mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIntegration(Integration integration) {
|
|
||||||
this.integration = integration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Risk getRisk() {
|
|
||||||
return risk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRisk(Risk risk) {
|
public record Runtime(String redisKeyPrefix, boolean requireRedisForOpenAdd, boolean tradingEnabled) {
|
||||||
this.risk = risk;
|
public Runtime {
|
||||||
|
redisKeyPrefix = defaultText(redisKeyPrefix, "trader:v4");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Sizing getSizing() {
|
|
||||||
return sizing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSizing(Sizing sizing) {
|
public record Outbox(boolean enabled, int maxRetryCount) {
|
||||||
this.sizing = sizing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataSource getDataSource() {
|
public record Release(boolean requireReviewForPaper, boolean requireReviewForLiveProbe, boolean activePointerCheckEnabled) {
|
||||||
return dataSource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDataSource(DataSource dataSource) {
|
public record Risk(BigDecimal maxDailyLossBps, BigDecimal maxTotalExposureRatio, BigDecimal minLiquidationBufferBps) {
|
||||||
this.dataSource = dataSource;
|
public Risk {
|
||||||
}
|
maxDailyLossBps = maxDailyLossBps == null ? new BigDecimal("200") : maxDailyLossBps;
|
||||||
|
maxTotalExposureRatio = maxTotalExposureRatio == null ? BigDecimal.ONE : maxTotalExposureRatio;
|
||||||
public static class Playbook {
|
minLiquidationBufferBps = minLiquidationBufferBps == null ? new BigDecimal("500") : minLiquidationBufferBps;
|
||||||
private String locationPattern = "classpath:/playbooks/*.yml";
|
|
||||||
|
|
||||||
public String getLocationPattern() {
|
|
||||||
return locationPattern;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLocationPattern(String locationPattern) {
|
|
||||||
this.locationPattern = locationPattern;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Replay {
|
|
||||||
private String outputDir = "/Users/zach/Desktop/app/trader/replay-output";
|
|
||||||
private boolean failOnDataMissing = true;
|
|
||||||
|
|
||||||
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 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 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 BigDecimal getLeverageScreen() {
|
|
||||||
return leverageScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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) {
|
public record PositionManager(BigDecimal maxSingleLegRatio, BigDecimal maxTotalPositionRatio) {
|
||||||
this.hashMode = hashMode;
|
public PositionManager {
|
||||||
|
maxSingleLegRatio = maxSingleLegRatio == null ? BigDecimal.ONE : maxSingleLegRatio;
|
||||||
|
maxTotalPositionRatio = maxTotalPositionRatio == null ? BigDecimal.ONE : maxTotalPositionRatio;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
public record TraderApiError(
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
String code,
|
|
||||||
String message
|
public record TraderApiError(TraderErrorCode code, String message) {
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderException;
|
import com.quantai.trader.domain.TraderException;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class TraderApiExceptionHandler {
|
public class TraderApiExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(TraderException.class)
|
@ExceptionHandler(TraderException.class)
|
||||||
public ResponseEntity<TraderApiError> handleTraderException(TraderException ex) {
|
ResponseEntity<TraderApiError> traderException(TraderException exception) {
|
||||||
return ResponseEntity.badRequest().body(new TraderApiError(ex.errorCode().name(), ex.getMessage()));
|
return ResponseEntity.badRequest().body(new TraderApiError(exception.code(), exception.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
public ResponseEntity<TraderApiError> handleIllegalArgument(IllegalArgumentException ex) {
|
ResponseEntity<TraderApiError> illegalArgument(IllegalArgumentException exception) {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new TraderApiError("TRADER_NOT_FOUND", ex.getMessage()));
|
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;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.config.TraderProperties;
|
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 com.quantai.trader.enums.TraderErrorCode;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trader/feedback")
|
|
||||||
public class TraderFeedbackController {
|
public class TraderFeedbackController {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class);
|
private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class);
|
||||||
private final TraderProperties properties;
|
private final TraderProperties properties;
|
||||||
|
private final FeedbackValidator feedbackValidator;
|
||||||
|
|
||||||
public TraderFeedbackController(TraderProperties properties) {
|
public TraderFeedbackController(TraderProperties properties, FeedbackValidator feedbackValidator) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.feedbackValidator = feedbackValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping("/api/trader/feedback")
|
||||||
public ResponseEntity<?> feedback(@RequestBody TraderFeedbackRequest request) {
|
public Map<String, Object> feedback(@RequestBody TraderAppFeedback feedback) {
|
||||||
if (!properties.getIntegration().isHttpFeedbackEnabled()) {
|
if (!properties.feedback().httpEnabled()) {
|
||||||
log.info(
|
throw new TraderException(TraderErrorCode.TRADER_FEEDBACK_INVALID, "HTTP feedback is disabled in P0");
|
||||||
"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"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return ResponseEntity.accepted().body(Map.of(
|
feedbackValidator.validateP0(feedback);
|
||||||
"status", "ACCEPTED_CONTRACT_ONLY",
|
log.info("event=trader.feedback.accepted runId={} cycleId={} actionId={} source={}",
|
||||||
"runId", request.runId(),
|
feedback.runId(), feedback.cycleId(), feedback.actionId(), feedback.feedbackSource());
|
||||||
"actionId", request.actionId()
|
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;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.config.TraderProperties;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trader")
|
|
||||||
public class TraderHealthController {
|
public class TraderHealthController {
|
||||||
|
|
||||||
private final TraderProperties properties;
|
private final TraderProperties properties;
|
||||||
private final TraderPlaybookCatalog catalog;
|
|
||||||
|
|
||||||
public TraderHealthController(TraderProperties properties, TraderPlaybookCatalog catalog) {
|
public TraderHealthController(TraderProperties properties) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.catalog = catalog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/health")
|
@GetMapping("/api/trader/health")
|
||||||
public Map<String, Object> health() {
|
public Map<String, Object> health() {
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"service", properties.getServiceName(),
|
"status", "UP",
|
||||||
"runMode", properties.getRunMode(),
|
"runMode", properties.runMode(),
|
||||||
"symbol", properties.getSymbol(),
|
"executionMode", properties.execution().mode(),
|
||||||
"playbookCount", catalog.list().size(),
|
"modelBundleVersion", properties.artifact().modelBundleVersion(),
|
||||||
"httpFeedbackEnabled", properties.getIntegration().isHttpFeedbackEnabled()
|
"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;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderReplayReport;
|
import com.quantai.trader.replay.ReplayMarketEvent;
|
||||||
import com.quantai.trader.persistence.ReplayReportRepository;
|
import com.quantai.trader.replay.TraderCycleResult;
|
||||||
import com.quantai.trader.replay.ReplayRun;
|
import com.quantai.trader.replay.TraderP0CycleRunner;
|
||||||
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 org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
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;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trader/replay/runs")
|
|
||||||
public class TraderReplayController {
|
public class TraderReplayController {
|
||||||
|
private final TraderP0CycleRunner runner;
|
||||||
|
|
||||||
private final ReplayRunService replayRunService;
|
public TraderReplayController(TraderP0CycleRunner runner) {
|
||||||
private final ReplayReportRepository reportRepository;
|
this.runner = runner;
|
||||||
|
|
||||||
public TraderReplayController(ReplayRunService replayRunService, ReplayReportRepository reportRepository) {
|
|
||||||
this.replayRunService = replayRunService;
|
|
||||||
this.reportRepository = reportRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping("/api/trader/replay/cycles")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
public TraderCycleResult runOneCycle(@RequestBody ReplayMarketEvent event) {
|
||||||
public ReplayRunResponse create(@RequestBody ReplayRunConfig config) {
|
return runner.runFlatCycle(event);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
import com.quantai.trader.enums.TraderSide;
|
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderAction(
|
public record TraderAction(
|
||||||
|
String actionId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String actionId,
|
String modelOutputId,
|
||||||
|
String pmDecisionId,
|
||||||
|
String riskDecisionId,
|
||||||
TraderActionType actionType,
|
TraderActionType actionType,
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
String symbol,
|
String symbol,
|
||||||
TraderSide side,
|
PositionSide side,
|
||||||
BigDecimal price,
|
String pricePlanId,
|
||||||
|
String pricePlanConfigHash,
|
||||||
|
BigDecimal positionRatio,
|
||||||
BigDecimal quantity,
|
BigDecimal quantity,
|
||||||
Instant actionTime,
|
BigDecimal stopPrice,
|
||||||
|
BigDecimal targetPrice,
|
||||||
|
boolean reduceOnly,
|
||||||
|
String idempotencyKey,
|
||||||
String reason,
|
String reason,
|
||||||
Map<String, Object> actionContext,
|
Map<String, Object> actionContextJson
|
||||||
String sendStatus
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderAction {
|
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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderFeedbackSource;
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
import com.quantai.trader.enums.TraderFeedbackType;
|
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.FeedbackSource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderAppFeedback(
|
public record TraderAppFeedback(
|
||||||
|
String feedbackId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String actionId,
|
String actionId,
|
||||||
TraderFeedbackType feedbackType,
|
FeedbackSource feedbackSource,
|
||||||
TraderFeedbackSource feedbackSource,
|
|
||||||
String proxyMethod,
|
|
||||||
String simulatorVersion,
|
|
||||||
boolean realFill,
|
boolean realFill,
|
||||||
String orderId,
|
String orderId,
|
||||||
String positionId,
|
|
||||||
String orderStatus,
|
String orderStatus,
|
||||||
Instant appReceivedTime,
|
Instant appReceivedTime,
|
||||||
Instant exchangeAckTime,
|
Instant exchangeAckTime,
|
||||||
@@ -26,19 +24,24 @@ public record TraderAppFeedback(
|
|||||||
BigDecimal filledQuantity,
|
BigDecimal filledQuantity,
|
||||||
BigDecimal fee,
|
BigDecimal fee,
|
||||||
BigDecimal slippageBps,
|
BigDecimal slippageBps,
|
||||||
String closeReason,
|
String rejectReason,
|
||||||
String closeSignalSource,
|
Map<String, Object> rawFeedbackJson
|
||||||
String exchangeErrorCode,
|
|
||||||
String platformErrorCode,
|
|
||||||
Map<String, Object> rawFeedback
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderAppFeedback {
|
public TraderAppFeedback {
|
||||||
rawFeedback = Maps.immutable(rawFeedback);
|
feedbackId = requiredText(feedbackId, "feedbackId");
|
||||||
boolean sourceCanBeReal = feedbackSource == TraderFeedbackSource.PAPER_APP
|
runId = requiredText(runId, "runId");
|
||||||
|| feedbackSource == TraderFeedbackSource.REAL_APP;
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
if (realFill != sourceCanBeReal) {
|
actionId = requiredText(actionId, "actionId");
|
||||||
throw new IllegalArgumentException("feedback_source and realFill are inconsistent");
|
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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderRunMode;
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
import com.quantai.trader.enums.TraderState;
|
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderDecisionCycle(
|
public record TraderDecisionCycle(
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String snapshotId,
|
|
||||||
String symbol,
|
String symbol,
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
TraderState state,
|
|
||||||
Instant cycleTime,
|
Instant cycleTime,
|
||||||
TraderRunMode runMode,
|
TraderRunMode runMode,
|
||||||
String decisionStatus,
|
String modelBundleVersion,
|
||||||
String blocker
|
String calibrationBundleVersion,
|
||||||
|
String pmConfigVersion
|
||||||
) {
|
) {
|
||||||
|
public TraderDecisionCycle {
|
||||||
public TraderDecisionCycle withState(TraderState nextState, String nextStatus, String nextBlocker) {
|
runId = requiredText(runId, "runId");
|
||||||
return new TraderDecisionCycle(
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
runId,
|
symbol = requiredText(symbol, "symbol");
|
||||||
cycleId,
|
cycleTime = Objects.requireNonNull(cycleTime, "cycleTime is required");
|
||||||
snapshotId,
|
runMode = Objects.requireNonNull(runMode, "runMode is required");
|
||||||
symbol,
|
modelBundleVersion = requiredText(modelBundleVersion, "modelBundleVersion");
|
||||||
playbookId,
|
calibrationBundleVersion = requiredText(calibrationBundleVersion, "calibrationBundleVersion");
|
||||||
playbookVersion,
|
pmConfigVersion = requiredText(pmConfigVersion, "pmConfigVersion");
|
||||||
nextState,
|
|
||||||
cycleTime,
|
|
||||||
runMode,
|
|
||||||
nextStatus,
|
|
||||||
nextBlocker
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderEvidence(
|
public record TraderEvidence(
|
||||||
|
String evidenceId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String evidenceId,
|
|
||||||
String stage,
|
String stage,
|
||||||
boolean pass,
|
boolean pass,
|
||||||
String reason,
|
String reason,
|
||||||
String blocker,
|
String blocker,
|
||||||
Instant evidenceTime,
|
Instant evidenceTime,
|
||||||
Map<String, Object> details
|
Map<String, Object> detailsJson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderEvidence {
|
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;
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
|
|
||||||
public class TraderException extends RuntimeException {
|
public class TraderException extends RuntimeException {
|
||||||
|
private final TraderErrorCode code;
|
||||||
|
|
||||||
private final TraderErrorCode errorCode;
|
public TraderException(TraderErrorCode code, String message) {
|
||||||
|
|
||||||
public TraderException(TraderErrorCode errorCode, String message) {
|
|
||||||
super(message);
|
super(message);
|
||||||
this.errorCode = errorCode;
|
this.code = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TraderErrorCode errorCode() {
|
public TraderErrorCode code() {
|
||||||
return errorCode;
|
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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public record TraderMarketSnapshot(
|
public record TraderMarketSnapshot(
|
||||||
|
String snapshotId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String snapshotId,
|
|
||||||
String symbol,
|
String symbol,
|
||||||
Instant snapshotTime,
|
Instant snapshotTime,
|
||||||
String featureVersion,
|
String featureVersion,
|
||||||
Map<String, Object> contextFeatures,
|
BigDecimal markPrice,
|
||||||
Map<String, Object> setupFeatures,
|
BigDecimal indexPrice,
|
||||||
Map<String, Object> triggerFeatures,
|
BigDecimal spreadBps,
|
||||||
Map<String, Object> executionFeatures,
|
BigDecimal fundingRateBps,
|
||||||
Map<String, Object> dataQuality,
|
BigDecimal depthNotional5Bps,
|
||||||
Map<String, Object> labelInputs
|
BigDecimal depthNotional10Bps,
|
||||||
|
BigDecimal depthNotional25Bps,
|
||||||
|
boolean dataReady,
|
||||||
|
Map<String, Object> featureJson,
|
||||||
|
Map<String, Object> dataQualityJson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderMarketSnapshot {
|
public TraderMarketSnapshot {
|
||||||
contextFeatures = Maps.immutable(contextFeatures);
|
snapshotId = requiredText(snapshotId, "snapshotId");
|
||||||
setupFeatures = Maps.immutable(setupFeatures);
|
runId = requiredText(runId, "runId");
|
||||||
triggerFeatures = Maps.immutable(triggerFeatures);
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
executionFeatures = Maps.immutable(executionFeatures);
|
symbol = requiredText(symbol, "symbol");
|
||||||
dataQuality = Maps.immutable(dataQuality);
|
snapshotTime = java.util.Objects.requireNonNull(snapshotTime, "snapshotTime is required");
|
||||||
labelInputs = Maps.immutable(labelInputs);
|
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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderModelOutput(
|
public record TraderModelOutput(
|
||||||
String modelName,
|
String modelOutputId,
|
||||||
String modelVersion,
|
String runId,
|
||||||
BigDecimal score,
|
String cycleId,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
DirectionOutput direction,
|
||||||
|
EntryOutput entry,
|
||||||
|
ContinueOutput continuation,
|
||||||
|
ExitOutput exit,
|
||||||
|
RiskOutput risk,
|
||||||
BigDecimal uncertainty,
|
BigDecimal uncertainty,
|
||||||
BigDecimal oodScore,
|
BigDecimal oodScore,
|
||||||
Instant predictedAt,
|
Map<String, Object> explanation
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderModelOutput {
|
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;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderRiskDecision(
|
public record TraderRiskDecision(
|
||||||
|
String riskDecisionId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String actionId,
|
String pmDecisionId,
|
||||||
String accountStateId,
|
|
||||||
TraderActionType actionType,
|
|
||||||
BigDecimal leverageScreen,
|
|
||||||
BigDecimal plannedTotalPositionRatio,
|
|
||||||
BigDecimal maxLossBps,
|
|
||||||
BigDecimal liquidationBufferBps,
|
|
||||||
BigDecimal expectedValueBps1x,
|
|
||||||
BigDecimal expectedValueBps10x,
|
|
||||||
BigDecimal uncertainty,
|
|
||||||
BigDecimal oodScore,
|
|
||||||
boolean allowAction,
|
boolean allowAction,
|
||||||
|
TraderActionType originalAction,
|
||||||
|
TraderActionType finalAction,
|
||||||
String blocker,
|
String blocker,
|
||||||
Map<String, Object> decision,
|
Map<String, Object> decisionJson
|
||||||
Instant createdAt
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderRiskDecision {
|
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 {
|
public enum TraderActionType {
|
||||||
WAIT,
|
WAIT,
|
||||||
OPEN_INITIAL,
|
OPEN_LONG,
|
||||||
OPEN_PLANNED_LEG,
|
OPEN_SHORT,
|
||||||
|
ADD_LONG,
|
||||||
|
ADD_SHORT,
|
||||||
HOLD,
|
HOLD,
|
||||||
REDUCE,
|
REDUCE_LONG,
|
||||||
|
REDUCE_SHORT,
|
||||||
MOVE_STOP,
|
MOVE_STOP,
|
||||||
CLOSE,
|
CLOSE_LONG,
|
||||||
CANCEL,
|
CLOSE_SHORT,
|
||||||
REQUOTE
|
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;
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
public enum TraderErrorCode {
|
public enum TraderErrorCode {
|
||||||
TRADER_PLAYBOOK_VERSION_CONFLICT,
|
TRADER_DATA_NOT_READY,
|
||||||
TRADER_DATA_SOURCE_MISSING,
|
TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
TRADER_DATA_QUALITY_FAILED,
|
TRADER_CALIBRATION_MISMATCH,
|
||||||
TRADER_ENTRY_PLAN_INCOMPLETE,
|
TRADER_PM_CONFIG_MISMATCH,
|
||||||
TRADER_ILLEGAL_ACTION_TRANSITION,
|
TRADER_MODEL_OUTPUT_INVALID,
|
||||||
TRADER_PLANNED_LEG_AFTER_REDUCE,
|
|
||||||
TRADER_RISK_BLOCKED,
|
TRADER_RISK_BLOCKED,
|
||||||
TRADER_FEEDBACK_DISABLED,
|
TRADER_EXECUTION_BLOCKED,
|
||||||
TRADER_SAMPLE_EXPORT_FAILED
|
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;
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
public enum TraderRunMode {
|
public enum TraderRunMode {
|
||||||
REPLAY,
|
REPLAY_SIM,
|
||||||
SHADOW,
|
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;
|
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.domain.TraderEvidence;
|
||||||
import com.quantai.trader.persistence.TraderEvidenceRepository;
|
|
||||||
import com.quantai.trader.util.Ids;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
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
|
@Component
|
||||||
public class EvidenceAppender {
|
public class EvidenceAppender {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class);
|
private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class);
|
||||||
private final TraderEvidenceRepository repository;
|
private final CopyOnWriteArrayList<TraderEvidence> evidence = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public EvidenceAppender(TraderEvidenceRepository repository) {
|
public TraderEvidence append(String runId, String cycleId, String stage, boolean pass, String reason, String blocker, Map<String, Object> details) {
|
||||||
this.repository = repository;
|
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) {
|
public List<TraderEvidence> all() {
|
||||||
TraderEvidence evidence = new TraderEvidence(
|
return new ArrayList<>(evidence);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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