Initial quant trader service baseline
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# Runtime and local data stay outside source control.
|
||||
logs/
|
||||
run/
|
||||
tmp/
|
||||
*.pid
|
||||
|
||||
# Training data and model artifacts live under /Users/zach/Desktop/quant-strategy-training-data.
|
||||
data/
|
||||
models/
|
||||
artifacts/
|
||||
@@ -0,0 +1,65 @@
|
||||
# quant-trader-service
|
||||
|
||||
Clean P0 rebuild of the Trader-style strategy service.
|
||||
|
||||
## Scope
|
||||
|
||||
This implementation follows the desktop design documents in
|
||||
`/Users/zach/Desktop/app/trader`:
|
||||
|
||||
- `00-操盘手Trader新策略服务概要设计-20260622.md`
|
||||
- `03-Trader服务详细设计说明书-20260623.md`
|
||||
|
||||
P0 keeps the service in replay/shadow preparation mode:
|
||||
|
||||
- no real trading
|
||||
- no old V3 `latest/promoted` path
|
||||
- no `SCALE_IN` action surface
|
||||
- no real App feedback by default
|
||||
- no model training that can control live size
|
||||
|
||||
## Implemented P0 Surface
|
||||
|
||||
- Spring Boot P0 service under `com.quantai.trader`
|
||||
- Playbook YAML loading, validation, normalized JSON, SHA-256 definition hash
|
||||
- MySQL 8 Flyway DDL for the P0 trader tables
|
||||
- Core domain records for cycles, actions, entry plans, risk, evidence, feedback, samples, and reports
|
||||
- State-machine guardrails for initial entry, planned legs, and management actions
|
||||
- Dynamic position sizing from signal, execution quality, and risk scores
|
||||
- Risk gate that records every allow/block decision
|
||||
- Evidence appender and proxy-only training sample exporter
|
||||
- Async replay-run registry and report contract
|
||||
- MyBatis-Plus repositories aligned with `quant-app-server`
|
||||
- Deterministic JSONL replay fixtures for accepted, rejected, blocked, and hard-fail paths
|
||||
- Feedback endpoint that returns `TRADER_FEEDBACK_DISABLED` unless explicitly enabled
|
||||
- Focused unit and MVC tests
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
The service uses MySQL through MyBatis-Plus. Configure the database with
|
||||
`TRADER_DB_URL`, `TRADER_DB_USERNAME`, and `TRADER_DB_PASSWORD` when the local
|
||||
defaults are not available.
|
||||
|
||||
Replay acceptance fixtures live under `src/test/resources/replay-fixtures`.
|
||||
They are deterministic P0 contract samples, not a profitability backtest corpus.
|
||||
|
||||
## HTTP
|
||||
|
||||
```text
|
||||
GET /api/trader/health
|
||||
GET /api/trader/playbooks
|
||||
GET /api/trader/playbooks/{playbookId}
|
||||
POST /api/trader/replay/runs
|
||||
GET /api/trader/replay/runs/{runId}
|
||||
POST /api/trader/replay/runs/{runId}/cancel
|
||||
GET /api/trader/replay/runs/{runId}/report
|
||||
POST /api/trader/feedback
|
||||
```
|
||||
|
||||
Remark: `/api/trader/feedback` is intentionally disabled in P0 unless
|
||||
`trader.integration.http-feedback-enabled=true`.
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.quantai</groupId>
|
||||
<artifactId>quant-trader-service</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>quant-trader-service</name>
|
||||
<description>Clean P0 rebuild of the Trader-style strategy service.</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-flyway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.quantai.trader;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@MapperScan("com.quantai.trader.infrastructure.mapper")
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class QuantTraderServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(QuantTraderServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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(),
|
||||
TraderSide.LONG,
|
||||
playbook.variant(),
|
||||
snapshot.snapshotTime(),
|
||||
pricePlan,
|
||||
playbook.definition().plannedEntryLegRule().maxPlannedEntryLegs(),
|
||||
snapshot.setupFeatures()
|
||||
));
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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()), 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, 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()), 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()), 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(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.quantai.trader.config;
|
||||
|
||||
import com.quantai.trader.enums.TraderRunMode;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@ConfigurationProperties(prefix = "trader")
|
||||
public class TraderProperties {
|
||||
|
||||
private String serviceName = "quant-trader-service";
|
||||
private TraderRunMode runMode = TraderRunMode.REPLAY;
|
||||
private String symbol = "BTCUSDT";
|
||||
private String featureVersion = "trader_feature_v0";
|
||||
private String labelVersion = "trader_label_v0";
|
||||
private Playbook playbook = new Playbook();
|
||||
private Replay replay = new Replay();
|
||||
private Integration integration = new Integration();
|
||||
private Risk risk = new Risk();
|
||||
private Sizing sizing = new Sizing();
|
||||
private DataSource dataSource = new DataSource();
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
public void setServiceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
public TraderRunMode getRunMode() {
|
||||
return runMode;
|
||||
}
|
||||
|
||||
public void setRunMode(TraderRunMode runMode) {
|
||||
this.runMode = runMode;
|
||||
}
|
||||
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
public void setSymbol(String symbol) {
|
||||
this.symbol = symbol;
|
||||
}
|
||||
|
||||
public String getFeatureVersion() {
|
||||
return featureVersion;
|
||||
}
|
||||
|
||||
public void setFeatureVersion(String featureVersion) {
|
||||
this.featureVersion = featureVersion;
|
||||
}
|
||||
|
||||
public String getLabelVersion() {
|
||||
return labelVersion;
|
||||
}
|
||||
|
||||
public void setLabelVersion(String labelVersion) {
|
||||
this.labelVersion = labelVersion;
|
||||
}
|
||||
|
||||
public Playbook getPlaybook() {
|
||||
return playbook;
|
||||
}
|
||||
|
||||
public void setPlaybook(Playbook playbook) {
|
||||
this.playbook = playbook;
|
||||
}
|
||||
|
||||
public Replay getReplay() {
|
||||
return replay;
|
||||
}
|
||||
|
||||
public void setReplay(Replay replay) {
|
||||
this.replay = replay;
|
||||
}
|
||||
|
||||
public Integration getIntegration() {
|
||||
return integration;
|
||||
}
|
||||
|
||||
public void setIntegration(Integration integration) {
|
||||
this.integration = integration;
|
||||
}
|
||||
|
||||
public Risk getRisk() {
|
||||
return risk;
|
||||
}
|
||||
|
||||
public void setRisk(Risk risk) {
|
||||
this.risk = risk;
|
||||
}
|
||||
|
||||
public Sizing getSizing() {
|
||||
return sizing;
|
||||
}
|
||||
|
||||
public void setSizing(Sizing sizing) {
|
||||
this.sizing = sizing;
|
||||
}
|
||||
|
||||
public DataSource getDataSource() {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
public void setDataSource(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public static class Playbook {
|
||||
private String locationPattern = "classpath:/playbooks/*.yml";
|
||||
|
||||
public String getLocationPattern() {
|
||||
return locationPattern;
|
||||
}
|
||||
|
||||
public void setLocationPattern(String locationPattern) {
|
||||
this.locationPattern = locationPattern;
|
||||
}
|
||||
}
|
||||
|
||||
public 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) {
|
||||
this.hashMode = hashMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.quantai.trader.controller;
|
||||
|
||||
public record TraderApiError(
|
||||
String code,
|
||||
String message
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.quantai.trader.controller;
|
||||
|
||||
import com.quantai.trader.domain.TraderException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class TraderApiExceptionHandler {
|
||||
|
||||
@ExceptionHandler(TraderException.class)
|
||||
public ResponseEntity<TraderApiError> handleTraderException(TraderException ex) {
|
||||
return ResponseEntity.badRequest().body(new TraderApiError(ex.errorCode().name(), ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<TraderApiError> handleIllegalArgument(IllegalArgumentException ex) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new TraderApiError("TRADER_NOT_FOUND", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.quantai.trader.controller;
|
||||
|
||||
import com.quantai.trader.config.TraderProperties;
|
||||
import com.quantai.trader.enums.TraderErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/trader/feedback")
|
||||
public class TraderFeedbackController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class);
|
||||
private final TraderProperties properties;
|
||||
|
||||
public TraderFeedbackController(TraderProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> feedback(@RequestBody TraderFeedbackRequest request) {
|
||||
if (!properties.getIntegration().isHttpFeedbackEnabled()) {
|
||||
log.info(
|
||||
"event=trader.feedback.rejected runId={} cycleId={} actionId={} reason={}",
|
||||
request.runId(),
|
||||
request.cycleId(),
|
||||
request.actionId(),
|
||||
TraderErrorCode.TRADER_FEEDBACK_DISABLED
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new TraderApiError(
|
||||
TraderErrorCode.TRADER_FEEDBACK_DISABLED.name(),
|
||||
"P0 feedback endpoint is disabled; future App main channel is trader-core jar"
|
||||
));
|
||||
}
|
||||
return ResponseEntity.accepted().body(Map.of(
|
||||
"status", "ACCEPTED_CONTRACT_ONLY",
|
||||
"runId", request.runId(),
|
||||
"actionId", request.actionId()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.quantai.trader.controller;
|
||||
|
||||
import com.quantai.trader.config.TraderProperties;
|
||||
import com.quantai.trader.playbook.TraderPlaybookCatalog;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/trader")
|
||||
public class TraderHealthController {
|
||||
|
||||
private final TraderProperties properties;
|
||||
private final TraderPlaybookCatalog catalog;
|
||||
|
||||
public TraderHealthController(TraderProperties properties, TraderPlaybookCatalog catalog) {
|
||||
this.properties = properties;
|
||||
this.catalog = catalog;
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
public Map<String, Object> health() {
|
||||
return Map.of(
|
||||
"service", properties.getServiceName(),
|
||||
"runMode", properties.getRunMode(),
|
||||
"symbol", properties.getSymbol(),
|
||||
"playbookCount", catalog.list().size(),
|
||||
"httpFeedbackEnabled", properties.getIntegration().isHttpFeedbackEnabled()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.quantai.trader.controller;
|
||||
|
||||
import com.quantai.trader.domain.TraderReplayReport;
|
||||
import com.quantai.trader.persistence.ReplayReportRepository;
|
||||
import com.quantai.trader.replay.ReplayRun;
|
||||
import com.quantai.trader.replay.ReplayRunConfig;
|
||||
import com.quantai.trader.replay.ReplayRunResponse;
|
||||
import com.quantai.trader.replay.ReplayRunService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/trader/replay/runs")
|
||||
public class TraderReplayController {
|
||||
|
||||
private final ReplayRunService replayRunService;
|
||||
private final ReplayReportRepository reportRepository;
|
||||
|
||||
public TraderReplayController(ReplayRunService replayRunService, ReplayReportRepository reportRepository) {
|
||||
this.replayRunService = replayRunService;
|
||||
this.reportRepository = reportRepository;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public ReplayRunResponse create(@RequestBody ReplayRunConfig config) {
|
||||
return replayRunService.createRun(config);
|
||||
}
|
||||
|
||||
@GetMapping("/{runId}")
|
||||
public ReplayRun get(@PathVariable String runId) {
|
||||
return replayRunService.find(runId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("replay run not found: " + runId));
|
||||
}
|
||||
|
||||
@PostMapping("/{runId}/cancel")
|
||||
public ReplayRun cancel(@PathVariable String runId) {
|
||||
return replayRunService.cancel(runId);
|
||||
}
|
||||
|
||||
@GetMapping("/{runId}/report")
|
||||
public TraderReplayReport report(@PathVariable String runId) {
|
||||
return reportRepository.findByRunId(runId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("replay report not found: " + runId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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,16 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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,25 @@
|
||||
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,14 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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,27 @@
|
||||
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,30 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import com.quantai.trader.enums.TraderActionType;
|
||||
import com.quantai.trader.enums.TraderSide;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record TraderAction(
|
||||
String runId,
|
||||
String cycleId,
|
||||
String actionId,
|
||||
TraderActionType actionType,
|
||||
String playbookId,
|
||||
String playbookVersion,
|
||||
String symbol,
|
||||
TraderSide side,
|
||||
BigDecimal price,
|
||||
BigDecimal quantity,
|
||||
Instant actionTime,
|
||||
String reason,
|
||||
Map<String, Object> actionContext,
|
||||
String sendStatus
|
||||
) {
|
||||
|
||||
public TraderAction {
|
||||
actionContext = Maps.immutable(actionContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import com.quantai.trader.enums.TraderFeedbackSource;
|
||||
import com.quantai.trader.enums.TraderFeedbackType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record TraderAppFeedback(
|
||||
String runId,
|
||||
String cycleId,
|
||||
String actionId,
|
||||
TraderFeedbackType feedbackType,
|
||||
TraderFeedbackSource feedbackSource,
|
||||
String proxyMethod,
|
||||
String simulatorVersion,
|
||||
boolean realFill,
|
||||
String orderId,
|
||||
String positionId,
|
||||
String orderStatus,
|
||||
Instant appReceivedTime,
|
||||
Instant exchangeAckTime,
|
||||
Instant filledTime,
|
||||
BigDecimal filledPrice,
|
||||
BigDecimal filledQuantity,
|
||||
BigDecimal fee,
|
||||
BigDecimal slippageBps,
|
||||
String closeReason,
|
||||
String closeSignalSource,
|
||||
String exchangeErrorCode,
|
||||
String platformErrorCode,
|
||||
Map<String, Object> rawFeedback
|
||||
) {
|
||||
|
||||
public TraderAppFeedback {
|
||||
rawFeedback = Maps.immutable(rawFeedback);
|
||||
boolean sourceCanBeReal = feedbackSource == TraderFeedbackSource.PAPER_APP
|
||||
|| feedbackSource == TraderFeedbackSource.REAL_APP;
|
||||
if (realFill != sourceCanBeReal) {
|
||||
throw new IllegalArgumentException("feedback_source and realFill are inconsistent");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import com.quantai.trader.enums.TraderRunMode;
|
||||
import com.quantai.trader.enums.TraderState;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record TraderDecisionCycle(
|
||||
String runId,
|
||||
String cycleId,
|
||||
String snapshotId,
|
||||
String symbol,
|
||||
String playbookId,
|
||||
String playbookVersion,
|
||||
TraderState state,
|
||||
Instant cycleTime,
|
||||
TraderRunMode runMode,
|
||||
String decisionStatus,
|
||||
String blocker
|
||||
) {
|
||||
|
||||
public TraderDecisionCycle withState(TraderState nextState, String nextStatus, String nextBlocker) {
|
||||
return new TraderDecisionCycle(
|
||||
runId,
|
||||
cycleId,
|
||||
snapshotId,
|
||||
symbol,
|
||||
playbookId,
|
||||
playbookVersion,
|
||||
nextState,
|
||||
cycleTime,
|
||||
runMode,
|
||||
nextStatus,
|
||||
nextBlocker
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record TraderEvidence(
|
||||
String runId,
|
||||
String cycleId,
|
||||
String evidenceId,
|
||||
String stage,
|
||||
boolean pass,
|
||||
String reason,
|
||||
String blocker,
|
||||
Instant evidenceTime,
|
||||
Map<String, Object> details
|
||||
) {
|
||||
|
||||
public TraderEvidence {
|
||||
details = Maps.immutable(details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import com.quantai.trader.enums.TraderErrorCode;
|
||||
|
||||
public class TraderException extends RuntimeException {
|
||||
|
||||
private final TraderErrorCode errorCode;
|
||||
|
||||
public TraderException(TraderErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public TraderErrorCode errorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record TraderMarketSnapshot(
|
||||
String runId,
|
||||
String cycleId,
|
||||
String snapshotId,
|
||||
String symbol,
|
||||
Instant snapshotTime,
|
||||
String featureVersion,
|
||||
Map<String, Object> contextFeatures,
|
||||
Map<String, Object> setupFeatures,
|
||||
Map<String, Object> triggerFeatures,
|
||||
Map<String, Object> executionFeatures,
|
||||
Map<String, Object> dataQuality
|
||||
) {
|
||||
|
||||
public TraderMarketSnapshot {
|
||||
contextFeatures = Maps.immutable(contextFeatures);
|
||||
setupFeatures = Maps.immutable(setupFeatures);
|
||||
triggerFeatures = Maps.immutable(triggerFeatures);
|
||||
executionFeatures = Maps.immutable(executionFeatures);
|
||||
dataQuality = Maps.immutable(dataQuality);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.quantai.trader.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record TraderModelOutput(
|
||||
String modelName,
|
||||
String modelVersion,
|
||||
BigDecimal score,
|
||||
BigDecimal uncertainty,
|
||||
BigDecimal oodScore,
|
||||
Instant predictedAt,
|
||||
Map<String, Object> details
|
||||
) {
|
||||
|
||||
public TraderModelOutput {
|
||||
details = Maps.immutable(details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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,43 @@
|
||||
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,23 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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 TraderRiskDecision(
|
||||
String runId,
|
||||
String cycleId,
|
||||
String actionId,
|
||||
String accountStateId,
|
||||
TraderActionType actionType,
|
||||
BigDecimal leverageScreen,
|
||||
BigDecimal plannedTotalPositionRatio,
|
||||
BigDecimal maxLossBps,
|
||||
BigDecimal liquidationBufferBps,
|
||||
BigDecimal expectedValueBps1x,
|
||||
BigDecimal expectedValueBps10x,
|
||||
BigDecimal uncertainty,
|
||||
BigDecimal oodScore,
|
||||
boolean allowAction,
|
||||
String blocker,
|
||||
Map<String, Object> decision,
|
||||
Instant createdAt
|
||||
) {
|
||||
|
||||
public TraderRiskDecision {
|
||||
decision = Maps.immutable(decision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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,10 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum ReplayRunStatus {
|
||||
CREATED,
|
||||
RUNNING,
|
||||
CANCEL_REQUESTED,
|
||||
CANCELLED,
|
||||
COMPLETED,
|
||||
FAILED
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderActionType {
|
||||
WAIT,
|
||||
OPEN_INITIAL,
|
||||
OPEN_PLANNED_LEG,
|
||||
HOLD,
|
||||
REDUCE,
|
||||
MOVE_STOP,
|
||||
CLOSE,
|
||||
CANCEL,
|
||||
REQUOTE
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderErrorCode {
|
||||
TRADER_PLAYBOOK_VERSION_CONFLICT,
|
||||
TRADER_DATA_SOURCE_MISSING,
|
||||
TRADER_DATA_QUALITY_FAILED,
|
||||
TRADER_ENTRY_PLAN_INCOMPLETE,
|
||||
TRADER_ILLEGAL_ACTION_TRANSITION,
|
||||
TRADER_PLANNED_LEG_AFTER_REDUCE,
|
||||
TRADER_RISK_BLOCKED,
|
||||
TRADER_FEEDBACK_DISABLED,
|
||||
TRADER_SAMPLE_EXPORT_FAILED
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderFeedbackSource {
|
||||
MARKET_PROXY,
|
||||
SHADOW_APP,
|
||||
PAPER_APP,
|
||||
REAL_APP
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderFeedbackType {
|
||||
FILL_EVENT,
|
||||
CANCEL_EVENT,
|
||||
CLOSE_EVENT,
|
||||
REJECT_EVENT
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderPlaybookId {
|
||||
BREAKOUT_RETEST_CONTINUATION,
|
||||
SUPPORT_PULLBACK_CONTINUATION,
|
||||
FALSE_BREAK_RECLAIM
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderRunMode {
|
||||
REPLAY,
|
||||
SHADOW,
|
||||
PAPER
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.quantai.trader.enums;
|
||||
|
||||
public enum TraderSide {
|
||||
LONG,
|
||||
SHORT
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.quantai.trader.evidence;
|
||||
|
||||
import com.quantai.trader.domain.StageDecision;
|
||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
||||
import com.quantai.trader.domain.TraderEvidence;
|
||||
import com.quantai.trader.persistence.TraderEvidenceRepository;
|
||||
import com.quantai.trader.util.Ids;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class EvidenceAppender {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class);
|
||||
private final TraderEvidenceRepository repository;
|
||||
|
||||
public EvidenceAppender(TraderEvidenceRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public TraderEvidence append(TraderDecisionCycle cycle, String stage, StageDecision decision) {
|
||||
TraderEvidence evidence = new TraderEvidence(
|
||||
cycle.runId(),
|
||||
cycle.cycleId(),
|
||||
Ids.evidenceId(cycle, stage),
|
||||
stage,
|
||||
decision.pass(),
|
||||
decision.reason(),
|
||||
decision.blocker(),
|
||||
cycle.cycleTime(),
|
||||
decision.details()
|
||||
);
|
||||
repository.insert(evidence);
|
||||
log.info(
|
||||
"event=trader.evidence runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} stage={} pass={} reason={} blocker={}",
|
||||
cycle.runId(),
|
||||
cycle.cycleId(),
|
||||
cycle.symbol(),
|
||||
cycle.playbookId(),
|
||||
cycle.playbookVersion(),
|
||||
cycle.state(),
|
||||
stage,
|
||||
decision.pass(),
|
||||
decision.reason(),
|
||||
decision.blocker()
|
||||
);
|
||||
return evidence;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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
@@ -0,0 +1,7 @@
|
||||
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> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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> {
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
public TraderMarketSnapshot buildNextManagementSnapshot(
|
||||
TraderMarketSnapshot previous,
|
||||
ReplayClockTick tick,
|
||||
TraderRuntimeState runtimeState
|
||||
) {
|
||||
return build(tick, runtimeState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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,75 @@
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.quantai.trader.domain.TraderException;
|
||||
import com.quantai.trader.enums.TraderErrorCode;
|
||||
import com.quantai.trader.infrastructure.entity.TraderPlaybookDefinitionEntity;
|
||||
import com.quantai.trader.infrastructure.mapper.TraderPlaybookDefinitionMapper;
|
||||
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class MybatisTraderPlaybookDefinitionRepository implements TraderPlaybookDefinitionRepository {
|
||||
|
||||
private final TraderPlaybookDefinitionMapper mapper;
|
||||
|
||||
public MybatisTraderPlaybookDefinitionRepository(TraderPlaybookDefinitionMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertPlaybookDefinitionIfAbsent(TraderPlaybookDefinitionSnapshot definition) {
|
||||
Optional<TraderPlaybookDefinitionSnapshot> existing = findPlaybookDefinition(
|
||||
definition.playbookId(),
|
||||
definition.playbookVersion()
|
||||
);
|
||||
if (existing.isPresent()) {
|
||||
if (!existing.get().definitionHashSha256().equals(definition.definitionHashSha256())) {
|
||||
throw new TraderException(
|
||||
TraderErrorCode.TRADER_PLAYBOOK_VERSION_CONFLICT,
|
||||
"playbook version already exists with another definition hash"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
mapper.insert(toEntity(definition));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TraderPlaybookDefinitionSnapshot> findPlaybookDefinition(String playbookId, String playbookVersion) {
|
||||
TraderPlaybookDefinitionEntity entity = mapper.selectOne(
|
||||
Wrappers.<TraderPlaybookDefinitionEntity>lambdaQuery()
|
||||
.eq(TraderPlaybookDefinitionEntity::getPlaybookId, playbookId)
|
||||
.eq(TraderPlaybookDefinitionEntity::getPlaybookVersion, playbookVersion)
|
||||
);
|
||||
return Optional.ofNullable(entity).map(this::toDomain);
|
||||
}
|
||||
|
||||
private TraderPlaybookDefinitionEntity toEntity(TraderPlaybookDefinitionSnapshot definition) {
|
||||
TraderPlaybookDefinitionEntity entity = new TraderPlaybookDefinitionEntity();
|
||||
entity.setPlaybookId(definition.playbookId());
|
||||
entity.setPlaybookVersion(definition.playbookVersion());
|
||||
entity.setFamily(definition.family());
|
||||
entity.setVariant(definition.variant());
|
||||
entity.setSideMode(Objects.requireNonNull(definition.definition(), "playbook definition is required").sideMode());
|
||||
entity.setSourcePath(definition.sourcePath());
|
||||
entity.setDefinitionHashSha256(definition.definitionHashSha256());
|
||||
entity.setDefinitionJson(definition.definitionJson());
|
||||
entity.setLoadedAt(TraderPersistenceCodec.local(definition.loadedAt()));
|
||||
entity.setStatus(definition.status());
|
||||
entity.setCreatedAt(TraderPersistenceCodec.local(Instant.now()));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private TraderPlaybookDefinitionSnapshot toDomain(TraderPlaybookDefinitionEntity entity) {
|
||||
String definitionJson = entity.getDefinitionJson();
|
||||
return new TraderPlaybookDefinitionSnapshot(
|
||||
entity.getPlaybookId(),
|
||||
entity.getPlaybookVersion(),
|
||||
entity.getFamily(),
|
||||
entity.getVariant(),
|
||||
entity.getSourcePath(),
|
||||
entity.getDefinitionHashSha256(),
|
||||
definitionJson,
|
||||
TraderPersistenceCodec.instant(entity.getLoadedAt()),
|
||||
entity.getStatus(),
|
||||
TraderPersistenceCodec.playbookDefinition(definitionJson)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.quantai.trader.domain.TraderRiskDecision;
|
||||
import com.quantai.trader.enums.TraderActionType;
|
||||
import com.quantai.trader.infrastructure.entity.TraderRiskDecisionEntity;
|
||||
import com.quantai.trader.infrastructure.mapper.TraderRiskDecisionMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class MybatisTraderRiskDecisionRepository implements TraderRiskDecisionRepository {
|
||||
|
||||
private final TraderRiskDecisionMapper mapper;
|
||||
|
||||
public MybatisTraderRiskDecisionRepository(TraderRiskDecisionMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(TraderRiskDecision decision) {
|
||||
TraderRiskDecisionEntity entity = new TraderRiskDecisionEntity();
|
||||
entity.setRunId(decision.runId());
|
||||
entity.setCycleId(decision.cycleId());
|
||||
entity.setActionId(decision.actionId());
|
||||
entity.setAccountStateId(decision.accountStateId());
|
||||
entity.setActionType(decision.actionType().name());
|
||||
entity.setLeverageScreen(decision.leverageScreen());
|
||||
entity.setPlannedTotalPositionRatio(decision.plannedTotalPositionRatio());
|
||||
entity.setMaxLossBps(decision.maxLossBps());
|
||||
entity.setLiquidationBufferBps(decision.liquidationBufferBps());
|
||||
entity.setExpectedValueBps1x(decision.expectedValueBps1x());
|
||||
entity.setExpectedValueBps10x(decision.expectedValueBps10x());
|
||||
entity.setUncertainty(decision.uncertainty());
|
||||
entity.setOodScore(decision.oodScore());
|
||||
entity.setAllowAction(decision.allowAction());
|
||||
entity.setBlocker(decision.blocker());
|
||||
entity.setDecisionJson(TraderPersistenceCodec.json(decision.decision()));
|
||||
entity.setCreatedAt(TraderPersistenceCodec.requiredLocal("decision.createdAt", decision.createdAt()));
|
||||
mapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TraderRiskDecision> findByCycleId(String runId, String cycleId) {
|
||||
return mapper.selectList(
|
||||
Wrappers.<TraderRiskDecisionEntity>lambdaQuery()
|
||||
.eq(TraderRiskDecisionEntity::getRunId, runId)
|
||||
.eq(TraderRiskDecisionEntity::getCycleId, cycleId)
|
||||
.orderByAsc(TraderRiskDecisionEntity::getCreatedAt)
|
||||
.orderByAsc(TraderRiskDecisionEntity::getId)
|
||||
).stream().map(this::toDomain).toList();
|
||||
}
|
||||
|
||||
private TraderRiskDecision toDomain(TraderRiskDecisionEntity entity) {
|
||||
return new TraderRiskDecision(
|
||||
entity.getRunId(),
|
||||
entity.getCycleId(),
|
||||
entity.getActionId(),
|
||||
entity.getAccountStateId(),
|
||||
TraderActionType.valueOf(entity.getActionType()),
|
||||
entity.getLeverageScreen(),
|
||||
entity.getPlannedTotalPositionRatio(),
|
||||
entity.getMaxLossBps(),
|
||||
entity.getLiquidationBufferBps(),
|
||||
entity.getExpectedValueBps1x(),
|
||||
entity.getExpectedValueBps10x(),
|
||||
entity.getUncertainty(),
|
||||
entity.getOodScore(),
|
||||
Boolean.TRUE.equals(entity.getAllowAction()),
|
||||
entity.getBlocker(),
|
||||
TraderPersistenceCodec.map(entity.getDecisionJson()),
|
||||
TraderPersistenceCodec.instant(entity.getCreatedAt())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.quantai.trader.domain.TraderTrainingSample;
|
||||
import com.quantai.trader.infrastructure.entity.TraderTrainingSampleEntity;
|
||||
import com.quantai.trader.infrastructure.mapper.TraderTrainingSampleMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class MybatisTraderSampleRepository implements TraderSampleRepository {
|
||||
|
||||
private final TraderTrainingSampleMapper mapper;
|
||||
|
||||
public MybatisTraderSampleRepository(TraderTrainingSampleMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(TraderTrainingSample sample) {
|
||||
TraderTrainingSampleEntity entity = new TraderTrainingSampleEntity();
|
||||
entity.setRunId(sample.runId());
|
||||
entity.setCycleId(sample.cycleId());
|
||||
entity.setSampleId(sample.sampleId());
|
||||
entity.setActionId(sample.actionId());
|
||||
entity.setPositionId(sample.positionId());
|
||||
entity.setFeatureVersion(sample.featureVersion());
|
||||
entity.setLabelVersion(sample.labelVersion());
|
||||
entity.setSampleTime(TraderPersistenceCodec.local(sample.sampleTime()));
|
||||
entity.setFeaturesJson(TraderPersistenceCodec.json(sample.features()));
|
||||
entity.setLabelsJson(TraderPersistenceCodec.json(sample.labels()));
|
||||
entity.setNetReturnBps1x(sample.netReturnBps1x());
|
||||
entity.setNetReturnBps10x(sample.netReturnBps10x());
|
||||
entity.setProxyOnly(sample.proxyOnly());
|
||||
entity.setCreatedAt(TraderPersistenceCodec.local(Instant.now()));
|
||||
mapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TraderTrainingSample> findByRunId(String runId) {
|
||||
return mapper.selectList(
|
||||
Wrappers.<TraderTrainingSampleEntity>lambdaQuery()
|
||||
.eq(TraderTrainingSampleEntity::getRunId, runId)
|
||||
.orderByAsc(TraderTrainingSampleEntity::getSampleTime)
|
||||
.orderByAsc(TraderTrainingSampleEntity::getId)
|
||||
).stream().map(this::toDomain).toList();
|
||||
}
|
||||
|
||||
private TraderTrainingSample toDomain(TraderTrainingSampleEntity entity) {
|
||||
return new TraderTrainingSample(
|
||||
entity.getRunId(),
|
||||
entity.getCycleId(),
|
||||
entity.getSampleId(),
|
||||
entity.getActionId(),
|
||||
entity.getPositionId(),
|
||||
entity.getFeatureVersion(),
|
||||
entity.getLabelVersion(),
|
||||
TraderPersistenceCodec.instant(entity.getSampleTime()),
|
||||
TraderPersistenceCodec.map(entity.getFeaturesJson()),
|
||||
TraderPersistenceCodec.map(entity.getLabelsJson()),
|
||||
entity.getNetReturnBps1x(),
|
||||
entity.getNetReturnBps10x(),
|
||||
Boolean.TRUE.equals(entity.getProxyOnly())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.quantai.trader.domain.TraderReplayReport;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ReplayReportRepository {
|
||||
|
||||
void insert(TraderReplayReport report);
|
||||
|
||||
Optional<TraderReplayReport> findByRunId(String runId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.quantai.trader.replay.ReplayRun;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ReplayRunRepository {
|
||||
|
||||
void insert(ReplayRun run);
|
||||
|
||||
void update(ReplayRun run);
|
||||
|
||||
Optional<ReplayRun> findByRunId(String runId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.quantai.trader.domain.TraderEvidence;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TraderEvidenceRepository {
|
||||
|
||||
void insert(TraderEvidence evidence);
|
||||
|
||||
List<TraderEvidence> findByCycleId(String runId, String cycleId);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.quantai.trader.playbook.TraderPlaybookDefinition;
|
||||
import com.quantai.trader.replay.ReplayRunConfig;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class TraderPersistenceCodec {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().findAndRegisterModules();
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
private static final TypeReference<List<String>> STRING_LIST_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private TraderPersistenceCodec() {
|
||||
}
|
||||
|
||||
static String json(Object value) {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(Objects.requireNonNull(value, "json value is required"));
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to serialize trader persistence json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, Object> map(String json) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(requiredJson(json), MAP_TYPE);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to deserialize trader persistence json map", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> stringList(String json) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(requiredJson(json), STRING_LIST_TYPE);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to deserialize trader persistence json list", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static ReplayRunConfig replayRunConfig(String json) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(requiredJson(json), ReplayRunConfig.class);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to deserialize replay run config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static TraderPlaybookDefinition playbookDefinition(String json) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(requiredJson(json), TraderPlaybookDefinition.class);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to deserialize playbook definition", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static LocalDateTime local(Instant value) {
|
||||
return value == null ? null : LocalDateTime.ofInstant(value, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
static Instant instant(LocalDateTime value) {
|
||||
return value == null ? null : value.toInstant(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
static LocalDateTime requiredLocal(String fieldName, Instant value) {
|
||||
return LocalDateTime.ofInstant(Objects.requireNonNull(value, fieldName + " is required"), ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private static String requiredJson(String json) {
|
||||
Objects.requireNonNull(json, "json text is required");
|
||||
String trimmed = json.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
throw new IllegalArgumentException("json text is blank");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TraderPlaybookDefinitionRepository {
|
||||
|
||||
void insertPlaybookDefinitionIfAbsent(TraderPlaybookDefinitionSnapshot definition);
|
||||
|
||||
Optional<TraderPlaybookDefinitionSnapshot> findPlaybookDefinition(String playbookId, String playbookVersion);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.quantai.trader.domain.TraderRiskDecision;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TraderRiskDecisionRepository {
|
||||
|
||||
void insert(TraderRiskDecision decision);
|
||||
|
||||
List<TraderRiskDecision> findByCycleId(String runId, String cycleId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.quantai.trader.persistence;
|
||||
|
||||
import com.quantai.trader.domain.TraderTrainingSample;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TraderSampleRepository {
|
||||
|
||||
void insert(TraderTrainingSample sample);
|
||||
|
||||
List<TraderTrainingSample> findByRunId(String runId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.quantai.trader.playbook;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record RuleDefinition(
|
||||
String name,
|
||||
String description,
|
||||
@JsonProperty("max_planned_entry_legs")
|
||||
Integer maxPlannedEntryLegs,
|
||||
@JsonProperty("ratio_mode")
|
||||
String ratioMode,
|
||||
@JsonProperty("allow_full_initial_entry")
|
||||
Boolean allowFullInitialEntry,
|
||||
@JsonProperty("ratio_template_fixed")
|
||||
Boolean ratioTemplateFixed,
|
||||
@JsonProperty("description_ext")
|
||||
String descriptionExt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.quantai.trader.playbook;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.quantai.trader.config.TraderProperties;
|
||||
import com.quantai.trader.persistence.TraderPlaybookDefinitionRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class TraderPlaybookCatalog {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TraderPlaybookCatalog.class);
|
||||
private final TraderProperties properties;
|
||||
private final TraderPlaybookValidator validator;
|
||||
private final TraderPlaybookDefinitionRepository repository;
|
||||
private final ObjectMapper yamlMapper;
|
||||
private final ObjectMapper jsonMapper;
|
||||
private volatile Map<String, TraderPlaybookDefinitionSnapshot> definitions = Map.of();
|
||||
|
||||
public TraderPlaybookCatalog(
|
||||
TraderProperties properties,
|
||||
TraderPlaybookValidator validator,
|
||||
TraderPlaybookDefinitionRepository repository
|
||||
) {
|
||||
this.properties = properties;
|
||||
this.validator = validator;
|
||||
this.repository = repository;
|
||||
this.yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
this.jsonMapper = JsonMapper.builder()
|
||||
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
|
||||
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void load() {
|
||||
try {
|
||||
Resource[] resources = new PathMatchingResourcePatternResolver()
|
||||
.getResources(properties.getPlaybook().getLocationPattern());
|
||||
Map<String, TraderPlaybookDefinitionSnapshot> loaded = new LinkedHashMap<>();
|
||||
for (Resource resource : resources) {
|
||||
TraderPlaybookDefinitionSnapshot snapshot = loadResource(resource);
|
||||
repository.insertPlaybookDefinitionIfAbsent(snapshot);
|
||||
loaded.put(snapshot.playbookId(), snapshot);
|
||||
log.info(
|
||||
"event=trader.playbook.loaded playbookId={} playbookVersion={} variant={} definitionHashSha256={} sourcePath={}",
|
||||
snapshot.playbookId(),
|
||||
snapshot.playbookVersion(),
|
||||
snapshot.variant(),
|
||||
snapshot.definitionHashSha256(),
|
||||
snapshot.sourcePath()
|
||||
);
|
||||
}
|
||||
this.definitions = Map.copyOf(loaded);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to load trader playbooks", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public List<TraderPlaybookDefinitionSnapshot> list() {
|
||||
return definitions.values().stream()
|
||||
.sorted(Comparator.comparing(TraderPlaybookDefinitionSnapshot::playbookId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public TraderPlaybookDefinitionSnapshot require(String playbookId) {
|
||||
TraderPlaybookDefinitionSnapshot snapshot = definitions.get(playbookId);
|
||||
if (snapshot == null) {
|
||||
throw new TraderPlaybookValidationException("playbook not loaded: " + playbookId);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public TraderPlaybookDefinitionSnapshot require(String playbookId, String version) {
|
||||
TraderPlaybookDefinitionSnapshot snapshot = require(playbookId);
|
||||
if (!snapshot.playbookVersion().equals(version)) {
|
||||
throw new TraderPlaybookValidationException("playbook version not loaded: " + playbookId + "/" + version);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private TraderPlaybookDefinitionSnapshot loadResource(Resource resource) throws IOException {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
TraderPlaybookDefinition definition = yamlMapper.readValue(inputStream, TraderPlaybookDefinition.class);
|
||||
validator.validate(definition);
|
||||
String definitionJson = normalizedJson(definition);
|
||||
return new TraderPlaybookDefinitionSnapshot(
|
||||
definition.playbookId(),
|
||||
definition.playbookVersion(),
|
||||
definition.family(),
|
||||
definition.variant(),
|
||||
resource.getDescription(),
|
||||
sha256(definitionJson),
|
||||
definitionJson,
|
||||
Instant.now(),
|
||||
"ACTIVE",
|
||||
definition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizedJson(TraderPlaybookDefinition definition) throws IOException {
|
||||
JsonNode node = jsonMapper.valueToTree(definition);
|
||||
return jsonMapper.writeValueAsString(node);
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
||||
for (byte b : bytes) {
|
||||
builder.append(String.format("%02x", b));
|
||||
}
|
||||
return builder.toString();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException("SHA-256 digest is unavailable", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.quantai.trader.playbook;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record TraderPlaybookDefinition(
|
||||
@JsonProperty("playbook_id")
|
||||
String playbookId,
|
||||
@JsonProperty("playbook_version")
|
||||
String playbookVersion,
|
||||
String family,
|
||||
String variant,
|
||||
@JsonProperty("side_mode")
|
||||
String sideMode,
|
||||
@JsonProperty("context_timeframes")
|
||||
List<String> contextTimeframes,
|
||||
@JsonProperty("setup_timeframes")
|
||||
List<String> setupTimeframes,
|
||||
@JsonProperty("trigger_timeframes")
|
||||
List<String> triggerTimeframes,
|
||||
@JsonProperty("execution_window")
|
||||
String executionWindow,
|
||||
@JsonProperty("management_windows")
|
||||
List<String> managementWindows,
|
||||
@JsonProperty("entry_rule")
|
||||
RuleDefinition entryRule,
|
||||
@JsonProperty("planned_entry_leg_rule")
|
||||
RuleDefinition plannedEntryLegRule,
|
||||
@JsonProperty("invalid_rule")
|
||||
RuleDefinition invalidRule,
|
||||
@JsonProperty("stop_rule")
|
||||
RuleDefinition stopRule,
|
||||
@JsonProperty("target_rule")
|
||||
RuleDefinition targetRule,
|
||||
@JsonProperty("partial_take_profit_rule")
|
||||
RuleDefinition partialTakeProfitRule,
|
||||
@JsonProperty("max_hold_rule")
|
||||
RuleDefinition maxHoldRule,
|
||||
@JsonProperty("failure_exit_rule")
|
||||
RuleDefinition failureExitRule,
|
||||
@JsonProperty("risk_constraints")
|
||||
Map<String, String> riskConstraints,
|
||||
@JsonProperty("required_features")
|
||||
List<String> requiredFeatures,
|
||||
@JsonProperty("output_actions")
|
||||
List<String> outputActions
|
||||
) {
|
||||
|
||||
public List<String> outputActions() {
|
||||
return outputActions == null ? List.of() : List.copyOf(outputActions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.quantai.trader.playbook;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record TraderPlaybookDefinitionSnapshot(
|
||||
String playbookId,
|
||||
String playbookVersion,
|
||||
String family,
|
||||
String variant,
|
||||
String sourcePath,
|
||||
String definitionHashSha256,
|
||||
String definitionJson,
|
||||
Instant loadedAt,
|
||||
String status,
|
||||
TraderPlaybookDefinition definition
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.quantai.trader.playbook;
|
||||
|
||||
public class TraderPlaybookValidationException extends RuntimeException {
|
||||
|
||||
public TraderPlaybookValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.quantai.trader.playbook;
|
||||
|
||||
import com.quantai.trader.enums.TraderActionType;
|
||||
import com.quantai.trader.enums.TraderPlaybookId;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class TraderPlaybookValidator {
|
||||
|
||||
private static final Set<String> REQUIRED_FEATURES = Set.of("candles", "trades", "level_1");
|
||||
|
||||
public void validate(TraderPlaybookDefinition definition) {
|
||||
requireText(definition.playbookId(), "playbook_id is required");
|
||||
requireText(definition.playbookVersion(), "playbook_version is required");
|
||||
requireEnum(definition.playbookId());
|
||||
requireRule(definition.entryRule(), "entry_rule is required");
|
||||
requireRule(definition.plannedEntryLegRule(), "planned_entry_leg_rule is required");
|
||||
requireRule(definition.invalidRule(), "invalid_rule is required");
|
||||
requireRule(definition.stopRule(), "stop_rule is required");
|
||||
requireRule(definition.targetRule(), "target_rule is required");
|
||||
requireRule(definition.maxHoldRule(), "max_hold_rule is required");
|
||||
requireOutputActions(definition.outputActions());
|
||||
requirePlannedLegBoundary(definition);
|
||||
requireRequiredFeatures(definition.requiredFeatures());
|
||||
}
|
||||
|
||||
private void requireEnum(String playbookId) {
|
||||
try {
|
||||
TraderPlaybookId.valueOf(playbookId);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new TraderPlaybookValidationException("unknown playbook_id: " + playbookId);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireOutputActions(List<String> outputActions) {
|
||||
if (outputActions == null || outputActions.isEmpty()) {
|
||||
throw new TraderPlaybookValidationException("output_actions is required");
|
||||
}
|
||||
if (outputActions.contains("SCALE_IN")) {
|
||||
throw new TraderPlaybookValidationException("SCALE_IN is not a P0 action");
|
||||
}
|
||||
for (String action : outputActions) {
|
||||
try {
|
||||
TraderActionType.valueOf(action);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new TraderPlaybookValidationException("unsupported output action: " + action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePlannedLegBoundary(TraderPlaybookDefinition definition) {
|
||||
if (definition.outputActions().contains(TraderActionType.OPEN_PLANNED_LEG.name())
|
||||
&& definition.plannedEntryLegRule() == null) {
|
||||
throw new TraderPlaybookValidationException("OPEN_PLANNED_LEG requires planned_entry_leg_rule");
|
||||
}
|
||||
RuleDefinition rule = definition.plannedEntryLegRule();
|
||||
if (rule != null) {
|
||||
int maxLegs = rule.maxPlannedEntryLegs() == null ? 0 : rule.maxPlannedEntryLegs();
|
||||
if (maxLegs < 1 || maxLegs > 3) {
|
||||
throw new TraderPlaybookValidationException("max_planned_entry_legs must be between 1 and 3");
|
||||
}
|
||||
if (Boolean.TRUE.equals(rule.ratioTemplateFixed())) {
|
||||
throw new TraderPlaybookValidationException("fixed leg ratio templates are not allowed in P0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireRequiredFeatures(List<String> requiredFeatures) {
|
||||
if (requiredFeatures == null || !requiredFeatures.containsAll(REQUIRED_FEATURES)) {
|
||||
throw new TraderPlaybookValidationException("required_features must include candles, trades, and level_1");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireRule(RuleDefinition rule, String message) {
|
||||
if (rule == null || rule.name() == null || rule.name().isBlank()) {
|
||||
throw new TraderPlaybookValidationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireText(String value, String message) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new TraderPlaybookValidationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.quantai.trader.position;
|
||||
|
||||
import com.quantai.trader.domain.TraderAction;
|
||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
||||
import com.quantai.trader.domain.TraderException;
|
||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
||||
import com.quantai.trader.domain.TraderPositionPath;
|
||||
import com.quantai.trader.enums.TraderActionType;
|
||||
import com.quantai.trader.enums.TraderErrorCode;
|
||||
import com.quantai.trader.util.Ids;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class TraderPositionManager {
|
||||
|
||||
private final Map<String, TraderPositionPath> pathsByPosition = new ConcurrentHashMap<>();
|
||||
|
||||
public TraderPositionPath simulateOrUpdate(
|
||||
TraderDecisionCycle cycle,
|
||||
TraderAction action,
|
||||
TraderMarketSnapshot snapshot
|
||||
) {
|
||||
return switch (action.actionType()) {
|
||||
case OPEN_INITIAL -> openInitialProxy(cycle, action, snapshot);
|
||||
case OPEN_PLANNED_LEG -> appendPlannedLegProxy(cycle, action, snapshot);
|
||||
case HOLD, MOVE_STOP, CANCEL, REQUOTE -> updatePathOnly(cycle, action, snapshot);
|
||||
case REDUCE -> reducePosition(cycle, action, snapshot);
|
||||
case CLOSE -> closePosition(cycle, action, snapshot);
|
||||
case WAIT -> noPositionChange(cycle, action, snapshot);
|
||||
};
|
||||
}
|
||||
|
||||
private TraderPositionPath openInitialProxy(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
BigDecimal ratio = contextDecimal(action, "plannedLegRatio");
|
||||
String positionId = Ids.positionId(cycle.runId(), cycle.symbol(), action.side().name(), 1);
|
||||
TraderPositionPath path = new TraderPositionPath(
|
||||
cycle.runId(),
|
||||
cycle.cycleId(),
|
||||
action.actionId(),
|
||||
positionId,
|
||||
action.side(),
|
||||
action.actionTime(),
|
||||
snapshot.snapshotTime(),
|
||||
action.price(),
|
||||
currentPrice(action, snapshot),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
ratio,
|
||||
Map.of("proxyFill", true, "lastAction", action.actionType().name())
|
||||
);
|
||||
pathsByPosition.put(positionId, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private TraderPositionPath appendPlannedLegProxy(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
String positionId = String.valueOf(action.actionContext().get("positionId"));
|
||||
TraderPositionPath existing = pathsByPosition.get(positionId);
|
||||
if (existing == null) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_ILLEGAL_ACTION_TRANSITION, "planned leg position path not found: " + positionId);
|
||||
}
|
||||
if (existing.reduceSeen()) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_PLANNED_LEG_AFTER_REDUCE, "planned leg cannot follow reduce");
|
||||
}
|
||||
BigDecimal ratio = contextDecimal(action, "plannedLegRatio");
|
||||
TraderPositionPath updated = new TraderPositionPath(
|
||||
existing.runId(),
|
||||
cycle.cycleId(),
|
||||
action.actionId(),
|
||||
existing.positionId(),
|
||||
existing.side(),
|
||||
existing.entryTime(),
|
||||
snapshot.snapshotTime(),
|
||||
existing.entryPrice(),
|
||||
currentPrice(action, snapshot),
|
||||
existing.mfeBps(),
|
||||
existing.maeBps(),
|
||||
existing.timeToTargetMs(),
|
||||
existing.timeToInvalidMs(),
|
||||
existing.targetBeforeStop(),
|
||||
existing.stagnationTimeoutHit(),
|
||||
true,
|
||||
false,
|
||||
existing.filledLegCount() + 1,
|
||||
existing.totalPositionRatio().add(ratio),
|
||||
Map.of("proxyFill", true, "lastAction", action.actionType().name())
|
||||
);
|
||||
pathsByPosition.put(positionId, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private TraderPositionPath updatePathOnly(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
String positionId = String.valueOf(action.actionContext().get("positionId"));
|
||||
TraderPositionPath existing = pathsByPosition.get(positionId);
|
||||
if (existing == null) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_ILLEGAL_ACTION_TRANSITION, "position path not found: " + positionId);
|
||||
}
|
||||
TraderPositionPath updated = new TraderPositionPath(
|
||||
existing.runId(),
|
||||
cycle.cycleId(),
|
||||
action.actionId(),
|
||||
existing.positionId(),
|
||||
existing.side(),
|
||||
existing.entryTime(),
|
||||
snapshot.snapshotTime(),
|
||||
existing.entryPrice(),
|
||||
currentPrice(action, snapshot),
|
||||
existing.mfeBps(),
|
||||
existing.maeBps(),
|
||||
existing.timeToTargetMs(),
|
||||
existing.timeToInvalidMs(),
|
||||
existing.targetBeforeStop(),
|
||||
existing.stagnationTimeoutHit(),
|
||||
true,
|
||||
existing.reduceSeen(),
|
||||
existing.filledLegCount(),
|
||||
existing.totalPositionRatio(),
|
||||
Map.of("proxyFill", true, "lastAction", action.actionType().name())
|
||||
);
|
||||
pathsByPosition.put(positionId, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private TraderPositionPath reducePosition(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
TraderPositionPath updated = updatePathOnly(cycle, action, snapshot);
|
||||
if (!updated.opened()) {
|
||||
return updated;
|
||||
}
|
||||
TraderPositionPath reduced = new TraderPositionPath(
|
||||
updated.runId(),
|
||||
updated.cycleId(),
|
||||
updated.actionId(),
|
||||
updated.positionId(),
|
||||
updated.side(),
|
||||
updated.entryTime(),
|
||||
updated.lastEventTime(),
|
||||
updated.entryPrice(),
|
||||
updated.currentPrice(),
|
||||
updated.mfeBps(),
|
||||
updated.maeBps(),
|
||||
updated.timeToTargetMs(),
|
||||
updated.timeToInvalidMs(),
|
||||
updated.targetBeforeStop(),
|
||||
updated.stagnationTimeoutHit(),
|
||||
true,
|
||||
true,
|
||||
updated.filledLegCount(),
|
||||
updated.totalPositionRatio().divide(BigDecimal.valueOf(2)),
|
||||
Map.of("proxyFill", true, "lastAction", TraderActionType.REDUCE.name())
|
||||
);
|
||||
pathsByPosition.put(reduced.positionId(), reduced);
|
||||
return reduced;
|
||||
}
|
||||
|
||||
private TraderPositionPath closePosition(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
TraderPositionPath updated = updatePathOnly(cycle, action, snapshot);
|
||||
if (!updated.opened()) {
|
||||
return updated;
|
||||
}
|
||||
TraderPositionPath closed = new TraderPositionPath(
|
||||
updated.runId(),
|
||||
updated.cycleId(),
|
||||
updated.actionId(),
|
||||
updated.positionId(),
|
||||
updated.side(),
|
||||
updated.entryTime(),
|
||||
updated.lastEventTime(),
|
||||
updated.entryPrice(),
|
||||
updated.currentPrice(),
|
||||
updated.mfeBps(),
|
||||
updated.maeBps(),
|
||||
updated.timeToTargetMs(),
|
||||
updated.timeToInvalidMs(),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
updated.reduceSeen(),
|
||||
updated.filledLegCount(),
|
||||
BigDecimal.ZERO,
|
||||
Map.of("proxyFill", true, "lastAction", TraderActionType.CLOSE.name())
|
||||
);
|
||||
pathsByPosition.put(closed.positionId(), closed);
|
||||
return closed;
|
||||
}
|
||||
|
||||
private TraderPositionPath noPositionChange(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
Instant now = snapshot.snapshotTime();
|
||||
return new TraderPositionPath(
|
||||
cycle.runId(),
|
||||
cycle.cycleId(),
|
||||
action.actionId(),
|
||||
null,
|
||||
action.side(),
|
||||
now,
|
||||
now,
|
||||
action.price(),
|
||||
action.price(),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
BigDecimal.ZERO,
|
||||
Map.of("lastAction", action.actionType().name())
|
||||
);
|
||||
}
|
||||
|
||||
private BigDecimal currentPrice(TraderAction action, TraderMarketSnapshot snapshot) {
|
||||
Object lastPrice = snapshot.executionFeatures().get("lastPrice");
|
||||
if (lastPrice instanceof Number number) {
|
||||
return BigDecimal.valueOf(number.doubleValue());
|
||||
}
|
||||
if (lastPrice instanceof String text && !text.isBlank()) {
|
||||
return new BigDecimal(text);
|
||||
}
|
||||
return action.price();
|
||||
}
|
||||
|
||||
private BigDecimal contextDecimal(TraderAction action, String key) {
|
||||
Object value = action.actionContext().get(key);
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
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, "missing action context decimal: " + key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record DataSourceSpec(
|
||||
@JsonProperty("sourceId")
|
||||
String sourceId,
|
||||
String path,
|
||||
@JsonProperty("hashSha256")
|
||||
String hashSha256,
|
||||
@JsonProperty("schemaHashSha256")
|
||||
String schemaHashSha256,
|
||||
Long rowCount,
|
||||
Instant minEventTime,
|
||||
Instant maxEventTime,
|
||||
String timezone,
|
||||
Map<String, Object> missingSummary
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.quantai.trader.domain.TraderException;
|
||||
import com.quantai.trader.enums.TraderErrorCode;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JsonlReplayMarketEventReader implements ReplayMarketEventReader {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JsonlReplayMarketEventReader() {
|
||||
this.objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateReadable(ReplayRunConfig config) {
|
||||
DataSourceSpec source = selectReplaySource(config);
|
||||
if (source.path() == null || source.path().isBlank()) {
|
||||
throw new TraderException(
|
||||
TraderErrorCode.TRADER_DATA_SOURCE_MISSING,
|
||||
"replay source path is required"
|
||||
);
|
||||
}
|
||||
Path path = Path.of(source.path());
|
||||
if (!Files.isRegularFile(path) || !Files.isReadable(path)) {
|
||||
throw new TraderException(
|
||||
TraderErrorCode.TRADER_DATA_SOURCE_MISSING,
|
||||
"replay source is not readable: " + source.path()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReplayClockTick> readTicks(ReplayRunConfig config) {
|
||||
DataSourceSpec source = selectReplaySource(config);
|
||||
Path path = Path.of(source.path());
|
||||
validateReadable(config);
|
||||
try (var lines = Files.lines(path)) {
|
||||
List<ReplayClockTick> ticks = lines
|
||||
.map(String::trim)
|
||||
.filter(line -> !line.isEmpty())
|
||||
.map(line -> parseLine(config, line))
|
||||
.sorted(Comparator.comparing(ReplayClockTick::eventTime))
|
||||
.toList();
|
||||
if (ticks.isEmpty()) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "replay source produced no ticks");
|
||||
}
|
||||
return ticks;
|
||||
} catch (IOException ex) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "failed to read replay source: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private ReplayClockTick parseLine(ReplayRunConfig config, String line) {
|
||||
try {
|
||||
ReplayTickFixture fixture = objectMapper.readValue(line, ReplayTickFixture.class);
|
||||
if (fixture.eventTime() == null) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "replay tick eventTime is required");
|
||||
}
|
||||
Instant eventTime = Instant.parse(fixture.eventTime());
|
||||
return new ReplayClockTick(
|
||||
config.runId(),
|
||||
config.symbol(),
|
||||
eventTime,
|
||||
fixture.contextFeatures(),
|
||||
fixture.setupFeatures(),
|
||||
fixture.triggerFeatures(),
|
||||
fixture.executionFeatures(),
|
||||
fixture.dataQuality()
|
||||
);
|
||||
} catch (IOException ex) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "invalid replay tick json: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private DataSourceSpec selectReplaySource(ReplayRunConfig config) {
|
||||
DataSourceSpec explicit = config.dataSources().get("ticks");
|
||||
if (explicit != null) {
|
||||
return explicit;
|
||||
}
|
||||
throw new TraderException(
|
||||
TraderErrorCode.TRADER_DATA_SOURCE_MISSING,
|
||||
"P0 replay requires dataSources.ticks"
|
||||
);
|
||||
}
|
||||
|
||||
public record ReplayTickFixture(
|
||||
String eventTime,
|
||||
Map<String, Object> contextFeatures,
|
||||
Map<String, Object> setupFeatures,
|
||||
Map<String, Object> triggerFeatures,
|
||||
Map<String, Object> executionFeatures,
|
||||
Map<String, Object> dataQuality
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record ReplayClockTick(
|
||||
String runId,
|
||||
String symbol,
|
||||
Instant eventTime,
|
||||
Map<String, Object> contextFeatures,
|
||||
Map<String, Object> setupFeatures,
|
||||
Map<String, Object> triggerFeatures,
|
||||
Map<String, Object> executionFeatures,
|
||||
Map<String, Object> dataQuality
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ReplayMarketEventReader {
|
||||
|
||||
void validateReadable(ReplayRunConfig config);
|
||||
|
||||
List<ReplayClockTick> readTicks(ReplayRunConfig config);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import com.quantai.trader.enums.ReplayRunStatus;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record ReplayRun(
|
||||
String runId,
|
||||
ReplayRunStatus status,
|
||||
ReplayRunConfig config,
|
||||
String playbookDefinitionHash,
|
||||
Instant createdAt,
|
||||
Instant startedAt,
|
||||
Instant finishedAt,
|
||||
String failureReason
|
||||
) {
|
||||
|
||||
public ReplayRun withStatus(ReplayRunStatus nextStatus) {
|
||||
Instant now = Instant.now();
|
||||
return new ReplayRun(
|
||||
runId,
|
||||
nextStatus,
|
||||
config,
|
||||
playbookDefinitionHash,
|
||||
createdAt,
|
||||
nextStatus == ReplayRunStatus.RUNNING ? now : startedAt,
|
||||
nextStatus == ReplayRunStatus.COMPLETED || nextStatus == ReplayRunStatus.CANCELLED || nextStatus == ReplayRunStatus.FAILED ? now : finishedAt,
|
||||
failureReason
|
||||
);
|
||||
}
|
||||
|
||||
public ReplayRun failed(String reason) {
|
||||
return new ReplayRun(
|
||||
runId,
|
||||
ReplayRunStatus.FAILED,
|
||||
config,
|
||||
playbookDefinitionHash,
|
||||
createdAt,
|
||||
startedAt,
|
||||
Instant.now(),
|
||||
reason
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record ReplayRunConfig(
|
||||
String runId,
|
||||
String symbol,
|
||||
String playbookId,
|
||||
String playbookVersion,
|
||||
Instant from,
|
||||
Instant to,
|
||||
String featureVersion,
|
||||
String labelVersion,
|
||||
@JsonProperty("dataSources")
|
||||
Map<String, DataSourceSpec> dataSources
|
||||
) {
|
||||
|
||||
public ReplayRunConfig withRunId(String nextRunId) {
|
||||
return new ReplayRunConfig(
|
||||
nextRunId,
|
||||
symbol,
|
||||
playbookId,
|
||||
playbookVersion,
|
||||
from,
|
||||
to,
|
||||
featureVersion,
|
||||
labelVersion,
|
||||
Map.copyOf(dataSources)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import com.quantai.trader.enums.ReplayRunStatus;
|
||||
|
||||
public record ReplayRunResponse(
|
||||
String runId,
|
||||
ReplayRunStatus status
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.quantai.trader.replay;
|
||||
|
||||
import com.quantai.trader.brain.TraderCycleResult;
|
||||
import com.quantai.trader.brain.TraderDecisionCycleRunner;
|
||||
import com.quantai.trader.domain.TraderDataSourceManifest;
|
||||
import com.quantai.trader.domain.TraderException;
|
||||
import com.quantai.trader.enums.ReplayRunStatus;
|
||||
import com.quantai.trader.enums.TraderErrorCode;
|
||||
import com.quantai.trader.enums.TraderRunMode;
|
||||
import com.quantai.trader.persistence.ReplayRunRepository;
|
||||
import com.quantai.trader.playbook.TraderPlaybookCatalog;
|
||||
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
|
||||
import com.quantai.trader.report.ReplayReportWriter;
|
||||
import com.quantai.trader.state.TraderRuntimeState;
|
||||
import com.quantai.trader.util.Ids;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Service
|
||||
public class ReplayRunService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ReplayRunService.class);
|
||||
private final TraderPlaybookCatalog catalog;
|
||||
private final ReplayRunRepository repository;
|
||||
private final ReplayReportWriter reportWriter;
|
||||
private final ReplayMarketEventReader eventReader;
|
||||
private final TraderDecisionCycleRunner cycleRunner;
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "trader-replay-worker");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
public ReplayRunService(
|
||||
TraderPlaybookCatalog catalog,
|
||||
ReplayRunRepository repository,
|
||||
ReplayReportWriter reportWriter,
|
||||
ReplayMarketEventReader eventReader,
|
||||
TraderDecisionCycleRunner cycleRunner
|
||||
) {
|
||||
this.catalog = catalog;
|
||||
this.repository = repository;
|
||||
this.reportWriter = reportWriter;
|
||||
this.eventReader = eventReader;
|
||||
this.cycleRunner = cycleRunner;
|
||||
}
|
||||
|
||||
public ReplayRunResponse createRun(ReplayRunConfig request) {
|
||||
validateRequest(request);
|
||||
TraderPlaybookDefinitionSnapshot playbook = catalog.require(request.playbookId(), request.playbookVersion());
|
||||
request.dataSources().forEach((sourceType, spec) -> validateDataSource(request, sourceType, spec));
|
||||
eventReader.validateReadable(request);
|
||||
|
||||
String runId = Ids.runId(Instant.now());
|
||||
ReplayRunConfig config = request.withRunId(runId);
|
||||
ReplayRun run = new ReplayRun(
|
||||
runId,
|
||||
ReplayRunStatus.CREATED,
|
||||
config,
|
||||
playbook.definitionHashSha256(),
|
||||
Instant.now(),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
repository.insert(run);
|
||||
log.info(
|
||||
"event=trader.replay.created runId={} symbol={} playbookId={} playbookVersion={} status={}",
|
||||
runId,
|
||||
config.symbol(),
|
||||
config.playbookId(),
|
||||
config.playbookVersion(),
|
||||
ReplayRunStatus.CREATED
|
||||
);
|
||||
executorService.submit(() -> execute(run, playbook));
|
||||
return new ReplayRunResponse(runId, ReplayRunStatus.CREATED);
|
||||
}
|
||||
|
||||
public Optional<ReplayRun> find(String runId) {
|
||||
return repository.findByRunId(runId);
|
||||
}
|
||||
|
||||
public ReplayRun cancel(String runId) {
|
||||
ReplayRun run = repository.findByRunId(runId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("replay run not found: " + runId));
|
||||
if (run.status() == ReplayRunStatus.COMPLETED
|
||||
|| run.status() == ReplayRunStatus.CANCELLED
|
||||
|| run.status() == ReplayRunStatus.FAILED) {
|
||||
return run;
|
||||
}
|
||||
ReplayRun cancelled = run.withStatus(ReplayRunStatus.CANCEL_REQUESTED);
|
||||
repository.update(cancelled);
|
||||
log.info("event=trader.replay.cancel_requested runId={} status={}", runId, cancelled.status());
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
private void execute(ReplayRun run, TraderPlaybookDefinitionSnapshot playbook) {
|
||||
try {
|
||||
repository.update(run.withStatus(ReplayRunStatus.RUNNING));
|
||||
log.info(
|
||||
"event=trader.replay.start runId={} symbol={} playbookId={} playbookVersion={} status={}",
|
||||
run.runId(),
|
||||
run.config().symbol(),
|
||||
playbook.playbookId(),
|
||||
playbook.playbookVersion(),
|
||||
ReplayRunStatus.RUNNING
|
||||
);
|
||||
List<ReplayClockTick> ticks = eventReader.readTicks(run.config());
|
||||
List<TraderCycleResult> results = new ArrayList<>(ticks.size());
|
||||
TraderRuntimeState runtimeState = new TraderRuntimeState(
|
||||
run.runId(),
|
||||
TraderRunMode.REPLAY,
|
||||
playbook.playbookId(),
|
||||
playbook.playbookVersion()
|
||||
);
|
||||
for (ReplayClockTick tick : ticks) {
|
||||
ReplayRun current = currentRun(run.runId());
|
||||
if (current.status() == ReplayRunStatus.CANCEL_REQUESTED) {
|
||||
repository.update(current.withStatus(ReplayRunStatus.CANCELLED));
|
||||
log.info("event=trader.replay.cancelled runId={} status={}", run.runId(), ReplayRunStatus.CANCELLED);
|
||||
return;
|
||||
}
|
||||
results.add(cycleRunner.runReplayTick(tick, runtimeState));
|
||||
}
|
||||
reportWriter.writeReport(run.config(), playbook, results);
|
||||
ReplayRun current = currentRun(run.runId());
|
||||
repository.update(current.withStatus(ReplayRunStatus.COMPLETED));
|
||||
log.info(
|
||||
"event=trader.replay.completed runId={} symbol={} playbookId={} playbookVersion={} status={} tickCount={} resultCount={}",
|
||||
run.runId(),
|
||||
run.config().symbol(),
|
||||
playbook.playbookId(),
|
||||
playbook.playbookVersion(),
|
||||
ReplayRunStatus.COMPLETED,
|
||||
ticks.size(),
|
||||
results.size()
|
||||
);
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn(
|
||||
"event=trader.replay.failed_detected runId={} symbol={} playbookId={} playbookVersion={} status={} reason={}",
|
||||
run.runId(),
|
||||
run.config().symbol(),
|
||||
playbook.playbookId(),
|
||||
playbook.playbookVersion(),
|
||||
ReplayRunStatus.FAILED,
|
||||
ex.getMessage(),
|
||||
ex
|
||||
);
|
||||
try {
|
||||
repository.update(run.failed(ex.toString()));
|
||||
} catch (RuntimeException updateFailure) {
|
||||
log.error(
|
||||
"event=trader.replay.failed_status_update_failed runId={} originalReason={} updateReason={}",
|
||||
run.runId(),
|
||||
ex.getMessage(),
|
||||
updateFailure.getMessage(),
|
||||
updateFailure
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRequest(ReplayRunConfig request) {
|
||||
if (request.symbol() == null || request.symbol().isBlank()
|
||||
|| request.playbookId() == null || request.playbookId().isBlank()
|
||||
|| request.playbookVersion() == null || request.playbookVersion().isBlank()
|
||||
|| request.from() == null
|
||||
|| request.to() == null
|
||||
|| request.dataSources() == null
|
||||
|| request.dataSources().isEmpty()) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "replay request is missing required lineage fields");
|
||||
}
|
||||
}
|
||||
|
||||
private ReplayRun currentRun(String runId) {
|
||||
return repository.findByRunId(runId)
|
||||
.orElseThrow(() -> new IllegalStateException("replay run disappeared: " + runId));
|
||||
}
|
||||
|
||||
private void validateDataSource(ReplayRunConfig request, String sourceType, DataSourceSpec spec) {
|
||||
if (spec.timezone() == null || spec.timezone().isBlank()) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source timezone is required: " + sourceType);
|
||||
}
|
||||
if (spec.missingSummary() == null) {
|
||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source missingSummary is required: " + sourceType);
|
||||
}
|
||||
new TraderDataSourceManifest(
|
||||
spec.sourceId(),
|
||||
request.symbol(),
|
||||
sourceType,
|
||||
"BINANCE",
|
||||
sourceType.equals("candles") ? "1m" : "event",
|
||||
spec.path(),
|
||||
spec.hashSha256(),
|
||||
spec.schemaHashSha256(),
|
||||
request.from(),
|
||||
request.to(),
|
||||
spec.minEventTime(),
|
||||
spec.maxEventTime(),
|
||||
spec.timezone(),
|
||||
spec.rowCount(),
|
||||
spec.missingSummary(),
|
||||
"P0_ACCEPTED"
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user