Load trader V4 artifacts from manifests

This commit is contained in:
Codex
2026-06-26 22:07:43 +08:00
parent 6bbedda97d
commit 4e5f49d6fe
11 changed files with 598 additions and 151 deletions
@@ -10,12 +10,14 @@ public record TraderArtifactBundle(
String pmConfigVersion,
String bundleHashSha256,
Set<String> providedModels,
TraderPmConfig pmConfig
TraderPmConfig pmConfig,
TraderArtifactModelPolicy modelPolicy
) {
public TraderArtifactBundle {
if (providedModels == null || !providedModels.containsAll(Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"))) {
throw new IllegalArgumentException("artifact bundle must provide all five V4 models");
}
pmConfig = java.util.Objects.requireNonNull(pmConfig, "pmConfig is required");
modelPolicy = java.util.Objects.requireNonNull(modelPolicy, "modelPolicy is required");
}
}
@@ -1,71 +1,186 @@
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.domain.TraderException;
import com.quantai.trader.domain.TraderPmConfig;
import com.quantai.trader.enums.TraderRunMode;
import com.quantai.trader.enums.TraderErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@Component
public class TraderArtifactLoader {
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 ObjectMapper objectMapper;
public TraderArtifactLoader(TraderProperties properties) {
public TraderArtifactLoader(TraderProperties properties, ObjectMapper objectMapper) {
this.properties = properties;
this.objectMapper = objectMapper;
}
public TraderArtifactBundle loadActiveBundle() {
TraderProperties.Artifact artifact = properties.artifact();
if (artifact.modelBundleVersion().isBlank()
|| artifact.calibrationBundleVersion().isBlank()
|| artifact.pmConfigVersion().isBlank()) {
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
"model/calibration/pm version is required");
}
TraderArtifactBundle bundle = deterministicP0Bundle(artifact);
Path root = Path.of(artifact.artifactRoot());
TraderModelBundleManifest modelManifest = readModelBundleManifest(root.resolve("manifests/model_bundle_manifest.json"));
TraderPmConfigManifest pmManifest = readPmConfigManifest(root.resolve("manifests/position_manager_manifest.json"));
TraderArtifactModelPolicy modelPolicy = readJson(root.resolve("model_output_policy.json"), TraderArtifactModelPolicy.class);
validateVersions(artifact, modelManifest, pmManifest);
validateModelManifest(modelManifest);
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={}",
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), bundle.providedModels());
return bundle;
}
private TraderArtifactBundle deterministicP0Bundle(TraderProperties.Artifact artifact) {
TraderPmConfig pmConfig = new TraderPmConfig(
artifact.pmConfigVersion(),
new TraderPmConfig.OpenRuleConfig(
new BigDecimal("0.58"), new BigDecimal("0.58"),
new BigDecimal("0.55"), new BigDecimal("0.55"),
new BigDecimal("0.45"), new BigDecimal("1.0"),
new BigDecimal("0.03"), new BigDecimal("0.10"), new BigDecimal("0.80")),
new TraderPmConfig.AddRuleConfig(
new BigDecimal("0.60"), new BigDecimal("0.60"),
new BigDecimal("0.58"), new BigDecimal("0.55"), new BigDecimal("0.45"),
new BigDecimal("0.45"), new BigDecimal("0.50"),
new BigDecimal("1.0"), BigDecimal.ZERO, new BigDecimal("0.10"),
new BigDecimal("500"), 3, 5),
new TraderPmConfig.ExitRuleConfig(
new BigDecimal("0.70"), new BigDecimal("0.70"), new BigDecimal("0.70"),
new BigDecimal("0.25"), new BigDecimal("0.62"),
new BigDecimal("0.35"), new BigDecimal("0.70"),
new BigDecimal("5.0"), new BigDecimal("80")),
new TraderPmConfig.SizingConfig(
new BigDecimal("0.80"), new BigDecimal("0.05"), BigDecimal.ONE,
new BigDecimal("0.02"), new BigDecimal("0.25"), BigDecimal.ONE,
new BigDecimal("1.0"), new BigDecimal("80"),
new BigDecimal("0.20"), new BigDecimal("0.50"), new BigDecimal("500"))
);
return new TraderArtifactBundle(
artifact.modelBundleVersion(),
artifact.calibrationBundleVersion(),
artifact.pmConfigVersion(),
"deterministic-p0-fixture",
Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"),
pmConfig);
private TraderModelBundleManifest readModelBundleManifest(Path path) {
JsonNode root = readJsonNode(path);
return new TraderModelBundleManifest(
requiredText(root, "model_bundle_version", path),
requiredText(root, "calibration_bundle_version", path),
requiredText(root, "feature_version", path),
requiredText(root, "label_version", path),
requiredText(root, "split_version", path),
textSet(root, "required_models_json", path),
textSet(root, "provided_models_json", path),
textSet(root, "missing_models_json", path),
requiredText(root, "bundle_hash_sha256", path),
root.path("complete").asBoolean(false),
requiredText(root, "status", path));
}
private TraderPmConfigManifest readPmConfigManifest(Path path) {
JsonNode root = readJsonNode(path);
return new TraderPmConfigManifest(
requiredText(root, "pm_config_version", path),
requiredText(root, "model_bundle_version", path),
requiredText(root, "calibration_bundle_version", path),
enumSet(root, "allowed_run_modes_json", TraderRunMode.class, path),
convert(root.path("config_json"), TraderPmConfig.class, path),
requiredText(root, "config_hash_sha256", path),
requiredText(root, "status", path));
}
private void validateVersions(TraderProperties.Artifact expected, TraderModelBundleManifest model, TraderPmConfigManifest pm) {
if (!expected.modelBundleVersion().equals(model.modelBundleVersion())
|| !expected.calibrationBundleVersion().equals(model.calibrationBundleVersion())
|| !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;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.domain.*;
import com.quantai.trader.enums.PositionSide;
@@ -9,6 +11,9 @@ import com.quantai.trader.enums.TraderRunMode;
import com.quantai.trader.risk.RiskLimits;
import java.math.BigDecimal;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -27,6 +32,26 @@ public final class TestFixtures {
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,
boolean tradingEnabled, boolean feedbackHttpEnabled) {
return new TraderProperties(
@@ -190,4 +215,145 @@ public final class TestFixtures {
public static RiskLimits riskLimits() {
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 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 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.assertThatThrownBy;
class TraderArtifactLoaderTest {
@TempDir
Path artifactRoot;
@Test
void deterministicP0BundleProvidesAllFiveModelFamilies() {
TraderArtifactBundle bundle = new TraderArtifactLoader(properties()).loadActiveBundle();
void activeArtifactBundleProvidesAllFiveModelFamilies() throws IOException {
writeArtifactBundle(artifactRoot);
TraderArtifactBundle bundle = new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()).loadActiveBundle();
assertThat(bundle.providedModels()).containsExactlyInAnyOrder("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK");
assertThat(bundle.pmConfig().pmConfigVersion()).isEqualTo("pm-v4-btc-p0");
assertThat(bundle.modelPolicy().entry().pricePlanConfigHash()).isEqualTo("p0-price-plan-hash");
}
@Test
@@ -26,13 +35,21 @@ class TraderArtifactLoaderTest {
}
@Test
void bundleContractRejectsMissingModelFamily() {
TraderArtifactBundle bundle = new TraderArtifactLoader(properties()).loadActiveBundle();
void bundleContractRejectsMissingModelFamily() throws IOException {
writeArtifactBundle(artifactRoot);
TraderArtifactBundle bundle = new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()).loadActiveBundle();
assertThatThrownBy(() -> new TraderArtifactBundle(
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)
.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.evidence.EvidenceAppender;
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.TraderOutboxRepository;
import com.quantai.trader.persistence.TraderDecisionTraceWriter;
import com.quantai.trader.position.TraderPositionManager;
import com.quantai.trader.risk.TraderRiskGate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.util.ArrayList;
import java.util.List;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Path;
import static com.quantai.trader.TestFixtures.T0;
import static com.quantai.trader.TestFixtures.properties;
import static com.quantai.trader.TestFixtures.*;
import static org.assertj.core.api.Assertions.assertThat;
class TraderP0CycleRunnerTest {
@TempDir
Path artifactRoot;
@Test
void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() {
void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() throws IOException {
writeArtifactBundle(artifactRoot);
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
TraderP0CycleRunner runner = new TraderP0CycleRunner(
properties(),
new TraderArtifactLoader(properties()),
new DeterministicTraderModelService(),
propertiesWithArtifactRoot(artifactRoot),
new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()),
new ArtifactTraderModelService(),
new TraderPositionManager(),
new TraderRiskGate(),
new TraderActionFactory(),
@@ -53,15 +59,16 @@ class TraderP0CycleRunnerTest {
}
@Test
void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() {
void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() throws IOException {
writeArtifactBundle(artifactRoot);
RecordingEvidenceRepository evidenceRepository = new RecordingEvidenceRepository();
EvidenceAppender evidenceAppender = new EvidenceAppender(evidenceRepository);
RecordingTraceWriter traceWriter = new RecordingTraceWriter();
RecordingOutboxRepository outboxRepository = new RecordingOutboxRepository();
TraderP0CycleRunner runner = new TraderP0CycleRunner(
properties(),
new TraderArtifactLoader(properties()),
new DeterministicTraderModelService(),
propertiesWithArtifactRoot(artifactRoot),
new TraderArtifactLoader(propertiesWithArtifactRoot(artifactRoot), objectMapper()),
new ArtifactTraderModelService(),
new TraderPositionManager(),
new TraderRiskGate(),
new TraderActionFactory(),