Load trader V4 artifacts from manifests
This commit is contained in:
@@ -1,65 +1,72 @@
|
||||
# quant-trader-service
|
||||
|
||||
Clean P0 rebuild of the Trader-style strategy service.
|
||||
Trader V4 P0 decision service.
|
||||
|
||||
## Scope
|
||||
|
||||
This implementation follows the desktop design documents in
|
||||
`/Users/zach/Desktop/app/trader`:
|
||||
The service follows the V4 Trader documents under `/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`
|
||||
- `03-Trader服务详细设计说明书-20260623.md`
|
||||
## Implemented Surface
|
||||
|
||||
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
|
||||
- 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
|
||||
## Local Database
|
||||
|
||||
## Implemented P0 Surface
|
||||
Default configuration:
|
||||
|
||||
- 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
|
||||
```text
|
||||
jdbc:mysql://127.0.0.1:3306/quant_trader
|
||||
username: quant_trader
|
||||
password: quant_trader
|
||||
```
|
||||
|
||||
Override with `TRADER_DB_URL`, `TRADER_DB_USERNAME`, and `TRADER_DB_PASSWORD`.
|
||||
|
||||
## Artifact Root
|
||||
|
||||
Default artifact root:
|
||||
|
||||
```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
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
mvn spring-boot:run
|
||||
mvn clean test
|
||||
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
|
||||
|
||||
```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/replay/cycles
|
||||
POST /api/trader/feedback
|
||||
```
|
||||
|
||||
Remark: `/api/trader/feedback` is intentionally disabled in P0 unless
|
||||
`trader.integration.http-feedback-enabled=true`.
|
||||
`/api/trader/feedback` is disabled by default in P0 and still rejects
|
||||
`PAPER_APP`, `REAL_APP`, and any real fill when explicitly enabled for tests.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user