Load trader V4 artifacts from manifests
This commit is contained in:
@@ -1,65 +1,72 @@
|
|||||||
# quant-trader-service
|
# quant-trader-service
|
||||||
|
|
||||||
Clean P0 rebuild of the Trader-style strategy service.
|
Trader V4 P0 decision service.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This implementation follows the desktop design documents in
|
The service follows the V4 Trader documents under `/Users/zach/Desktop/app/trader`.
|
||||||
`/Users/zach/Desktop/app/trader`:
|
P0 only allows `REPLAY_SIM` and `SHADOW`; `PAPER`, `REAL`, real fills, and old
|
||||||
|
strategy-service contracts are rejected.
|
||||||
|
|
||||||
- `00-操盘手Trader新策略服务概要设计-20260622.md`
|
## Implemented Surface
|
||||||
- `03-Trader服务详细设计说明书-20260623.md`
|
|
||||||
|
|
||||||
P0 keeps the service in replay/shadow preparation mode:
|
- Spring Boot service under `com.quantai.trader`
|
||||||
|
- strict P0 runtime guard for run mode, execution mode, and trading switch
|
||||||
|
- JSON artifact loader for model bundle manifest, PM config manifest, and model output policy
|
||||||
|
- five-model output contract: Direction, Entry, Continue, Exit, Risk
|
||||||
|
- dynamic PM sizing with side-aware stop/target price calculation
|
||||||
|
- Risk Gate for hard blockers and forced close decisions
|
||||||
|
- JDBC persistence for run, cycle, model output, PM decision, risk decision, action, evidence, and outbox
|
||||||
|
- Flyway V1 schema for the V4 P0 tables
|
||||||
|
- health, replay-cycle, and feedback endpoints
|
||||||
|
- JaCoCo line coverage gate at 90%
|
||||||
|
|
||||||
- no real trading
|
## Local Database
|
||||||
- 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
|
Default configuration:
|
||||||
|
|
||||||
- Spring Boot P0 service under `com.quantai.trader`
|
```text
|
||||||
- Playbook YAML loading, validation, normalized JSON, SHA-256 definition hash
|
jdbc:mysql://127.0.0.1:3306/quant_trader
|
||||||
- MySQL 8 Flyway DDL for the P0 trader tables
|
username: quant_trader
|
||||||
- Core domain records for cycles, actions, entry plans, risk, evidence, feedback, samples, and reports
|
password: quant_trader
|
||||||
- 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
|
Override with `TRADER_DB_URL`, `TRADER_DB_USERNAME`, and `TRADER_DB_PASSWORD`.
|
||||||
- Evidence appender and proxy-only training sample exporter
|
|
||||||
- Async replay-run registry and report contract
|
## Artifact Root
|
||||||
- MyBatis-Plus repositories aligned with `quant-app-server`
|
|
||||||
- Deterministic JSONL replay fixtures for accepted, rejected, blocked, and hard-fail paths
|
Default artifact root:
|
||||||
- Feedback endpoint that returns `TRADER_FEEDBACK_DISABLED` unless explicitly enabled
|
|
||||||
- Focused unit and MVC tests
|
```text
|
||||||
|
/Users/zach/Desktop/quant-strategy-training-data/trader-v4/artifact_bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
Required files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
manifests/model_bundle_manifest.json
|
||||||
|
manifests/position_manager_manifest.json
|
||||||
|
model_output_policy.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader requires the configured model/calibration/PM version triple to match
|
||||||
|
the manifests, all five model families to be present, and the model/PM manifests
|
||||||
|
to be `ACTIVE`.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvn test
|
mvn clean test
|
||||||
mvn spring-boot:run
|
TRADER_DB_USERNAME=quant_trader TRADER_DB_PASSWORD=quant_trader SERVER_PORT=18080 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
|
## HTTP
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GET /api/trader/health
|
GET /api/trader/health
|
||||||
GET /api/trader/playbooks
|
POST /api/trader/replay/cycles
|
||||||
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
|
POST /api/trader/feedback
|
||||||
```
|
```
|
||||||
|
|
||||||
Remark: `/api/trader/feedback` is intentionally disabled in P0 unless
|
`/api/trader/feedback` is disabled by default in P0 and still rejects
|
||||||
`trader.integration.http-feedback-enabled=true`.
|
`PAPER_APP`, `REAL_APP`, and any real fill when explicitly enabled for tests.
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ public record TraderArtifactBundle(
|
|||||||
String pmConfigVersion,
|
String pmConfigVersion,
|
||||||
String bundleHashSha256,
|
String bundleHashSha256,
|
||||||
Set<String> providedModels,
|
Set<String> providedModels,
|
||||||
TraderPmConfig pmConfig
|
TraderPmConfig pmConfig,
|
||||||
|
TraderArtifactModelPolicy modelPolicy
|
||||||
) {
|
) {
|
||||||
public TraderArtifactBundle {
|
public TraderArtifactBundle {
|
||||||
if (providedModels == null || !providedModels.containsAll(Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"))) {
|
if (providedModels == null || !providedModels.containsAll(Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"))) {
|
||||||
throw new IllegalArgumentException("artifact bundle must provide all five V4 models");
|
throw new IllegalArgumentException("artifact bundle must provide all five V4 models");
|
||||||
}
|
}
|
||||||
pmConfig = java.util.Objects.requireNonNull(pmConfig, "pmConfig is required");
|
pmConfig = java.util.Objects.requireNonNull(pmConfig, "pmConfig is required");
|
||||||
|
modelPolicy = java.util.Objects.requireNonNull(modelPolicy, "modelPolicy is required");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,186 @@
|
|||||||
package com.quantai.trader.artifact;
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.quantai.trader.config.TraderProperties;
|
import com.quantai.trader.config.TraderProperties;
|
||||||
import com.quantai.trader.domain.TraderException;
|
import com.quantai.trader.domain.TraderException;
|
||||||
import com.quantai.trader.domain.TraderPmConfig;
|
import com.quantai.trader.domain.TraderPmConfig;
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
import com.quantai.trader.enums.TraderErrorCode;
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class TraderArtifactLoader {
|
public class TraderArtifactLoader {
|
||||||
private static final Logger log = LoggerFactory.getLogger(TraderArtifactLoader.class);
|
private static final Logger log = LoggerFactory.getLogger(TraderArtifactLoader.class);
|
||||||
|
private static final Set<String> REQUIRED_MODELS = Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK");
|
||||||
|
|
||||||
private final TraderProperties properties;
|
private final TraderProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public TraderArtifactLoader(TraderProperties properties) {
|
public TraderArtifactLoader(TraderProperties properties, ObjectMapper objectMapper) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TraderArtifactBundle loadActiveBundle() {
|
public TraderArtifactBundle loadActiveBundle() {
|
||||||
TraderProperties.Artifact artifact = properties.artifact();
|
TraderProperties.Artifact artifact = properties.artifact();
|
||||||
if (artifact.modelBundleVersion().isBlank()
|
Path root = Path.of(artifact.artifactRoot());
|
||||||
|| artifact.calibrationBundleVersion().isBlank()
|
TraderModelBundleManifest modelManifest = readModelBundleManifest(root.resolve("manifests/model_bundle_manifest.json"));
|
||||||
|| artifact.pmConfigVersion().isBlank()) {
|
TraderPmConfigManifest pmManifest = readPmConfigManifest(root.resolve("manifests/position_manager_manifest.json"));
|
||||||
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
TraderArtifactModelPolicy modelPolicy = readJson(root.resolve("model_output_policy.json"), TraderArtifactModelPolicy.class);
|
||||||
"model/calibration/pm version is required");
|
validateVersions(artifact, modelManifest, pmManifest);
|
||||||
}
|
validateModelManifest(modelManifest);
|
||||||
TraderArtifactBundle bundle = deterministicP0Bundle(artifact);
|
validatePmManifest(pmManifest, properties.runMode());
|
||||||
|
TraderArtifactBundle bundle = new TraderArtifactBundle(
|
||||||
|
modelManifest.modelBundleVersion(),
|
||||||
|
modelManifest.calibrationBundleVersion(),
|
||||||
|
pmManifest.pmConfigVersion(),
|
||||||
|
modelManifest.bundleHashSha256(),
|
||||||
|
modelManifest.providedModels(),
|
||||||
|
pmManifest.config(),
|
||||||
|
modelPolicy);
|
||||||
log.info("event=trader.artifact.loaded modelBundleVersion={} calibrationBundleVersion={} pmConfigVersion={} providedModels={}",
|
log.info("event=trader.artifact.loaded modelBundleVersion={} calibrationBundleVersion={} pmConfigVersion={} providedModels={}",
|
||||||
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), bundle.providedModels());
|
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), bundle.providedModels());
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TraderArtifactBundle deterministicP0Bundle(TraderProperties.Artifact artifact) {
|
private TraderModelBundleManifest readModelBundleManifest(Path path) {
|
||||||
TraderPmConfig pmConfig = new TraderPmConfig(
|
JsonNode root = readJsonNode(path);
|
||||||
artifact.pmConfigVersion(),
|
return new TraderModelBundleManifest(
|
||||||
new TraderPmConfig.OpenRuleConfig(
|
requiredText(root, "model_bundle_version", path),
|
||||||
new BigDecimal("0.58"), new BigDecimal("0.58"),
|
requiredText(root, "calibration_bundle_version", path),
|
||||||
new BigDecimal("0.55"), new BigDecimal("0.55"),
|
requiredText(root, "feature_version", path),
|
||||||
new BigDecimal("0.45"), new BigDecimal("1.0"),
|
requiredText(root, "label_version", path),
|
||||||
new BigDecimal("0.03"), new BigDecimal("0.10"), new BigDecimal("0.80")),
|
requiredText(root, "split_version", path),
|
||||||
new TraderPmConfig.AddRuleConfig(
|
textSet(root, "required_models_json", path),
|
||||||
new BigDecimal("0.60"), new BigDecimal("0.60"),
|
textSet(root, "provided_models_json", path),
|
||||||
new BigDecimal("0.58"), new BigDecimal("0.55"), new BigDecimal("0.45"),
|
textSet(root, "missing_models_json", path),
|
||||||
new BigDecimal("0.45"), new BigDecimal("0.50"),
|
requiredText(root, "bundle_hash_sha256", path),
|
||||||
new BigDecimal("1.0"), BigDecimal.ZERO, new BigDecimal("0.10"),
|
root.path("complete").asBoolean(false),
|
||||||
new BigDecimal("500"), 3, 5),
|
requiredText(root, "status", path));
|
||||||
new TraderPmConfig.ExitRuleConfig(
|
}
|
||||||
new BigDecimal("0.70"), new BigDecimal("0.70"), new BigDecimal("0.70"),
|
|
||||||
new BigDecimal("0.25"), new BigDecimal("0.62"),
|
private TraderPmConfigManifest readPmConfigManifest(Path path) {
|
||||||
new BigDecimal("0.35"), new BigDecimal("0.70"),
|
JsonNode root = readJsonNode(path);
|
||||||
new BigDecimal("5.0"), new BigDecimal("80")),
|
return new TraderPmConfigManifest(
|
||||||
new TraderPmConfig.SizingConfig(
|
requiredText(root, "pm_config_version", path),
|
||||||
new BigDecimal("0.80"), new BigDecimal("0.05"), BigDecimal.ONE,
|
requiredText(root, "model_bundle_version", path),
|
||||||
new BigDecimal("0.02"), new BigDecimal("0.25"), BigDecimal.ONE,
|
requiredText(root, "calibration_bundle_version", path),
|
||||||
new BigDecimal("1.0"), new BigDecimal("80"),
|
enumSet(root, "allowed_run_modes_json", TraderRunMode.class, path),
|
||||||
new BigDecimal("0.20"), new BigDecimal("0.50"), new BigDecimal("500"))
|
convert(root.path("config_json"), TraderPmConfig.class, path),
|
||||||
);
|
requiredText(root, "config_hash_sha256", path),
|
||||||
return new TraderArtifactBundle(
|
requiredText(root, "status", path));
|
||||||
artifact.modelBundleVersion(),
|
}
|
||||||
artifact.calibrationBundleVersion(),
|
|
||||||
artifact.pmConfigVersion(),
|
private void validateVersions(TraderProperties.Artifact expected, TraderModelBundleManifest model, TraderPmConfigManifest pm) {
|
||||||
"deterministic-p0-fixture",
|
if (!expected.modelBundleVersion().equals(model.modelBundleVersion())
|
||||||
Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"),
|
|| !expected.calibrationBundleVersion().equals(model.calibrationBundleVersion())
|
||||||
pmConfig);
|
|| !expected.pmConfigVersion().equals(pm.pmConfigVersion())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact version triple does not match configured model/calibration/pm versions");
|
||||||
|
}
|
||||||
|
if (!model.modelBundleVersion().equals(pm.modelBundleVersion())
|
||||||
|
|| !model.calibrationBundleVersion().equals(pm.calibrationBundleVersion())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_CALIBRATION_MISMATCH,
|
||||||
|
"model and pm manifests reference different model/calibration versions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateModelManifest(TraderModelBundleManifest manifest) {
|
||||||
|
if (!manifest.complete() || !"ACTIVE".equals(manifest.status())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model bundle manifest must be complete and ACTIVE");
|
||||||
|
}
|
||||||
|
if (!manifest.requiredModels().containsAll(REQUIRED_MODELS)
|
||||||
|
|| !manifest.providedModels().containsAll(REQUIRED_MODELS)
|
||||||
|
|| !manifest.missingModels().isEmpty()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model bundle must provide all five V4 models with no missing model");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePmManifest(TraderPmConfigManifest manifest, TraderRunMode runMode) {
|
||||||
|
if (!"ACTIVE".equals(manifest.status())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_PM_CONFIG_MISMATCH,
|
||||||
|
"pm config manifest must be ACTIVE");
|
||||||
|
}
|
||||||
|
if (!manifest.allowedRunModes().contains(runMode)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_PM_CONFIG_MISMATCH,
|
||||||
|
"pm config manifest does not allow current run mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode readJsonNode(Path path) {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact file is missing: " + path);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readTree(path.toFile());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact file cannot be read: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T readJson(Path path, Class<T> type) {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact file is missing: " + path);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(path.toFile(), type);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact file cannot be read: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T convert(JsonNode node, Class<T> type, Path path) {
|
||||||
|
if (node == null || node.isMissingNode() || node.isNull()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact field is missing: " + path + "#config_json");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.treeToValue(node, type);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact field cannot be parsed: " + path + "#config_json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requiredText(JsonNode node, String field, Path path) {
|
||||||
|
String value = node.path(field).asText("");
|
||||||
|
if (value.isBlank()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact field is required: " + path + "#" + field);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> textSet(JsonNode node, String field, Path path) {
|
||||||
|
JsonNode array = node.path(field);
|
||||||
|
if (!array.isArray()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact field must be array: " + path + "#" + field);
|
||||||
|
}
|
||||||
|
return StreamSupport.stream(array.spliterator(), false)
|
||||||
|
.map(JsonNode::asText)
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E extends Enum<E>> Set<E> enumSet(JsonNode node, String field, Class<E> type, Path path) {
|
||||||
|
return textSet(node, field, path).stream()
|
||||||
|
.map(value -> Enum.valueOf(type, value))
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record TraderArtifactModelPolicy(
|
||||||
|
DirectionPolicy direction,
|
||||||
|
EntryPolicy entry,
|
||||||
|
ContinuePolicy continuation,
|
||||||
|
ExitPolicy exit,
|
||||||
|
RiskPolicy risk,
|
||||||
|
BigDecimal uncertainty,
|
||||||
|
BigDecimal oodScore
|
||||||
|
) {
|
||||||
|
public record DirectionPolicy(
|
||||||
|
BigDecimal longProbWhenMarkGteIndex,
|
||||||
|
BigDecimal longProbWhenMarkLtIndex,
|
||||||
|
BigDecimal neutralProb,
|
||||||
|
BigDecimal expectedReturnBps,
|
||||||
|
int horizonMinutes,
|
||||||
|
String modelVersion
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EntryPolicy(
|
||||||
|
BigDecimal longEntryProb,
|
||||||
|
BigDecimal shortEntryProb,
|
||||||
|
BigDecimal entryQualityScore,
|
||||||
|
BigDecimal expectedEdgeBps,
|
||||||
|
String pricePlanId,
|
||||||
|
String pricePlanConfigHash,
|
||||||
|
BigDecimal stopDistanceBps,
|
||||||
|
BigDecimal targetDistanceBps,
|
||||||
|
int maxHoldMinutes,
|
||||||
|
BigDecimal costBps,
|
||||||
|
String modelVersion
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ContinuePolicy(
|
||||||
|
BigDecimal longContinueProb,
|
||||||
|
BigDecimal shortContinueProb,
|
||||||
|
BigDecimal trendPersistenceProb,
|
||||||
|
BigDecimal holdEdgeBps,
|
||||||
|
BigDecimal continueVsExitEdgeBps,
|
||||||
|
String modelVersion
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ExitPolicy(
|
||||||
|
BigDecimal longExitProb,
|
||||||
|
BigDecimal shortExitProb,
|
||||||
|
BigDecimal profitGivebackProb,
|
||||||
|
BigDecimal reversalProb,
|
||||||
|
BigDecimal stopRiskProb,
|
||||||
|
BigDecimal stagnationProb,
|
||||||
|
BigDecimal expectedGivebackBps,
|
||||||
|
String modelVersion
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RiskPolicy(
|
||||||
|
BigDecimal marketRiskProb,
|
||||||
|
BigDecimal positionRiskProb,
|
||||||
|
BigDecimal marketRiskSeverityBps,
|
||||||
|
BigDecimal positionRiskSeverityBps,
|
||||||
|
BigDecimal drawdownProb,
|
||||||
|
BigDecimal expectedShortfallBps,
|
||||||
|
BigDecimal volatilityExpansionProb,
|
||||||
|
BigDecimal spikeProb,
|
||||||
|
BigDecimal liquidityRiskProb,
|
||||||
|
BigDecimal liquidityCapacityRatioWhenReady,
|
||||||
|
BigDecimal liquidityCapacityRatioWhenNotReady,
|
||||||
|
String modelVersion
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TraderModelBundleManifest(
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String featureVersion,
|
||||||
|
String labelVersion,
|
||||||
|
String splitVersion,
|
||||||
|
Set<String> requiredModels,
|
||||||
|
Set<String> providedModels,
|
||||||
|
Set<String> missingModels,
|
||||||
|
String bundleHashSha256,
|
||||||
|
boolean complete,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.quantai.trader.domain.TraderPmConfig;
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TraderPmConfigManifest(
|
||||||
|
String pmConfigVersion,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
Set<TraderRunMode> allowedRunModes,
|
||||||
|
TraderPmConfig config,
|
||||||
|
String configHashSha256,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.quantai.trader.model;
|
||||||
|
|
||||||
|
import com.quantai.trader.artifact.TraderArtifactBundle;
|
||||||
|
import com.quantai.trader.artifact.TraderArtifactModelPolicy;
|
||||||
|
import com.quantai.trader.domain.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ArtifactTraderModelService implements TraderModelService {
|
||||||
|
@Override
|
||||||
|
public TraderModelOutput evaluate(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle) {
|
||||||
|
TraderArtifactModelPolicy policy = bundle.modelPolicy();
|
||||||
|
DirectionOutput direction = direction(snapshot, bundle, policy.direction());
|
||||||
|
return new TraderModelOutput(
|
||||||
|
"model_output_" + snapshot.cycleId(),
|
||||||
|
snapshot.runId(),
|
||||||
|
snapshot.cycleId(),
|
||||||
|
bundle.modelBundleVersion(),
|
||||||
|
bundle.calibrationBundleVersion(),
|
||||||
|
direction,
|
||||||
|
entry(bundle, policy.entry()),
|
||||||
|
continuation(bundle, policy.continuation()),
|
||||||
|
exit(bundle, policy.exit()),
|
||||||
|
risk(snapshot, bundle, policy.risk()),
|
||||||
|
policy.uncertainty(),
|
||||||
|
policy.oodScore(),
|
||||||
|
Map.of("artifactPolicy", bundle.bundleHashSha256()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DirectionOutput direction(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle, TraderArtifactModelPolicy.DirectionPolicy policy) {
|
||||||
|
BigDecimal longProb = snapshot.markPrice().compareTo(snapshot.indexPrice()) >= 0
|
||||||
|
? policy.longProbWhenMarkGteIndex()
|
||||||
|
: policy.longProbWhenMarkLtIndex();
|
||||||
|
BigDecimal shortProb = BigDecimal.ONE.subtract(longProb).subtract(policy.neutralProb()).max(BigDecimal.ZERO);
|
||||||
|
BigDecimal neutralProb = BigDecimal.ONE.subtract(longProb).subtract(shortProb);
|
||||||
|
return new DirectionOutput(longProb, shortProb, neutralProb, longProb.max(shortProb),
|
||||||
|
longProb.subtract(shortProb).abs(), policy.expectedReturnBps(), policy.horizonMinutes(),
|
||||||
|
policy.modelVersion(), bundle.calibrationBundleVersion(), Map.of("source", "artifact_policy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntryOutput entry(TraderArtifactBundle bundle, TraderArtifactModelPolicy.EntryPolicy policy) {
|
||||||
|
return new EntryOutput(policy.longEntryProb(), policy.shortEntryProb(), policy.entryQualityScore(),
|
||||||
|
policy.expectedEdgeBps(), policy.pricePlanId(), policy.pricePlanConfigHash(),
|
||||||
|
policy.stopDistanceBps(), policy.targetDistanceBps(), policy.maxHoldMinutes(), policy.costBps(),
|
||||||
|
policy.modelVersion(), bundle.calibrationBundleVersion(), Map.of("source", "artifact_policy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContinueOutput continuation(TraderArtifactBundle bundle, TraderArtifactModelPolicy.ContinuePolicy policy) {
|
||||||
|
return new ContinueOutput(policy.longContinueProb(), policy.shortContinueProb(), policy.trendPersistenceProb(),
|
||||||
|
policy.holdEdgeBps(), policy.continueVsExitEdgeBps(),
|
||||||
|
policy.modelVersion(), bundle.calibrationBundleVersion(), Map.of("source", "artifact_policy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExitOutput exit(TraderArtifactBundle bundle, TraderArtifactModelPolicy.ExitPolicy policy) {
|
||||||
|
return new ExitOutput(policy.longExitProb(), policy.shortExitProb(), policy.profitGivebackProb(),
|
||||||
|
policy.reversalProb(), policy.stopRiskProb(), policy.stagnationProb(), policy.expectedGivebackBps(),
|
||||||
|
policy.modelVersion(), bundle.calibrationBundleVersion(), Map.of("source", "artifact_policy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiskOutput risk(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle, TraderArtifactModelPolicy.RiskPolicy policy) {
|
||||||
|
BigDecimal liquidityCapacity = snapshot.dataReady()
|
||||||
|
? policy.liquidityCapacityRatioWhenReady()
|
||||||
|
: policy.liquidityCapacityRatioWhenNotReady();
|
||||||
|
return new RiskOutput(policy.marketRiskProb(), policy.positionRiskProb(), policy.marketRiskSeverityBps(),
|
||||||
|
policy.positionRiskSeverityBps(), policy.drawdownProb(), policy.expectedShortfallBps(),
|
||||||
|
policy.volatilityExpansionProb(), policy.spikeProb(), policy.liquidityRiskProb(), liquidityCapacity,
|
||||||
|
policy.modelVersion(), bundle.calibrationBundleVersion(), Map.of("source", "artifact_policy"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.quantai.trader.model;
|
|
||||||
|
|
||||||
import com.quantai.trader.artifact.TraderArtifactBundle;
|
|
||||||
import com.quantai.trader.domain.*;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class DeterministicTraderModelService implements TraderModelService {
|
|
||||||
@Override
|
|
||||||
public TraderModelOutput evaluate(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle) {
|
|
||||||
BigDecimal directionalBias = snapshot.markPrice().compareTo(snapshot.indexPrice()) >= 0
|
|
||||||
? new BigDecimal("0.62")
|
|
||||||
: new BigDecimal("0.32");
|
|
||||||
BigDecimal shortBias = BigDecimal.ONE.subtract(directionalBias).subtract(new BigDecimal("0.10"));
|
|
||||||
if (shortBias.compareTo(BigDecimal.ZERO) < 0) {
|
|
||||||
shortBias = BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
BigDecimal neutral = BigDecimal.ONE.subtract(directionalBias).subtract(shortBias);
|
|
||||||
EntryOutput entry = new EntryOutput(
|
|
||||||
new BigDecimal("0.63"), new BigDecimal("0.42"), new BigDecimal("0.64"),
|
|
||||||
new BigDecimal("12.0"), "p0-plan-atr-2r", "p0-price-plan-hash",
|
|
||||||
new BigDecimal("35"), new BigDecimal("70"), 45, new BigDecimal("4.0"),
|
|
||||||
"entry-p0", bundle.calibrationBundleVersion(), Map.of("source", "deterministic_fixture"));
|
|
||||||
return new TraderModelOutput(
|
|
||||||
"model_output_" + snapshot.cycleId(),
|
|
||||||
snapshot.runId(),
|
|
||||||
snapshot.cycleId(),
|
|
||||||
bundle.modelBundleVersion(),
|
|
||||||
bundle.calibrationBundleVersion(),
|
|
||||||
new DirectionOutput(directionalBias, shortBias, neutral, directionalBias, directionalBias.subtract(shortBias).abs(),
|
|
||||||
new BigDecimal("8.0"), 45, "direction-p0", bundle.calibrationBundleVersion(), Map.of()),
|
|
||||||
entry,
|
|
||||||
new ContinueOutput(new BigDecimal("0.61"), new BigDecimal("0.39"), new BigDecimal("0.58"),
|
|
||||||
new BigDecimal("5.0"), new BigDecimal("3.0"), "continue-p0", bundle.calibrationBundleVersion(), Map.of()),
|
|
||||||
new ExitOutput(new BigDecimal("0.24"), new BigDecimal("0.48"), new BigDecimal("0.20"),
|
|
||||||
new BigDecimal("0.25"), new BigDecimal("0.22"), new BigDecimal("0.20"),
|
|
||||||
new BigDecimal("10"), "exit-p0", bundle.calibrationBundleVersion(), Map.of()),
|
|
||||||
new RiskOutput(new BigDecimal("0.20"), new BigDecimal("0.18"),
|
|
||||||
new BigDecimal("20"), new BigDecimal("18"), new BigDecimal("0.15"),
|
|
||||||
new BigDecimal("20"), new BigDecimal("0.20"), new BigDecimal("0.10"),
|
|
||||||
new BigDecimal("0.12"), snapshot.depthNotional5Bps().compareTo(BigDecimal.ZERO) > 0 ? new BigDecimal("1.0") : BigDecimal.ZERO,
|
|
||||||
"risk-p0", bundle.calibrationBundleVersion(), Map.of()),
|
|
||||||
new BigDecimal("0.10"),
|
|
||||||
new BigDecimal("0.05"),
|
|
||||||
Map.of("source", "deterministic_p0_fixture"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.quantai.trader;
|
package com.quantai.trader;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
import com.quantai.trader.config.TraderProperties;
|
import com.quantai.trader.config.TraderProperties;
|
||||||
import com.quantai.trader.domain.*;
|
import com.quantai.trader.domain.*;
|
||||||
import com.quantai.trader.enums.PositionSide;
|
import com.quantai.trader.enums.PositionSide;
|
||||||
@@ -9,6 +11,9 @@ import com.quantai.trader.enums.TraderRunMode;
|
|||||||
import com.quantai.trader.risk.RiskLimits;
|
import com.quantai.trader.risk.RiskLimits;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -27,6 +32,26 @@ public final class TestFixtures {
|
|||||||
return properties(TraderRunMode.SHADOW, TraderExecutionMode.SHADOW, false, false);
|
return properties(TraderRunMode.SHADOW, TraderExecutionMode.SHADOW, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ObjectMapper objectMapper() {
|
||||||
|
return JsonMapper.builder().findAndAddModules().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TraderProperties propertiesWithArtifactRoot(Path artifactRoot) {
|
||||||
|
TraderProperties base = properties();
|
||||||
|
return new TraderProperties(
|
||||||
|
base.serviceName(),
|
||||||
|
base.runMode(),
|
||||||
|
base.symbol(),
|
||||||
|
new TraderProperties.Artifact("trader-v4-btc-p0", "cal-v4-btc-p0", "pm-v4-btc-p0", artifactRoot.toString()),
|
||||||
|
base.feedback(),
|
||||||
|
base.execution(),
|
||||||
|
base.runtime(),
|
||||||
|
base.outbox(),
|
||||||
|
base.release(),
|
||||||
|
base.risk(),
|
||||||
|
base.positionManager());
|
||||||
|
}
|
||||||
|
|
||||||
public static TraderProperties properties(TraderRunMode runMode, TraderExecutionMode executionMode,
|
public static TraderProperties properties(TraderRunMode runMode, TraderExecutionMode executionMode,
|
||||||
boolean tradingEnabled, boolean feedbackHttpEnabled) {
|
boolean tradingEnabled, boolean feedbackHttpEnabled) {
|
||||||
return new TraderProperties(
|
return new TraderProperties(
|
||||||
@@ -190,4 +215,145 @@ public final class TestFixtures {
|
|||||||
public static RiskLimits riskLimits() {
|
public static RiskLimits riskLimits() {
|
||||||
return new RiskLimits(bd("200"), BigDecimal.ONE, bd("500"), 3, 1500, false, false);
|
return new RiskLimits(bd("200"), BigDecimal.ONE, bd("500"), 3, 1500, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void writeArtifactBundle(Path artifactRoot) throws IOException {
|
||||||
|
Files.createDirectories(artifactRoot.resolve("manifests"));
|
||||||
|
Files.writeString(artifactRoot.resolve("manifests/model_bundle_manifest.json"), """
|
||||||
|
{
|
||||||
|
"model_bundle_version": "trader-v4-btc-p0",
|
||||||
|
"calibration_bundle_version": "cal-v4-btc-p0",
|
||||||
|
"feature_version": "feature-v4-p0",
|
||||||
|
"label_version": "label-v4-p0",
|
||||||
|
"split_version": "split-v4-p0",
|
||||||
|
"required_models_json": ["DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"],
|
||||||
|
"provided_models_json": ["DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"],
|
||||||
|
"missing_models_json": [],
|
||||||
|
"bundle_hash_sha256": "00000000000000000000000000000000000000000000000000000000000000a1",
|
||||||
|
"complete": true,
|
||||||
|
"status": "ACTIVE"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(artifactRoot.resolve("manifests/position_manager_manifest.json"), """
|
||||||
|
{
|
||||||
|
"pm_config_version": "pm-v4-btc-p0",
|
||||||
|
"model_bundle_version": "trader-v4-btc-p0",
|
||||||
|
"calibration_bundle_version": "cal-v4-btc-p0",
|
||||||
|
"allowed_run_modes_json": ["REPLAY_SIM", "SHADOW"],
|
||||||
|
"config_hash_sha256": "00000000000000000000000000000000000000000000000000000000000000b1",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"config_json": {
|
||||||
|
"pmConfigVersion": "pm-v4-btc-p0",
|
||||||
|
"open": {
|
||||||
|
"longOpenProb": 0.58,
|
||||||
|
"shortOpenProb": 0.58,
|
||||||
|
"minLongEntryProb": 0.55,
|
||||||
|
"minShortEntryProb": 0.55,
|
||||||
|
"maxMarketRiskProb": 0.45,
|
||||||
|
"minExpectedEdgeBps": 1.0,
|
||||||
|
"minDirectionMargin": 0.03,
|
||||||
|
"minLiquidityCapacityRatio": 0.10,
|
||||||
|
"maxOodScore": 0.80
|
||||||
|
},
|
||||||
|
"add": {
|
||||||
|
"minLongProb": 0.60,
|
||||||
|
"minShortProb": 0.60,
|
||||||
|
"minContinueProb": 0.58,
|
||||||
|
"minEntryProb": 0.55,
|
||||||
|
"maxExitProb": 0.45,
|
||||||
|
"maxMarketRiskProb": 0.45,
|
||||||
|
"maxPositionRiskProb": 0.50,
|
||||||
|
"minExpectedEdgeBps": 1.0,
|
||||||
|
"minContinueVsExitEdgeBps": 0,
|
||||||
|
"minLiquidityCapacityRatio": 0.10,
|
||||||
|
"minPostTradeLiquidationBufferBps": 500,
|
||||||
|
"maxAddCount": 3,
|
||||||
|
"cooldownMinutes": 5
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"closeExitProb": 0.70,
|
||||||
|
"closePositionRiskProb": 0.70,
|
||||||
|
"closeMarketRiskProb": 0.70,
|
||||||
|
"closeContinueMax": 0.25,
|
||||||
|
"reduceGivebackProb": 0.62,
|
||||||
|
"reduceContinueMin": 0.35,
|
||||||
|
"reduceContinueMax": 0.70,
|
||||||
|
"minProfitForReduceBps": 5.0,
|
||||||
|
"maxExpectedShortfallBps": 80
|
||||||
|
},
|
||||||
|
"sizing": {
|
||||||
|
"baseRatio": 0.80,
|
||||||
|
"minInitialRatio": 0.05,
|
||||||
|
"maxSingleLegRatio": 1.0,
|
||||||
|
"minAddRatio": 0.02,
|
||||||
|
"maxAddRatio": 0.25,
|
||||||
|
"maxTotalPositionRatio": 1.0,
|
||||||
|
"minEdgeBps": 1.0,
|
||||||
|
"maxLossPerTradeBps": 80,
|
||||||
|
"maxLiquidityUsageRatio": 0.20,
|
||||||
|
"uncertaintyPenaltyMultiplier": 0.50,
|
||||||
|
"minPostTradeLiquidationBufferBps": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(artifactRoot.resolve("model_output_policy.json"), """
|
||||||
|
{
|
||||||
|
"direction": {
|
||||||
|
"longProbWhenMarkGteIndex": 0.62,
|
||||||
|
"longProbWhenMarkLtIndex": 0.32,
|
||||||
|
"neutralProb": 0.10,
|
||||||
|
"expectedReturnBps": 8.0,
|
||||||
|
"horizonMinutes": 45,
|
||||||
|
"modelVersion": "direction-p0"
|
||||||
|
},
|
||||||
|
"entry": {
|
||||||
|
"longEntryProb": 0.63,
|
||||||
|
"shortEntryProb": 0.42,
|
||||||
|
"entryQualityScore": 0.64,
|
||||||
|
"expectedEdgeBps": 12.0,
|
||||||
|
"pricePlanId": "p0-plan-atr-2r",
|
||||||
|
"pricePlanConfigHash": "p0-price-plan-hash",
|
||||||
|
"stopDistanceBps": 35,
|
||||||
|
"targetDistanceBps": 70,
|
||||||
|
"maxHoldMinutes": 45,
|
||||||
|
"costBps": 4.0,
|
||||||
|
"modelVersion": "entry-p0"
|
||||||
|
},
|
||||||
|
"continuation": {
|
||||||
|
"longContinueProb": 0.61,
|
||||||
|
"shortContinueProb": 0.39,
|
||||||
|
"trendPersistenceProb": 0.58,
|
||||||
|
"holdEdgeBps": 5.0,
|
||||||
|
"continueVsExitEdgeBps": 3.0,
|
||||||
|
"modelVersion": "continue-p0"
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"longExitProb": 0.24,
|
||||||
|
"shortExitProb": 0.48,
|
||||||
|
"profitGivebackProb": 0.20,
|
||||||
|
"reversalProb": 0.25,
|
||||||
|
"stopRiskProb": 0.22,
|
||||||
|
"stagnationProb": 0.20,
|
||||||
|
"expectedGivebackBps": 10,
|
||||||
|
"modelVersion": "exit-p0"
|
||||||
|
},
|
||||||
|
"risk": {
|
||||||
|
"marketRiskProb": 0.20,
|
||||||
|
"positionRiskProb": 0.18,
|
||||||
|
"marketRiskSeverityBps": 20,
|
||||||
|
"positionRiskSeverityBps": 18,
|
||||||
|
"drawdownProb": 0.15,
|
||||||
|
"expectedShortfallBps": 20,
|
||||||
|
"volatilityExpansionProb": 0.20,
|
||||||
|
"spikeProb": 0.10,
|
||||||
|
"liquidityRiskProb": 0.12,
|
||||||
|
"liquidityCapacityRatioWhenReady": 1.0,
|
||||||
|
"liquidityCapacityRatioWhenNotReady": 0,
|
||||||
|
"modelVersion": "risk-p0"
|
||||||
|
},
|
||||||
|
"uncertainty": 0.10,
|
||||||
|
"oodScore": 0.05
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,29 @@ package com.quantai.trader.artifact;
|
|||||||
|
|
||||||
import com.quantai.trader.config.TraderProperties;
|
import com.quantai.trader.config.TraderProperties;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static com.quantai.trader.TestFixtures.properties;
|
import static com.quantai.trader.TestFixtures.*;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
class TraderArtifactLoaderTest {
|
class TraderArtifactLoaderTest {
|
||||||
|
@TempDir
|
||||||
|
Path artifactRoot;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void deterministicP0BundleProvidesAllFiveModelFamilies() {
|
void activeArtifactBundleProvidesAllFiveModelFamilies() throws IOException {
|
||||||
TraderArtifactBundle bundle = new TraderArtifactLoader(properties()).loadActiveBundle();
|
writeArtifactBundle(artifactRoot);
|
||||||
|
|
||||||
|
TraderArtifactBundle bundle = new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()).loadActiveBundle();
|
||||||
|
|
||||||
assertThat(bundle.providedModels()).containsExactlyInAnyOrder("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK");
|
assertThat(bundle.providedModels()).containsExactlyInAnyOrder("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK");
|
||||||
assertThat(bundle.pmConfig().pmConfigVersion()).isEqualTo("pm-v4-btc-p0");
|
assertThat(bundle.pmConfig().pmConfigVersion()).isEqualTo("pm-v4-btc-p0");
|
||||||
|
assertThat(bundle.modelPolicy().entry().pricePlanConfigHash()).isEqualTo("p0-price-plan-hash");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -26,13 +35,21 @@ class TraderArtifactLoaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bundleContractRejectsMissingModelFamily() {
|
void bundleContractRejectsMissingModelFamily() throws IOException {
|
||||||
TraderArtifactBundle bundle = new TraderArtifactLoader(properties()).loadActiveBundle();
|
writeArtifactBundle(artifactRoot);
|
||||||
|
TraderArtifactBundle bundle = new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()).loadActiveBundle();
|
||||||
|
|
||||||
assertThatThrownBy(() -> new TraderArtifactBundle(
|
assertThatThrownBy(() -> new TraderArtifactBundle(
|
||||||
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(),
|
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(),
|
||||||
bundle.bundleHashSha256(), Set.of("DIRECTION", "ENTRY", "CONTINUE", "RISK"), bundle.pmConfig()))
|
bundle.bundleHashSha256(), Set.of("DIRECTION", "ENTRY", "CONTINUE", "RISK"), bundle.pmConfig(), bundle.modelPolicy()))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessageContaining("all five");
|
.hasMessageContaining("all five");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsMissingArtifactFiles() {
|
||||||
|
assertThatThrownBy(() -> new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()).loadActiveBundle())
|
||||||
|
.isInstanceOf(com.quantai.trader.domain.TraderException.class)
|
||||||
|
.hasMessageContaining("artifact file is missing");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,39 @@ import com.quantai.trader.domain.*;
|
|||||||
import com.quantai.trader.enums.TraderActionType;
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
import com.quantai.trader.evidence.EvidenceAppender;
|
import com.quantai.trader.evidence.EvidenceAppender;
|
||||||
import com.quantai.trader.evidence.TraderEvidenceRepository;
|
import com.quantai.trader.evidence.TraderEvidenceRepository;
|
||||||
import com.quantai.trader.model.DeterministicTraderModelService;
|
import com.quantai.trader.model.ArtifactTraderModelService;
|
||||||
import com.quantai.trader.outbox.TraderOutboxEvent;
|
import com.quantai.trader.outbox.TraderOutboxEvent;
|
||||||
import com.quantai.trader.outbox.TraderOutboxRepository;
|
import com.quantai.trader.outbox.TraderOutboxRepository;
|
||||||
import com.quantai.trader.persistence.TraderDecisionTraceWriter;
|
import com.quantai.trader.persistence.TraderDecisionTraceWriter;
|
||||||
import com.quantai.trader.position.TraderPositionManager;
|
import com.quantai.trader.position.TraderPositionManager;
|
||||||
import com.quantai.trader.risk.TraderRiskGate;
|
import com.quantai.trader.risk.TraderRiskGate;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static com.quantai.trader.TestFixtures.T0;
|
import static com.quantai.trader.TestFixtures.*;
|
||||||
import static com.quantai.trader.TestFixtures.properties;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class TraderP0CycleRunnerTest {
|
class TraderP0CycleRunnerTest {
|
||||||
|
@TempDir
|
||||||
|
Path artifactRoot;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() {
|
void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() throws IOException {
|
||||||
|
writeArtifactBundle(artifactRoot);
|
||||||
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
|
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
|
||||||
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
|
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
|
||||||
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
|
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
|
||||||
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
|
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
|
||||||
TraderP0CycleRunner runner = new TraderP0CycleRunner(
|
TraderP0CycleRunner runner = new TraderP0CycleRunner(
|
||||||
properties(),
|
propertiesWithArtifactRoot(artifactRoot),
|
||||||
new TraderArtifactLoader(properties()),
|
new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()),
|
||||||
new DeterministicTraderModelService(),
|
new ArtifactTraderModelService(),
|
||||||
new TraderPositionManager(),
|
new TraderPositionManager(),
|
||||||
new TraderRiskGate(),
|
new TraderRiskGate(),
|
||||||
new TraderActionFactory(),
|
new TraderActionFactory(),
|
||||||
@@ -53,15 +59,16 @@ class TraderP0CycleRunnerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() {
|
void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() throws IOException {
|
||||||
|
writeArtifactBundle(artifactRoot);
|
||||||
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
|
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
|
||||||
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
|
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
|
||||||
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
|
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
|
||||||
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
|
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
|
||||||
TraderP0CycleRunner runner = new TraderP0CycleRunner(
|
TraderP0CycleRunner runner = new TraderP0CycleRunner(
|
||||||
properties(),
|
propertiesWithArtifactRoot(artifactRoot),
|
||||||
new TraderArtifactLoader(properties()),
|
new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()),
|
||||||
new DeterministicTraderModelService(),
|
new ArtifactTraderModelService(),
|
||||||
new TraderPositionManager(),
|
new TraderPositionManager(),
|
||||||
new TraderRiskGate(),
|
new TraderRiskGate(),
|
||||||
new TraderActionFactory(),
|
new TraderActionFactory(),
|
||||||
|
|||||||
Reference in New Issue
Block a user