Compare commits
6 Commits
2fe4077164
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9acb3460a1 | |||
| e58e4a5572 | |||
| dad6b831b4 | |||
| 4e5f49d6fe | |||
| 6bbedda97d | |||
| 5d210053d0 |
@@ -3,6 +3,8 @@ target/
|
|||||||
*.iml
|
*.iml
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# Runtime and local data stay outside source control.
|
# Runtime and local data stay outside source control.
|
||||||
logs/
|
logs/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
<artifactId>quant-trader-service</artifactId>
|
<artifactId>quant-trader-service</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>quant-trader-service</name>
|
<name>quant-trader-service</name>
|
||||||
<description>Clean P0 rebuild of the Trader-style strategy service.</description>
|
<description>Trader V4 P0 decision service: REPLAY_SIM and SHADOW only.</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
<jacoco.version>0.8.13</jacoco.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -35,15 +35,6 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.baomidou</groupId>
|
|
||||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
|
||||||
<version>${mybatis-plus.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||||
@@ -53,14 +44,22 @@
|
|||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>commons-csv</artifactId>
|
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||||
<version>1.10.0</version>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-flyway</artifactId>
|
<artifactId>spring-boot-starter-flyway</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.microsoft.onnxruntime</groupId>
|
||||||
|
<artifactId>onnxruntime</artifactId>
|
||||||
|
<version>1.22.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-mysql</artifactId>
|
<artifactId>flyway-mysql</artifactId>
|
||||||
@@ -99,6 +98,46 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>${jacoco.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>test</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>check-line-coverage</id>
|
||||||
|
<phase>test</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.90</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.quantai.trader;
|
package com.quantai.trader;
|
||||||
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
|
||||||
@MapperScan("com.quantai.trader.infrastructure.mapper")
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@ConfigurationPropertiesScan
|
@ConfigurationPropertiesScan
|
||||||
public class QuantTraderServiceApplication {
|
public class QuantTraderServiceApplication {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
|
import com.quantai.trader.persistence.TraderJsonCodec;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class JdbcTraderArtifactManifestRepository implements TraderArtifactManifestRepository {
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final TraderJsonCodec jsonCodec;
|
||||||
|
|
||||||
|
public JdbcTraderArtifactManifestRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.jsonCodec = new TraderJsonCodec(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upsertActiveBundle(TraderArtifactBundle bundle) {
|
||||||
|
upsertModelBundle(bundle.modelBundleManifest());
|
||||||
|
bundle.modelManifests().forEach(this::upsertModelManifest);
|
||||||
|
bundle.calibrationManifests().forEach(this::upsertCalibrationManifest);
|
||||||
|
upsertPmConfigManifest(bundle.pmConfigManifest());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsertModelBundle(TraderModelBundleManifest manifest) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into trader_model_bundle_manifest
|
||||||
|
(manifest_schema_version, model_bundle_version, calibration_bundle_version,
|
||||||
|
feature_version, label_version, split_version, training_run_id, training_export_id,
|
||||||
|
backtest_manifest_id, required_models_json, provided_models_json, missing_models_json,
|
||||||
|
allowed_run_modes_json, bundle_hash_sha256, complete, status)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
on duplicate key update
|
||||||
|
manifest_schema_version = values(manifest_schema_version),
|
||||||
|
feature_version = values(feature_version),
|
||||||
|
label_version = values(label_version),
|
||||||
|
split_version = values(split_version),
|
||||||
|
training_run_id = values(training_run_id),
|
||||||
|
training_export_id = values(training_export_id),
|
||||||
|
backtest_manifest_id = values(backtest_manifest_id),
|
||||||
|
required_models_json = values(required_models_json),
|
||||||
|
provided_models_json = values(provided_models_json),
|
||||||
|
missing_models_json = values(missing_models_json),
|
||||||
|
allowed_run_modes_json = values(allowed_run_modes_json),
|
||||||
|
bundle_hash_sha256 = values(bundle_hash_sha256),
|
||||||
|
complete = values(complete),
|
||||||
|
status = values(status)
|
||||||
|
""",
|
||||||
|
manifest.manifestSchemaVersion(), manifest.modelBundleVersion(), manifest.calibrationBundleVersion(),
|
||||||
|
manifest.featureVersion(), manifest.labelVersion(), manifest.splitVersion(),
|
||||||
|
manifest.trainingRunId(), manifest.trainingExportId(), manifest.backtestManifestId(),
|
||||||
|
jsonCodec.toJson(manifest.requiredModels()),
|
||||||
|
jsonCodec.toJson(manifest.providedModels()), jsonCodec.toJson(manifest.missingModels()),
|
||||||
|
jsonCodec.toJson(manifest.allowedRunModes().stream().map(TraderRunMode::name).toList()),
|
||||||
|
manifest.bundleHashSha256(), manifest.complete(), manifest.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsertModelManifest(TraderModelManifest manifest) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into trader_model_manifest
|
||||||
|
(model_bundle_version, calibration_bundle_version, model_name, model_type, side,
|
||||||
|
symbol_scope_json, bar_interval, horizon_minutes, model_format, model_runtime,
|
||||||
|
model_runtime_version, onnx_opset_version, producer_name, producer_version,
|
||||||
|
artifact_path, artifact_hash_sha256, source_hash, feature_version, feature_schema_path,
|
||||||
|
feature_schema_hash, feature_order_path, feature_order_hash, input_tensor_name, input_dtype, input_shape_json,
|
||||||
|
input_example_path, output_schema_path, output_schema_hash, output_tensor_names_json,
|
||||||
|
output_mapping_json, output_value_rules_json, label_version, split_version, training_fold,
|
||||||
|
train_start, train_end, validation_start, validation_end, test_start, test_end,
|
||||||
|
metrics_json, status)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
on duplicate key update
|
||||||
|
model_type = values(model_type),
|
||||||
|
symbol_scope_json = values(symbol_scope_json),
|
||||||
|
bar_interval = values(bar_interval),
|
||||||
|
model_format = values(model_format),
|
||||||
|
model_runtime = values(model_runtime),
|
||||||
|
model_runtime_version = values(model_runtime_version),
|
||||||
|
onnx_opset_version = values(onnx_opset_version),
|
||||||
|
producer_name = values(producer_name),
|
||||||
|
producer_version = values(producer_version),
|
||||||
|
artifact_path = values(artifact_path),
|
||||||
|
artifact_hash_sha256 = values(artifact_hash_sha256),
|
||||||
|
source_hash = values(source_hash),
|
||||||
|
feature_version = values(feature_version),
|
||||||
|
feature_schema_path = values(feature_schema_path),
|
||||||
|
feature_schema_hash = values(feature_schema_hash),
|
||||||
|
feature_order_path = values(feature_order_path),
|
||||||
|
feature_order_hash = values(feature_order_hash),
|
||||||
|
input_tensor_name = values(input_tensor_name),
|
||||||
|
input_dtype = values(input_dtype),
|
||||||
|
input_shape_json = values(input_shape_json),
|
||||||
|
input_example_path = values(input_example_path),
|
||||||
|
output_schema_path = values(output_schema_path),
|
||||||
|
output_schema_hash = values(output_schema_hash),
|
||||||
|
output_tensor_names_json = values(output_tensor_names_json),
|
||||||
|
output_mapping_json = values(output_mapping_json),
|
||||||
|
output_value_rules_json = values(output_value_rules_json),
|
||||||
|
label_version = values(label_version),
|
||||||
|
split_version = values(split_version),
|
||||||
|
training_fold = values(training_fold),
|
||||||
|
train_start = values(train_start),
|
||||||
|
train_end = values(train_end),
|
||||||
|
validation_start = values(validation_start),
|
||||||
|
validation_end = values(validation_end),
|
||||||
|
test_start = values(test_start),
|
||||||
|
test_end = values(test_end),
|
||||||
|
metrics_json = values(metrics_json),
|
||||||
|
status = values(status)
|
||||||
|
""",
|
||||||
|
manifest.modelBundleVersion(), manifest.calibrationBundleVersion(), manifest.modelName(),
|
||||||
|
manifest.modelType(), manifest.side(), jsonCodec.toJson(manifest.symbolScope()),
|
||||||
|
manifest.barInterval(), manifest.horizonMinutes(), manifest.modelFormat(), manifest.modelRuntime(),
|
||||||
|
manifest.modelRuntimeVersion(), manifest.onnxOpsetVersion(), manifest.producerName(),
|
||||||
|
manifest.producerVersion(), manifest.artifactPath(), manifest.artifactHashSha256(),
|
||||||
|
manifest.sourceHash(), manifest.featureVersion(), manifest.featureSchemaPath(),
|
||||||
|
manifest.featureSchemaHash(), manifest.featureOrderPath(), manifest.featureOrderHash(), manifest.inputTensorName(),
|
||||||
|
manifest.inputDtype(), jsonCodec.toJson(manifest.inputShapeJson()), manifest.inputExamplePath(),
|
||||||
|
manifest.outputSchemaPath(), manifest.outputSchemaHash(), jsonCodec.toJson(manifest.outputTensorNames()),
|
||||||
|
jsonCodec.toJson(manifest.outputMapping()), jsonCodec.toJson(manifest.outputValueRules()),
|
||||||
|
manifest.labelVersion(), manifest.splitVersion(), manifest.trainingFold(),
|
||||||
|
java.sql.Timestamp.from(manifest.trainStart()), java.sql.Timestamp.from(manifest.trainEnd()),
|
||||||
|
java.sql.Timestamp.from(manifest.validationStart()), java.sql.Timestamp.from(manifest.validationEnd()),
|
||||||
|
java.sql.Timestamp.from(manifest.testStart()), java.sql.Timestamp.from(manifest.testEnd()),
|
||||||
|
jsonCodec.toJson(manifest.metricsJson()), manifest.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsertCalibrationManifest(TraderCalibrationManifest manifest) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into trader_calibration_manifest
|
||||||
|
(calibration_bundle_version, model_bundle_version, model_name, calibrator_version,
|
||||||
|
calibration_method, calibrator_path, calibrator_hash_sha256,
|
||||||
|
calibration_window_from, calibration_window_to, calibration_metrics_json,
|
||||||
|
bucket_metrics_json, output_after_calibration_schema_hash, status)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
on duplicate key update
|
||||||
|
calibrator_version = values(calibrator_version),
|
||||||
|
calibration_method = values(calibration_method),
|
||||||
|
calibrator_path = values(calibrator_path),
|
||||||
|
calibrator_hash_sha256 = values(calibrator_hash_sha256),
|
||||||
|
calibration_window_from = values(calibration_window_from),
|
||||||
|
calibration_window_to = values(calibration_window_to),
|
||||||
|
calibration_metrics_json = values(calibration_metrics_json),
|
||||||
|
bucket_metrics_json = values(bucket_metrics_json),
|
||||||
|
output_after_calibration_schema_hash = values(output_after_calibration_schema_hash),
|
||||||
|
status = values(status)
|
||||||
|
""",
|
||||||
|
manifest.calibrationBundleVersion(), manifest.modelBundleVersion(), manifest.modelName(),
|
||||||
|
manifest.calibratorVersion(), manifest.calibrationMethod(), manifest.calibratorPath(),
|
||||||
|
manifest.calibratorHashSha256(), java.sql.Timestamp.from(manifest.calibrationWindowFrom()),
|
||||||
|
java.sql.Timestamp.from(manifest.calibrationWindowTo()), jsonCodec.toJson(manifest.calibrationMetrics()),
|
||||||
|
jsonCodec.toJson(manifest.bucketMetricsJson()), manifest.outputAfterCalibrationSchemaHash(),
|
||||||
|
manifest.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsertPmConfigManifest(TraderPmConfigManifest manifest) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into trader_pm_config_manifest
|
||||||
|
(pm_config_version, model_bundle_version, calibration_bundle_version, threshold_stability_json,
|
||||||
|
allowed_run_modes_json, config_json, config_hash_sha256, status)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
on duplicate key update
|
||||||
|
model_bundle_version = values(model_bundle_version),
|
||||||
|
calibration_bundle_version = values(calibration_bundle_version),
|
||||||
|
threshold_stability_json = values(threshold_stability_json),
|
||||||
|
allowed_run_modes_json = values(allowed_run_modes_json),
|
||||||
|
config_json = values(config_json),
|
||||||
|
config_hash_sha256 = values(config_hash_sha256),
|
||||||
|
status = values(status)
|
||||||
|
""",
|
||||||
|
manifest.pmConfigVersion(), manifest.modelBundleVersion(), manifest.calibrationBundleVersion(),
|
||||||
|
jsonCodec.toJson(manifest.thresholdStabilityJson()),
|
||||||
|
jsonCodec.toJson(manifest.allowedRunModes().stream().map(TraderRunMode::name).toList()),
|
||||||
|
jsonCodec.toJson(manifest.config()), manifest.configHashSha256(), manifest.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.quantai.trader.domain.TraderException;
|
||||||
|
import com.quantai.trader.domain.TraderPmConfig;
|
||||||
|
import com.quantai.trader.domain.TraderPricePlanContext;
|
||||||
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TraderArtifactBundle(
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String pmConfigVersion,
|
||||||
|
String bundleHashSha256,
|
||||||
|
Set<String> providedModels,
|
||||||
|
TraderModelBundleManifest modelBundleManifest,
|
||||||
|
List<TraderModelManifest> modelManifests,
|
||||||
|
List<TraderCalibrationManifest> calibrationManifests,
|
||||||
|
TraderPmConfigManifest pmConfigManifest,
|
||||||
|
TraderPmConfig pmConfig,
|
||||||
|
TraderPricePlanContext pricePlanContext,
|
||||||
|
TraderReplayModelFixture replayModelFixture
|
||||||
|
) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
modelBundleManifest = java.util.Objects.requireNonNull(modelBundleManifest, "modelBundleManifest is required");
|
||||||
|
modelManifests = List.copyOf(modelManifests);
|
||||||
|
calibrationManifests = List.copyOf(calibrationManifests);
|
||||||
|
pmConfigManifest = java.util.Objects.requireNonNull(pmConfigManifest, "pmConfigManifest is required");
|
||||||
|
pmConfig = java.util.Objects.requireNonNull(pmConfig, "pmConfig is required");
|
||||||
|
pricePlanContext = java.util.Objects.requireNonNull(pricePlanContext, "pricePlanContext is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TraderReplayModelFixture requireReplayModelFixture() {
|
||||||
|
if (replayModelFixture == null) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"replay model fixture is required for replay fixture inference");
|
||||||
|
}
|
||||||
|
return replayModelFixture;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.quantai.trader.config.TraderProperties;
|
||||||
|
import com.quantai.trader.domain.TraderException;
|
||||||
|
import com.quantai.trader.domain.TraderPmConfig;
|
||||||
|
import com.quantai.trader.domain.TraderPricePlanContext;
|
||||||
|
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.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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 static final int REQUIRED_FEATURE_COUNT = 54;
|
||||||
|
private static final int REQUIRED_ONNX_OPSET_VERSION = 17;
|
||||||
|
private static final Map<String, Set<String>> REQUIRED_OUTPUT_MAPPING_KEYS = Map.of(
|
||||||
|
"DIRECTION", Set.of("long_prob", "short_prob", "neutral_prob"),
|
||||||
|
"ENTRY", Set.of("long_entry_prob", "short_entry_prob", "long_expected_net_edge_bps", "short_expected_net_edge_bps"),
|
||||||
|
"CONTINUE", Set.of("long_continue_prob", "short_continue_prob", "long_expected_continue_edge_bps", "short_expected_continue_edge_bps"),
|
||||||
|
"EXIT", Set.of("long_exit_prob", "short_exit_prob", "long_adverse_move_bps", "short_adverse_move_bps",
|
||||||
|
"adverse_move_prob", "reversal_prob", "stop_hit_prob", "stagnation_prob"),
|
||||||
|
"RISK", Set.of("market_risk_prob", "long_position_risk_prob", "short_position_risk_prob",
|
||||||
|
"market_path_risk_bps", "long_position_path_risk_bps", "short_position_path_risk_bps",
|
||||||
|
"market_drawdown_prob", "volatility_expansion_prob", "spike_prob",
|
||||||
|
"liquidity_deterioration_prob", "position_drawdown_prob")
|
||||||
|
);
|
||||||
|
private static final Set<String> REJECTED_OUTPUT_MAPPING_KEYS = Set.of(
|
||||||
|
"expected_net_edge_bps",
|
||||||
|
"expected_continue_edge_bps",
|
||||||
|
"expected_giveback_bps",
|
||||||
|
"market_expected_shortfall_bps",
|
||||||
|
"position_expected_shortfall_bps",
|
||||||
|
"position_risk_target"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final TraderProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public TraderArtifactLoader(TraderProperties properties, ObjectMapper objectMapper) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TraderArtifactBundle loadActiveBundle() {
|
||||||
|
TraderProperties.Artifact artifact = properties.artifact();
|
||||||
|
Path root = Path.of(artifact.artifactRoot());
|
||||||
|
TraderModelBundleManifest modelManifest = readModelBundleManifest(root.resolve("manifests/model_bundle_manifest.json"));
|
||||||
|
List<TraderModelManifest> modelManifests = readModelManifests(root.resolve("manifests/model_manifest.json"));
|
||||||
|
List<TraderCalibrationManifest> calibrationManifests = readCalibrationManifests(root.resolve("manifests/calibration_manifest.json"));
|
||||||
|
TraderPmConfigManifest pmManifest = readPmConfigManifest(root.resolve("manifests/position_manager_manifest.json"));
|
||||||
|
TraderPricePlanContext pricePlanContext = readJson(root.resolve("price_plan_context.json"), TraderPricePlanContext.class);
|
||||||
|
TraderReplayModelFixture replayModelFixture = readOptionalJson(root.resolve("replay_model_fixture.json"), TraderReplayModelFixture.class);
|
||||||
|
validateVersions(artifact, modelManifest, pmManifest);
|
||||||
|
validateModelManifest(modelManifest);
|
||||||
|
validateModelArtifacts(root, modelManifest, modelManifests);
|
||||||
|
validateCalibrationArtifacts(root, modelManifest, calibrationManifests);
|
||||||
|
validatePmManifest(pmManifest, properties.runMode());
|
||||||
|
TraderArtifactBundle bundle = new TraderArtifactBundle(
|
||||||
|
modelManifest.modelBundleVersion(),
|
||||||
|
modelManifest.calibrationBundleVersion(),
|
||||||
|
pmManifest.pmConfigVersion(),
|
||||||
|
modelManifest.bundleHashSha256(),
|
||||||
|
modelManifest.providedModels(),
|
||||||
|
modelManifest,
|
||||||
|
modelManifests,
|
||||||
|
calibrationManifests,
|
||||||
|
pmManifest,
|
||||||
|
pmManifest.config(),
|
||||||
|
pricePlanContext,
|
||||||
|
replayModelFixture);
|
||||||
|
log.info("event=trader.artifact.loaded modelBundleVersion={} calibrationBundleVersion={} pmConfigVersion={} providedModels={}",
|
||||||
|
bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), bundle.providedModels());
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TraderModelManifest> readModelManifests(Path path) {
|
||||||
|
JsonNode root = readJsonNode(path);
|
||||||
|
if (!root.isArray()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model manifest must be an array: " + path);
|
||||||
|
}
|
||||||
|
return StreamSupport.stream(root.spliterator(), false)
|
||||||
|
.map(node -> new TraderModelManifest(
|
||||||
|
requiredText(node, "model_bundle_version", path),
|
||||||
|
requiredText(node, "calibration_bundle_version", path),
|
||||||
|
requiredText(node, "model_name", path),
|
||||||
|
requiredText(node, "model_type", path),
|
||||||
|
requiredText(node, "side", path),
|
||||||
|
textSet(node, "symbol_scope_json", path),
|
||||||
|
requiredText(node, "bar_interval", path),
|
||||||
|
node.path("horizon_minutes").asInt(-1),
|
||||||
|
requiredText(node, "model_format", path),
|
||||||
|
requiredText(node, "model_runtime", path),
|
||||||
|
requiredText(node, "model_runtime_version", path),
|
||||||
|
node.path("onnx_opset_version").asInt(-1),
|
||||||
|
requiredText(node, "producer_name", path),
|
||||||
|
requiredText(node, "producer_version", path),
|
||||||
|
requiredText(node, "feature_version", path),
|
||||||
|
requiredText(node, "feature_schema_path", path),
|
||||||
|
requiredText(node, "feature_schema_hash", path),
|
||||||
|
requiredText(node, "feature_order_path", path),
|
||||||
|
requiredText(node, "feature_order_hash", path),
|
||||||
|
requiredText(node, "input_tensor_name", path),
|
||||||
|
requiredText(node, "input_dtype", path),
|
||||||
|
objectMap(node, "input_shape_json", path),
|
||||||
|
requiredText(node, "input_example_path", path),
|
||||||
|
requiredText(node, "output_schema_path", path),
|
||||||
|
requiredText(node, "output_schema_hash", path),
|
||||||
|
textSet(node, "output_tensor_names_json", path),
|
||||||
|
objectMap(node, "output_mapping_json", path),
|
||||||
|
objectMap(node, "output_value_rules_json", path),
|
||||||
|
requiredText(node, "label_version", path),
|
||||||
|
requiredText(node, "split_version", path),
|
||||||
|
requiredText(node, "training_fold", path),
|
||||||
|
instant(node, "train_start", path),
|
||||||
|
instant(node, "train_end", path),
|
||||||
|
instant(node, "validation_start", path),
|
||||||
|
instant(node, "validation_end", path),
|
||||||
|
instant(node, "test_start", path),
|
||||||
|
instant(node, "test_end", path),
|
||||||
|
objectMap(node, "metrics_json", path),
|
||||||
|
requiredText(node, "artifact_path", path),
|
||||||
|
requiredText(node, "artifact_hash_sha256", path),
|
||||||
|
requiredText(node, "source_hash", path),
|
||||||
|
requiredText(node, "status", path)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TraderCalibrationManifest> readCalibrationManifests(Path path) {
|
||||||
|
JsonNode root = readJsonNode(path);
|
||||||
|
if (!root.isArray()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"calibration manifest must be an array: " + path);
|
||||||
|
}
|
||||||
|
return StreamSupport.stream(root.spliterator(), false)
|
||||||
|
.map(node -> new TraderCalibrationManifest(
|
||||||
|
requiredText(node, "calibration_bundle_version", path),
|
||||||
|
requiredText(node, "model_bundle_version", path),
|
||||||
|
requiredText(node, "model_name", path),
|
||||||
|
requiredText(node, "calibrator_version", path),
|
||||||
|
requiredText(node, "calibration_method", path),
|
||||||
|
requiredText(node, "calibrator_path", path),
|
||||||
|
requiredText(node, "calibrator_hash_sha256", path),
|
||||||
|
instant(node, "calibration_window_from", path),
|
||||||
|
instant(node, "calibration_window_to", path),
|
||||||
|
objectMap(node, "calibration_metrics_json", path),
|
||||||
|
objectMap(node, "bucket_metrics_json", path),
|
||||||
|
requiredText(node, "output_after_calibration_schema_hash", path),
|
||||||
|
requiredText(node, "status", path)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraderModelBundleManifest readModelBundleManifest(Path path) {
|
||||||
|
JsonNode root = readJsonNode(path);
|
||||||
|
return new TraderModelBundleManifest(
|
||||||
|
requiredText(root, "manifest_schema_version", path),
|
||||||
|
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),
|
||||||
|
requiredText(root, "training_run_id", path),
|
||||||
|
requiredText(root, "training_export_id", path),
|
||||||
|
requiredText(root, "backtest_manifest_id", path),
|
||||||
|
textSet(root, "required_models_json", path),
|
||||||
|
textSet(root, "provided_models_json", path),
|
||||||
|
textSet(root, "missing_models_json", path),
|
||||||
|
enumSet(root, "allowed_run_modes_json", TraderRunMode.class, 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),
|
||||||
|
objectMap(root, "threshold_stability_json", 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");
|
||||||
|
}
|
||||||
|
if (!manifest.allowedRunModes().contains(properties.runMode())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model bundle does not allow current run mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateModelArtifacts(Path root, TraderModelBundleManifest bundleManifest,
|
||||||
|
List<TraderModelManifest> manifests) {
|
||||||
|
Set<String> modelNames = manifests.stream().map(TraderModelManifest::modelName).collect(Collectors.toUnmodifiableSet());
|
||||||
|
if (!modelNames.containsAll(REQUIRED_MODELS)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model_manifest.json must contain all five V4 model families");
|
||||||
|
}
|
||||||
|
for (TraderModelManifest manifest : manifests) {
|
||||||
|
if (!bundleManifest.modelBundleVersion().equals(manifest.modelBundleVersion())
|
||||||
|
|| !bundleManifest.calibrationBundleVersion().equals(manifest.calibrationBundleVersion())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_CALIBRATION_MISMATCH,
|
||||||
|
"model manifest version does not match bundle manifest");
|
||||||
|
}
|
||||||
|
if (!"ACTIVE".equals(manifest.status())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model manifest must be ACTIVE: " + manifest.modelName());
|
||||||
|
}
|
||||||
|
if (!REQUIRED_MODELS.contains(manifest.modelType())
|
||||||
|
|| !"ONNX".equals(manifest.modelFormat())
|
||||||
|
|| !"ONNX_RUNTIME_JAVA".equals(manifest.modelRuntime())
|
||||||
|
|| !"FLOAT32".equals(manifest.inputDtype())
|
||||||
|
|| manifest.horizonMinutes() <= 0
|
||||||
|
|| manifest.onnxOpsetVersion() != REQUIRED_ONNX_OPSET_VERSION
|
||||||
|
|| inputFeatureCount(manifest) != REQUIRED_FEATURE_COUNT
|
||||||
|
|| !"features".equals(manifest.inputTensorName())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model manifest runtime contract is invalid: " + manifest.modelName());
|
||||||
|
}
|
||||||
|
validateOutputMapping(manifest);
|
||||||
|
validateSha256(root.resolve(manifest.artifactPath()), manifest.artifactHashSha256());
|
||||||
|
validateSha256(root.resolve(manifest.featureSchemaPath()), manifest.featureSchemaHash());
|
||||||
|
validateSha256(root.resolve(manifest.featureOrderPath()), manifest.featureOrderHash());
|
||||||
|
validateFeatureOrder(root.resolve(manifest.featureOrderPath()));
|
||||||
|
validateSha256(root.resolve(manifest.outputSchemaPath()), manifest.outputSchemaHash());
|
||||||
|
if (!Files.isRegularFile(root.resolve(manifest.inputExamplePath()))) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model input example is missing: " + manifest.inputExamplePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOutputMapping(TraderModelManifest manifest) {
|
||||||
|
if (manifest.outputMapping().keySet().stream().anyMatch(REJECTED_OUTPUT_MAPPING_KEYS::contains)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model output mapping contains rejected legacy key: " + manifest.modelName());
|
||||||
|
}
|
||||||
|
Set<String> requiredKeys = REQUIRED_OUTPUT_MAPPING_KEYS.get(manifest.modelType());
|
||||||
|
if (requiredKeys == null || !manifest.outputMapping().keySet().containsAll(requiredKeys)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"model output mapping does not match V4 contract: " + manifest.modelName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCalibrationArtifacts(Path root, TraderModelBundleManifest modelManifest,
|
||||||
|
List<TraderCalibrationManifest> calibrationManifests) {
|
||||||
|
Set<String> calibratedModels = calibrationManifests.stream()
|
||||||
|
.map(TraderCalibrationManifest::modelName)
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
|
if (!calibratedModels.containsAll(REQUIRED_MODELS)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_CALIBRATION_MISMATCH,
|
||||||
|
"calibration_manifest.json must contain all five V4 model families");
|
||||||
|
}
|
||||||
|
for (TraderCalibrationManifest calibrationManifest : calibrationManifests) {
|
||||||
|
if (!modelManifest.modelBundleVersion().equals(calibrationManifest.modelBundleVersion())
|
||||||
|
|| !modelManifest.calibrationBundleVersion().equals(calibrationManifest.calibrationBundleVersion())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_CALIBRATION_MISMATCH,
|
||||||
|
"calibration manifest version does not match bundle manifest");
|
||||||
|
}
|
||||||
|
if (!"ACTIVE".equals(calibrationManifest.status())) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_CALIBRATION_MISMATCH,
|
||||||
|
"calibration manifest must be ACTIVE");
|
||||||
|
}
|
||||||
|
validateSha256(root.resolve(calibrationManifest.calibratorPath()), calibrationManifest.calibratorHashSha256());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 int inputFeatureCount(TraderModelManifest manifest) {
|
||||||
|
Object features = manifest.inputShapeJson().get("features");
|
||||||
|
if (features instanceof Number number) {
|
||||||
|
return number.intValue();
|
||||||
|
}
|
||||||
|
if (features instanceof String text) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(text);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFeatureOrder(Path path) {
|
||||||
|
JsonNode featureOrder = readJsonNode(path);
|
||||||
|
if (!featureOrder.isArray() || featureOrder.size() != REQUIRED_FEATURE_COUNT) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"feature_order.json must contain exactly " + REQUIRED_FEATURE_COUNT + " fields: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 void validateSha256(Path path, String expectedHash) {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact referenced by manifest is missing: " + path);
|
||||||
|
}
|
||||||
|
String actualHash;
|
||||||
|
try {
|
||||||
|
actualHash = sha256(path);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact referenced by manifest cannot be read: " + path);
|
||||||
|
}
|
||||||
|
if (!expectedHash.equals(actualHash)) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact sha256 mismatch: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sha256(Path path) throws IOException {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = digest.digest(Files.readAllBytes(path));
|
||||||
|
StringBuilder builder = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte value : hash) {
|
||||||
|
builder.append(String.format("%02x", value & 0xff));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
} catch (NoSuchAlgorithmException exception) {
|
||||||
|
throw new IllegalStateException("SHA-256 is not available", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 readOptionalJson(Path path, Class<T> type) {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return readJson(path, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Instant instant(JsonNode node, String field, Path path) {
|
||||||
|
return Instant.parse(requiredText(node, field, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> objectMap(JsonNode node, String field, Path path) {
|
||||||
|
JsonNode value = node.path(field);
|
||||||
|
if (value.isMissingNode() || value.isNull() || !value.isObject()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
|
"artifact object field is required: " + path + "#" + field);
|
||||||
|
}
|
||||||
|
return objectMapper.convertValue(value, new TypeReference<>() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,5 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
public interface TraderArtifactManifestRepository {
|
||||||
|
void upsertActiveBundle(TraderArtifactBundle bundle);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TraderCalibrationManifest(
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String modelName,
|
||||||
|
String calibratorVersion,
|
||||||
|
String calibrationMethod,
|
||||||
|
String calibratorPath,
|
||||||
|
String calibratorHashSha256,
|
||||||
|
Instant calibrationWindowFrom,
|
||||||
|
Instant calibrationWindowTo,
|
||||||
|
Map<String, Object> calibrationMetrics,
|
||||||
|
Map<String, Object> bucketMetricsJson,
|
||||||
|
String outputAfterCalibrationSchemaHash,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
public TraderCalibrationManifest {
|
||||||
|
calibrationMetrics = Map.copyOf(calibrationMetrics == null ? Map.of() : calibrationMetrics);
|
||||||
|
bucketMetricsJson = Map.copyOf(bucketMetricsJson == null ? Map.of() : bucketMetricsJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TraderModelBundleManifest(
|
||||||
|
String manifestSchemaVersion,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String featureVersion,
|
||||||
|
String labelVersion,
|
||||||
|
String splitVersion,
|
||||||
|
String trainingRunId,
|
||||||
|
String trainingExportId,
|
||||||
|
String backtestManifestId,
|
||||||
|
Set<String> requiredModels,
|
||||||
|
Set<String> providedModels,
|
||||||
|
Set<String> missingModels,
|
||||||
|
Set<TraderRunMode> allowedRunModes,
|
||||||
|
String bundleHashSha256,
|
||||||
|
boolean complete,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TraderModelManifest(
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String modelName,
|
||||||
|
String modelType,
|
||||||
|
String side,
|
||||||
|
Set<String> symbolScope,
|
||||||
|
String barInterval,
|
||||||
|
int horizonMinutes,
|
||||||
|
String modelFormat,
|
||||||
|
String modelRuntime,
|
||||||
|
String modelRuntimeVersion,
|
||||||
|
int onnxOpsetVersion,
|
||||||
|
String producerName,
|
||||||
|
String producerVersion,
|
||||||
|
String featureVersion,
|
||||||
|
String featureSchemaPath,
|
||||||
|
String featureSchemaHash,
|
||||||
|
String featureOrderPath,
|
||||||
|
String featureOrderHash,
|
||||||
|
String inputTensorName,
|
||||||
|
String inputDtype,
|
||||||
|
Map<String, Object> inputShapeJson,
|
||||||
|
String inputExamplePath,
|
||||||
|
String outputSchemaPath,
|
||||||
|
String outputSchemaHash,
|
||||||
|
Set<String> outputTensorNames,
|
||||||
|
Map<String, Object> outputMapping,
|
||||||
|
Map<String, Object> outputValueRules,
|
||||||
|
String labelVersion,
|
||||||
|
String splitVersion,
|
||||||
|
String trainingFold,
|
||||||
|
Instant trainStart,
|
||||||
|
Instant trainEnd,
|
||||||
|
Instant validationStart,
|
||||||
|
Instant validationEnd,
|
||||||
|
Instant testStart,
|
||||||
|
Instant testEnd,
|
||||||
|
Map<String, Object> metricsJson,
|
||||||
|
String artifactPath,
|
||||||
|
String artifactHashSha256,
|
||||||
|
String sourceHash,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
public TraderModelManifest {
|
||||||
|
symbolScope = Set.copyOf(symbolScope == null ? Set.of() : symbolScope);
|
||||||
|
inputShapeJson = Map.copyOf(inputShapeJson == null ? Map.of() : inputShapeJson);
|
||||||
|
outputTensorNames = Set.copyOf(outputTensorNames == null ? Set.of() : outputTensorNames);
|
||||||
|
outputMapping = Map.copyOf(outputMapping == null ? Map.of() : outputMapping);
|
||||||
|
outputValueRules = Map.copyOf(outputValueRules == null ? Map.of() : outputValueRules);
|
||||||
|
metricsJson = Map.copyOf(metricsJson == null ? Map.of() : metricsJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import com.quantai.trader.domain.TraderPmConfig;
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TraderPmConfigManifest(
|
||||||
|
String pmConfigVersion,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
Map<String, Object> thresholdStabilityJson,
|
||||||
|
Set<TraderRunMode> allowedRunModes,
|
||||||
|
TraderPmConfig config,
|
||||||
|
String configHashSha256,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
public TraderPmConfigManifest {
|
||||||
|
thresholdStabilityJson = Map.copyOf(thresholdStabilityJson == null ? Map.of() : thresholdStabilityJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.quantai.trader.artifact;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TraderReplayModelFixture(
|
||||||
|
DirectionFixture direction,
|
||||||
|
EntryFixture entry,
|
||||||
|
ContinueFixture continuation,
|
||||||
|
ExitFixture exit,
|
||||||
|
RiskFixture risk,
|
||||||
|
BigDecimal uncertainty,
|
||||||
|
BigDecimal oodScore,
|
||||||
|
String featureSchemaHash,
|
||||||
|
String featureOrderHash,
|
||||||
|
String outputSchemaHash
|
||||||
|
) {
|
||||||
|
public record DirectionFixture(
|
||||||
|
BigDecimal longProbWhenMarkGteIndex,
|
||||||
|
BigDecimal longProbWhenMarkLtIndex,
|
||||||
|
BigDecimal neutralProb
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EntryFixture(
|
||||||
|
BigDecimal longEntryProb,
|
||||||
|
BigDecimal shortEntryProb,
|
||||||
|
BigDecimal longExpectedNetEdgeBps,
|
||||||
|
BigDecimal shortExpectedNetEdgeBps
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ContinueFixture(
|
||||||
|
BigDecimal longContinueProb,
|
||||||
|
BigDecimal shortContinueProb,
|
||||||
|
BigDecimal longExpectedContinueEdgeBps,
|
||||||
|
BigDecimal shortExpectedContinueEdgeBps
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ExitFixture(
|
||||||
|
BigDecimal longExitProb,
|
||||||
|
BigDecimal shortExitProb,
|
||||||
|
BigDecimal longAdverseMoveBps,
|
||||||
|
BigDecimal shortAdverseMoveBps,
|
||||||
|
Map<String, BigDecimal> exitReasonScores
|
||||||
|
) {
|
||||||
|
public ExitFixture {
|
||||||
|
exitReasonScores = Map.copyOf(exitReasonScores == null ? Map.of() : exitReasonScores);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RiskFixture(
|
||||||
|
BigDecimal marketRiskProb,
|
||||||
|
BigDecimal longPositionRiskProb,
|
||||||
|
BigDecimal shortPositionRiskProb,
|
||||||
|
BigDecimal marketPathRiskBps,
|
||||||
|
BigDecimal longPositionPathRiskBps,
|
||||||
|
BigDecimal shortPositionPathRiskBps,
|
||||||
|
Map<String, BigDecimal> riskReasonScores
|
||||||
|
) {
|
||||||
|
public RiskFixture {
|
||||||
|
riskReasonScores = Map.copyOf(riskReasonScores == null ? Map.of() : riskReasonScores);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.ManagementDecision;
|
|
||||||
import com.quantai.trader.domain.PlaybookCandidate;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
|
||||||
import com.quantai.trader.domain.TraderPositionPath;
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class LifecycleActionValueService {
|
|
||||||
|
|
||||||
public ManagementDecision decide(
|
|
||||||
TraderDecisionCycle cycle,
|
|
||||||
PlaybookCandidate candidate,
|
|
||||||
TraderPositionPath path,
|
|
||||||
TraderMarketSnapshot snapshot
|
|
||||||
) {
|
|
||||||
if (path != null && path.fullSize()) {
|
|
||||||
return new ManagementDecision(TraderActionType.CLOSE, "P0_PROXY_CLOSE_AFTER_FULL_PATH", Map.of("proxyOnly", true));
|
|
||||||
}
|
|
||||||
return new ManagementDecision(TraderActionType.HOLD, "P0_PROXY_HOLD", Map.of("proxyOnly", true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.PlaybookCandidate;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderException;
|
|
||||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
|
||||||
import com.quantai.trader.domain.TraderPricePlan;
|
|
||||||
import com.quantai.trader.enums.TraderErrorCode;
|
|
||||||
import com.quantai.trader.enums.TraderSide;
|
|
||||||
import com.quantai.trader.playbook.TraderPlaybookCatalog;
|
|
||||||
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
|
|
||||||
import com.quantai.trader.util.Ids;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class PlaybookCandidateEngine {
|
|
||||||
|
|
||||||
private final TraderPlaybookCatalog catalog;
|
|
||||||
|
|
||||||
public PlaybookCandidateEngine(TraderPlaybookCatalog catalog) {
|
|
||||||
this.catalog = catalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PlaybookCandidate> generate(TraderMarketSnapshot snapshot, TraderDecisionCycle cycle) {
|
|
||||||
if (!Boolean.TRUE.equals(snapshot.setupFeatures().get("setupPass"))) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
TraderPlaybookDefinitionSnapshot playbook = catalog.require(cycle.playbookId(), cycle.playbookVersion());
|
|
||||||
BigDecimal entry = requiredDecimal(snapshot.setupFeatures(), "entryPrice");
|
|
||||||
TraderPricePlan pricePlan = new TraderPricePlan(
|
|
||||||
entry,
|
|
||||||
requiredDecimal(snapshot.setupFeatures(), "invalidPrice"),
|
|
||||||
requiredDecimal(snapshot.setupFeatures(), "stopPrice"),
|
|
||||||
requiredDecimal(snapshot.setupFeatures(), "targetPrice"),
|
|
||||||
null,
|
|
||||||
300_000,
|
|
||||||
7_200_000
|
|
||||||
);
|
|
||||||
return List.of(new PlaybookCandidate(
|
|
||||||
cycle.runId(),
|
|
||||||
cycle.cycleId(),
|
|
||||||
Ids.candidateId(cycle, playbook.playbookId()),
|
|
||||||
playbook.playbookId(),
|
|
||||||
playbook.playbookVersion(),
|
|
||||||
requiredSide(snapshot.setupFeatures(), "side"),
|
|
||||||
playbook.variant(),
|
|
||||||
snapshot.snapshotTime(),
|
|
||||||
pricePlan,
|
|
||||||
playbook.definition().plannedEntryLegRule().maxPlannedEntryLegs(),
|
|
||||||
snapshot.setupFeatures()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private TraderSide requiredSide(Map<String, Object> map, String key) {
|
|
||||||
Object value = map.get(key);
|
|
||||||
if (value instanceof TraderSide side) {
|
|
||||||
return side;
|
|
||||||
}
|
|
||||||
if (value instanceof String text && !text.isBlank()) {
|
|
||||||
return TraderSide.valueOf(text.trim().toUpperCase());
|
|
||||||
}
|
|
||||||
throw new TraderException(
|
|
||||||
TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE,
|
|
||||||
"setup feature is required when setupPass=true: " + key
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal requiredDecimal(Map<String, Object> map, String key) {
|
|
||||||
Object value = map.get(key);
|
|
||||||
if (value instanceof Number number) {
|
|
||||||
return BigDecimal.valueOf(number.doubleValue());
|
|
||||||
}
|
|
||||||
if (value instanceof String text && !text.isBlank()) {
|
|
||||||
return new BigDecimal(text);
|
|
||||||
}
|
|
||||||
throw new TraderException(
|
|
||||||
TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE,
|
|
||||||
"setup feature is required when setupPass=true: " + key
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.StageDecision;
|
|
||||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class TraderContextGate {
|
|
||||||
|
|
||||||
public StageDecision evaluate(TraderMarketSnapshot snapshot) {
|
|
||||||
Object missing = snapshot.dataQuality().get("missing_features");
|
|
||||||
if (missing instanceof List<?> list && !list.isEmpty()) {
|
|
||||||
return new StageDecision(false, "DATA_MISSING", "TRADER_DATA_QUALITY_FAILED", Map.of(
|
|
||||||
"missingFeatures", list
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Object pass = snapshot.contextFeatures().get("contextPass");
|
|
||||||
if (Boolean.FALSE.equals(pass)) {
|
|
||||||
return StageDecision.block("CONTEXT_BLOCKED", "TRADER_RISK_BLOCKED");
|
|
||||||
}
|
|
||||||
return StageDecision.pass("CONTEXT_PASS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderAction;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderPositionPath;
|
|
||||||
import com.quantai.trader.domain.TraderTrainingSample;
|
|
||||||
|
|
||||||
public record TraderCycleResult(
|
|
||||||
TraderDecisionCycle cycle,
|
|
||||||
TraderAction action,
|
|
||||||
TraderPositionPath path,
|
|
||||||
TraderTrainingSample sample
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.ExecutionDecision;
|
|
||||||
import com.quantai.trader.domain.ManagementDecision;
|
|
||||||
import com.quantai.trader.domain.PlaybookCandidate;
|
|
||||||
import com.quantai.trader.domain.RiskDecision;
|
|
||||||
import com.quantai.trader.domain.StageDecision;
|
|
||||||
import com.quantai.trader.domain.TraderAction;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderEntryPlan;
|
|
||||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
|
||||||
import com.quantai.trader.domain.TraderPositionPath;
|
|
||||||
import com.quantai.trader.domain.TraderTrainingSample;
|
|
||||||
import com.quantai.trader.evidence.EvidenceAppender;
|
|
||||||
import com.quantai.trader.enums.TraderState;
|
|
||||||
import com.quantai.trader.execution.ExecutionQualityGate;
|
|
||||||
import com.quantai.trader.execution.TraderEntryPlanner;
|
|
||||||
import com.quantai.trader.market.SnapshotBuilder;
|
|
||||||
import com.quantai.trader.position.TraderPositionManager;
|
|
||||||
import com.quantai.trader.replay.ReplayClockTick;
|
|
||||||
import com.quantai.trader.risk.TraderRiskGate;
|
|
||||||
import com.quantai.trader.sample.TrainingSampleExporter;
|
|
||||||
import com.quantai.trader.state.TraderDecisionCycleFactory;
|
|
||||||
import com.quantai.trader.state.TraderRuntimeState;
|
|
||||||
import com.quantai.trader.state.TraderStateMachine;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class TraderDecisionCycleRunner {
|
|
||||||
|
|
||||||
private final SnapshotBuilder snapshotBuilder;
|
|
||||||
private final TraderContextGate contextGate;
|
|
||||||
private final PlaybookCandidateEngine playbookCandidateEngine;
|
|
||||||
private final TriggerMarkoutService triggerMarkoutService;
|
|
||||||
private final TraderEntryPlanner entryPlanner;
|
|
||||||
private final ExecutionQualityGate executionQualityGate;
|
|
||||||
private final TraderRiskGate riskGate;
|
|
||||||
private final TraderStateMachine stateMachine;
|
|
||||||
private final TraderPositionManager positionManager;
|
|
||||||
private final LifecycleActionValueService actionValueService;
|
|
||||||
private final EvidenceAppender evidenceAppender;
|
|
||||||
private final TrainingSampleExporter sampleExporter;
|
|
||||||
|
|
||||||
public TraderDecisionCycleRunner(
|
|
||||||
SnapshotBuilder snapshotBuilder,
|
|
||||||
TraderContextGate contextGate,
|
|
||||||
PlaybookCandidateEngine playbookCandidateEngine,
|
|
||||||
TriggerMarkoutService triggerMarkoutService,
|
|
||||||
TraderEntryPlanner entryPlanner,
|
|
||||||
ExecutionQualityGate executionQualityGate,
|
|
||||||
TraderRiskGate riskGate,
|
|
||||||
TraderStateMachine stateMachine,
|
|
||||||
TraderPositionManager positionManager,
|
|
||||||
LifecycleActionValueService actionValueService,
|
|
||||||
EvidenceAppender evidenceAppender,
|
|
||||||
TrainingSampleExporter sampleExporter
|
|
||||||
) {
|
|
||||||
this.snapshotBuilder = snapshotBuilder;
|
|
||||||
this.contextGate = contextGate;
|
|
||||||
this.playbookCandidateEngine = playbookCandidateEngine;
|
|
||||||
this.triggerMarkoutService = triggerMarkoutService;
|
|
||||||
this.entryPlanner = entryPlanner;
|
|
||||||
this.executionQualityGate = executionQualityGate;
|
|
||||||
this.riskGate = riskGate;
|
|
||||||
this.stateMachine = stateMachine;
|
|
||||||
this.positionManager = positionManager;
|
|
||||||
this.actionValueService = actionValueService;
|
|
||||||
this.evidenceAppender = evidenceAppender;
|
|
||||||
this.sampleExporter = sampleExporter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TraderCycleResult runReplayTick(ReplayClockTick tick, TraderRuntimeState runtimeState) {
|
|
||||||
TraderMarketSnapshot snapshot = snapshotBuilder.build(tick, runtimeState);
|
|
||||||
TraderDecisionCycle cycle = TraderDecisionCycleFactory.create(snapshot, runtimeState);
|
|
||||||
|
|
||||||
StageDecision context = contextGate.evaluate(snapshot);
|
|
||||||
evidenceAppender.append(cycle, "CONTEXT_GATE", context);
|
|
||||||
if (context.blocked()) {
|
|
||||||
TraderTrainingSample sample = sampleExporter.export(cycle.withState(TraderState.BLOCKED, "BLOCKED", context.blocker()), snapshot, null, null, null);
|
|
||||||
return new TraderCycleResult(cycle, null, null, sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PlaybookCandidate> candidates = playbookCandidateEngine.generate(snapshot, cycle);
|
|
||||||
if (candidates.isEmpty()) {
|
|
||||||
evidenceAppender.append(cycle, "PLAYBOOK_CANDIDATE", StageDecision.block("NO_PLAYBOOK_CANDIDATE", "NO_PLAYBOOK_CANDIDATE"));
|
|
||||||
TraderTrainingSample sample = sampleExporter.export(cycle, snapshot, null, null, null);
|
|
||||||
return new TraderCycleResult(cycle, null, null, sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaybookCandidate selected = candidates.getFirst();
|
|
||||||
var trigger = triggerMarkoutService.evaluate(snapshot, selected);
|
|
||||||
evidenceAppender.append(cycle, "TRIGGER_MARKOUT", new StageDecision(trigger.pass(), trigger.reason(), trigger.blocker(), trigger.details()));
|
|
||||||
if (trigger.blocked()) {
|
|
||||||
TraderTrainingSample sample = sampleExporter.export(cycle.withState(TraderState.TRIGGER_WAIT, "WAIT", trigger.blocker()), snapshot, selected, null, null);
|
|
||||||
return new TraderCycleResult(cycle, null, null, sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraderDecisionCycle entryCycle = cycle.withState(TraderState.ENTRY_PLANNED, "RUNNING", null);
|
|
||||||
TraderEntryPlan entryPlan = entryPlanner.planInitialEntry(entryCycle, selected, trigger);
|
|
||||||
ExecutionDecision execution = executionQualityGate.evaluate(snapshot, entryPlan);
|
|
||||||
evidenceAppender.append(entryCycle, "EXECUTION_QUALITY", new StageDecision(execution.pass(), execution.reason(), execution.blocker(), execution.details()));
|
|
||||||
RiskDecision risk = riskGate.evaluate(entryCycle, entryPlan, execution);
|
|
||||||
evidenceAppender.append(entryCycle, "RISK_GATE", new StageDecision(risk.allowAction(), risk.allowAction() ? "RISK_PASS" : "RISK_BLOCKED", risk.blocker(), risk.details()));
|
|
||||||
if (execution.blocked() || risk.blocked()) {
|
|
||||||
TraderTrainingSample sample = sampleExporter.export(entryCycle.withState(TraderState.BLOCKED, "BLOCKED", risk.blocker()), snapshot, selected, null, null);
|
|
||||||
return new TraderCycleResult(entryCycle, null, null, sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraderAction action = stateMachine.toInitialEntryAction(entryCycle, selected, entryPlan);
|
|
||||||
TraderPositionPath path = positionManager.simulateOrUpdate(entryCycle, action, snapshot);
|
|
||||||
evidenceAppender.append(entryCycle, "OPEN_INITIAL", StageDecision.pass(action.reason()));
|
|
||||||
TraderLifecycleResult lifecycle = runPositionLifecycle(entryCycle, selected, action, path, snapshot);
|
|
||||||
TraderTrainingSample sample = sampleExporter.export(
|
|
||||||
lifecycle.finalCycle(),
|
|
||||||
snapshot,
|
|
||||||
selected,
|
|
||||||
lifecycle.lastAction(),
|
|
||||||
lifecycle.finalPath()
|
|
||||||
);
|
|
||||||
return new TraderCycleResult(lifecycle.finalCycle(), lifecycle.lastAction(), lifecycle.finalPath(), sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TraderLifecycleResult runPositionLifecycle(
|
|
||||||
TraderDecisionCycle initialCycle,
|
|
||||||
PlaybookCandidate candidate,
|
|
||||||
TraderAction initialAction,
|
|
||||||
TraderPositionPath initialPath,
|
|
||||||
TraderMarketSnapshot snapshot
|
|
||||||
) {
|
|
||||||
TraderDecisionCycle cycle = initialCycle;
|
|
||||||
TraderPositionPath path = initialPath;
|
|
||||||
TraderAction lastAction = initialAction;
|
|
||||||
|
|
||||||
if (!path.fullSize()) {
|
|
||||||
cycle = cycle.withState(TraderState.PLANNED_LEG_WAIT, "RUNNING", null);
|
|
||||||
Optional<TraderEntryPlan> plannedLeg = entryPlanner.planNextDeclaredLeg(cycle, candidate, path);
|
|
||||||
if (plannedLeg.isPresent()) {
|
|
||||||
ExecutionDecision execution = executionQualityGate.evaluate(snapshot, plannedLeg.get());
|
|
||||||
RiskDecision risk = riskGate.evaluate(cycle, plannedLeg.get(), execution);
|
|
||||||
evidenceAppender.append(cycle, "PLANNED_LEG_EXECUTION", new StageDecision(execution.pass(), execution.reason(), execution.blocker(), execution.details()));
|
|
||||||
evidenceAppender.append(cycle, "PLANNED_LEG_RISK", new StageDecision(risk.allowAction(), risk.allowAction() ? "RISK_PASS" : "RISK_BLOCKED", risk.blocker(), risk.details()));
|
|
||||||
if (execution.pass() && risk.allowAction()) {
|
|
||||||
TraderAction legAction = stateMachine.toPlannedLegAction(cycle, plannedLeg.get(), path);
|
|
||||||
path = positionManager.simulateOrUpdate(cycle, legAction, snapshot);
|
|
||||||
evidenceAppender.append(cycle, "OPEN_PLANNED_LEG", StageDecision.pass(legAction.reason()));
|
|
||||||
lastAction = legAction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cycle = cycle.withState(TraderState.MANAGING, "RUNNING", null);
|
|
||||||
ManagementDecision management = actionValueService.decide(cycle, candidate, path, snapshot);
|
|
||||||
evidenceAppender.append(cycle, "ACTION_VALUE", new StageDecision(true, management.reason(), null, management.details()));
|
|
||||||
TraderAction managementAction = stateMachine.toManagementAction(cycle, path, management.actionType());
|
|
||||||
riskGate.evaluateManagement(cycle, managementAction, path, management);
|
|
||||||
path = positionManager.simulateOrUpdate(cycle, managementAction, snapshot);
|
|
||||||
evidenceAppender.append(cycle, "MANAGEMENT_ACTION", StageDecision.pass(managementAction.reason()));
|
|
||||||
lastAction = managementAction;
|
|
||||||
|
|
||||||
return new TraderLifecycleResult(cycle.withState(TraderState.SAMPLE_EXPORTED, "COMPLETED", null), path, lastAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderAction;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderPositionPath;
|
|
||||||
|
|
||||||
public record TraderLifecycleResult(
|
|
||||||
TraderDecisionCycle finalCycle,
|
|
||||||
TraderPositionPath finalPath,
|
|
||||||
TraderAction lastAction
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.quantai.trader.brain;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.PlaybookCandidate;
|
|
||||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
|
||||||
import com.quantai.trader.domain.TriggerDecision;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class TriggerMarkoutService {
|
|
||||||
|
|
||||||
public TriggerDecision evaluate(TraderMarketSnapshot snapshot, PlaybookCandidate candidate) {
|
|
||||||
BigDecimal score = readScore(snapshot.triggerFeatures().get("triggerScore"));
|
|
||||||
if (score == null) {
|
|
||||||
return new TriggerDecision(false, BigDecimal.ZERO, "TRIGGER_SCORE_MISSING", "NO_TRIGGER_MARKOUT", Map.of());
|
|
||||||
}
|
|
||||||
if (score.compareTo(new BigDecimal("0.50")) < 0) {
|
|
||||||
return new TriggerDecision(false, score, "TRIGGER_WAIT", "NO_TRIGGER_MARKOUT", Map.of(
|
|
||||||
"triggerScore", score
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return new TriggerDecision(true, score, "TRIGGER_ACCEPTED", null, Map.of(
|
|
||||||
"triggerScore", score,
|
|
||||||
"proxyOnly", true
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal readScore(Object value) {
|
|
||||||
if (value instanceof Number number) {
|
|
||||||
return BigDecimal.valueOf(number.doubleValue());
|
|
||||||
}
|
|
||||||
if (value instanceof String text && !text.isBlank()) {
|
|
||||||
return new BigDecimal(text);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.quantai.trader.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
@Bean
|
||||||
|
ObjectMapper traderObjectMapper() {
|
||||||
|
return JsonMapper.builder().findAndAddModules().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,299 +1,108 @@
|
|||||||
package com.quantai.trader.config;
|
package com.quantai.trader.config;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.TraderExecutionMode;
|
||||||
import com.quantai.trader.enums.TraderRunMode;
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.nonNegative;
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.positive;
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "trader")
|
@ConfigurationProperties(prefix = "trader")
|
||||||
public class TraderProperties {
|
public record TraderProperties(
|
||||||
|
String serviceName,
|
||||||
private String serviceName = "quant-trader-service";
|
TraderRunMode runMode,
|
||||||
private TraderRunMode runMode = TraderRunMode.REPLAY;
|
String symbol,
|
||||||
private String symbol = "BTCUSDT";
|
Artifact artifact,
|
||||||
private String featureVersion = "trader_feature_v0";
|
Feedback feedback,
|
||||||
private String labelVersion = "trader_label_v0";
|
Execution execution,
|
||||||
private Playbook playbook = new Playbook();
|
Runtime runtime,
|
||||||
private Replay replay = new Replay();
|
Outbox outbox,
|
||||||
private Integration integration = new Integration();
|
Release release,
|
||||||
private Risk risk = new Risk();
|
Risk risk,
|
||||||
private Sizing sizing = new Sizing();
|
PositionManager positionManager
|
||||||
private DataSource dataSource = new DataSource();
|
) {
|
||||||
|
public TraderProperties {
|
||||||
public String getServiceName() {
|
serviceName = requiredText(serviceName, "serviceName");
|
||||||
return serviceName;
|
runMode = require(runMode, "runMode");
|
||||||
|
symbol = requiredText(symbol, "symbol");
|
||||||
|
artifact = require(artifact, "artifact");
|
||||||
|
feedback = require(feedback, "feedback");
|
||||||
|
execution = require(execution, "execution");
|
||||||
|
runtime = require(runtime, "runtime");
|
||||||
|
outbox = require(outbox, "outbox");
|
||||||
|
release = require(release, "release");
|
||||||
|
risk = require(risk, "risk");
|
||||||
|
positionManager = require(positionManager, "positionManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setServiceName(String serviceName) {
|
private static <T> T require(T value, String field) {
|
||||||
this.serviceName = serviceName;
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException(field + " is required");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TraderRunMode getRunMode() {
|
public record Artifact(
|
||||||
return runMode;
|
String modelBundleVersion,
|
||||||
}
|
String calibrationBundleVersion,
|
||||||
|
String pmConfigVersion,
|
||||||
public void setRunMode(TraderRunMode runMode) {
|
String artifactRoot
|
||||||
this.runMode = runMode;
|
) {
|
||||||
}
|
public Artifact {
|
||||||
|
modelBundleVersion = requiredText(modelBundleVersion, "artifact.modelBundleVersion");
|
||||||
public String getSymbol() {
|
calibrationBundleVersion = requiredText(calibrationBundleVersion, "artifact.calibrationBundleVersion");
|
||||||
return symbol;
|
pmConfigVersion = requiredText(pmConfigVersion, "artifact.pmConfigVersion");
|
||||||
}
|
artifactRoot = requiredText(artifactRoot, "artifact.artifactRoot");
|
||||||
|
|
||||||
public void setSymbol(String symbol) {
|
|
||||||
this.symbol = symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFeatureVersion() {
|
|
||||||
return featureVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFeatureVersion(String featureVersion) {
|
|
||||||
this.featureVersion = featureVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLabelVersion() {
|
|
||||||
return labelVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLabelVersion(String labelVersion) {
|
|
||||||
this.labelVersion = labelVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Playbook getPlaybook() {
|
|
||||||
return playbook;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPlaybook(Playbook playbook) {
|
|
||||||
this.playbook = playbook;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Replay getReplay() {
|
|
||||||
return replay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReplay(Replay replay) {
|
|
||||||
this.replay = replay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integration getIntegration() {
|
|
||||||
return integration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIntegration(Integration integration) {
|
|
||||||
this.integration = integration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Risk getRisk() {
|
|
||||||
return risk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRisk(Risk risk) {
|
|
||||||
this.risk = risk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Sizing getSizing() {
|
|
||||||
return sizing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSizing(Sizing sizing) {
|
|
||||||
this.sizing = sizing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DataSource getDataSource() {
|
|
||||||
return dataSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDataSource(DataSource dataSource) {
|
|
||||||
this.dataSource = dataSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Playbook {
|
|
||||||
private String locationPattern = "classpath:/playbooks/*.yml";
|
|
||||||
|
|
||||||
public String getLocationPattern() {
|
|
||||||
return locationPattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocationPattern(String locationPattern) {
|
|
||||||
this.locationPattern = locationPattern;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Replay {
|
public record Feedback(boolean httpEnabled) {
|
||||||
private String outputDir = "/Users/zach/Desktop/app/trader/replay-output";
|
|
||||||
private boolean failOnDataMissing = true;
|
|
||||||
|
|
||||||
public String getOutputDir() {
|
|
||||||
return outputDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOutputDir(String outputDir) {
|
public record Execution(TraderExecutionMode mode, Integer maxApiErrorCount, Long maxExchangeLatencyMs) {
|
||||||
this.outputDir = outputDir;
|
public Execution {
|
||||||
|
mode = require(mode, "execution.mode");
|
||||||
|
if (require(maxApiErrorCount, "execution.maxApiErrorCount") < 0) {
|
||||||
|
throw new IllegalArgumentException("execution.maxApiErrorCount must be >= 0");
|
||||||
}
|
}
|
||||||
|
if (require(maxExchangeLatencyMs, "execution.maxExchangeLatencyMs") <= 0) {
|
||||||
public boolean isFailOnDataMissing() {
|
throw new IllegalArgumentException("execution.maxExchangeLatencyMs must be > 0");
|
||||||
return failOnDataMissing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFailOnDataMissing(boolean failOnDataMissing) {
|
|
||||||
this.failOnDataMissing = failOnDataMissing;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Integration {
|
public record Runtime(String redisKeyPrefix, boolean requireRedisForOpenAdd, boolean tradingEnabled) {
|
||||||
private String appActionChannel = "JAR_FUTURE";
|
public Runtime {
|
||||||
private boolean httpFeedbackEnabled = false;
|
redisKeyPrefix = requiredText(redisKeyPrefix, "runtime.redisKeyPrefix");
|
||||||
|
|
||||||
public String getAppActionChannel() {
|
|
||||||
return appActionChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAppActionChannel(String appActionChannel) {
|
|
||||||
this.appActionChannel = appActionChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isHttpFeedbackEnabled() {
|
|
||||||
return httpFeedbackEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHttpFeedbackEnabled(boolean httpFeedbackEnabled) {
|
|
||||||
this.httpFeedbackEnabled = httpFeedbackEnabled;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Risk {
|
public record Outbox(boolean enabled, Integer maxRetryCount) {
|
||||||
private BigDecimal leverageScreen = BigDecimal.TEN;
|
public Outbox {
|
||||||
private boolean requireOneXNotNegative = true;
|
if (require(maxRetryCount, "outbox.maxRetryCount") < 0) {
|
||||||
private int maxPlannedEntryLegs = 3;
|
throw new IllegalArgumentException("outbox.maxRetryCount must be >= 0");
|
||||||
private boolean allowFreeScaleIn = false;
|
|
||||||
private boolean allowReduceThenAdd = false;
|
|
||||||
private boolean requireStop = true;
|
|
||||||
private boolean requireTarget = true;
|
|
||||||
private boolean requireInvalid = true;
|
|
||||||
|
|
||||||
public BigDecimal getLeverageScreen() {
|
|
||||||
return leverageScreen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLeverageScreen(BigDecimal leverageScreen) {
|
|
||||||
this.leverageScreen = leverageScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRequireOneXNotNegative() {
|
|
||||||
return requireOneXNotNegative;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequireOneXNotNegative(boolean requireOneXNotNegative) {
|
|
||||||
this.requireOneXNotNegative = requireOneXNotNegative;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxPlannedEntryLegs() {
|
|
||||||
return maxPlannedEntryLegs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxPlannedEntryLegs(int maxPlannedEntryLegs) {
|
|
||||||
this.maxPlannedEntryLegs = maxPlannedEntryLegs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAllowFreeScaleIn() {
|
|
||||||
return allowFreeScaleIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllowFreeScaleIn(boolean allowFreeScaleIn) {
|
|
||||||
this.allowFreeScaleIn = allowFreeScaleIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAllowReduceThenAdd() {
|
|
||||||
return allowReduceThenAdd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllowReduceThenAdd(boolean allowReduceThenAdd) {
|
|
||||||
this.allowReduceThenAdd = allowReduceThenAdd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRequireStop() {
|
|
||||||
return requireStop;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequireStop(boolean requireStop) {
|
|
||||||
this.requireStop = requireStop;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRequireTarget() {
|
|
||||||
return requireTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequireTarget(boolean requireTarget) {
|
|
||||||
this.requireTarget = requireTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRequireInvalid() {
|
|
||||||
return requireInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequireInvalid(boolean requireInvalid) {
|
|
||||||
this.requireInvalid = requireInvalid;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Sizing {
|
public record Release(boolean requireReviewForPaper, boolean requireReviewForLiveProbe, boolean activePointerCheckEnabled) {
|
||||||
private String method = "SIGNAL_EXECUTION_RISK_DYNAMIC";
|
|
||||||
private boolean allowFullInitialEntry = true;
|
|
||||||
private int maxPlannedEntryLegs = 3;
|
|
||||||
private BigDecimal maxTotalPositionRatio = BigDecimal.ONE;
|
|
||||||
private BigDecimal maxSingleLegRatio = BigDecimal.ONE;
|
|
||||||
|
|
||||||
public String getMethod() {
|
|
||||||
return method;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMethod(String method) {
|
public record Risk(BigDecimal maxDailyLossBps, BigDecimal maxTotalExposureRatio, BigDecimal minLiquidationBufferBps) {
|
||||||
this.method = method;
|
public Risk {
|
||||||
}
|
maxDailyLossBps = nonNegative(maxDailyLossBps, "risk.maxDailyLossBps");
|
||||||
|
maxTotalExposureRatio = positive(maxTotalExposureRatio, "risk.maxTotalExposureRatio");
|
||||||
public boolean isAllowFullInitialEntry() {
|
minLiquidationBufferBps = nonNegative(minLiquidationBufferBps, "risk.minLiquidationBufferBps");
|
||||||
return allowFullInitialEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllowFullInitialEntry(boolean allowFullInitialEntry) {
|
|
||||||
this.allowFullInitialEntry = allowFullInitialEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxPlannedEntryLegs() {
|
|
||||||
return maxPlannedEntryLegs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxPlannedEntryLegs(int maxPlannedEntryLegs) {
|
|
||||||
this.maxPlannedEntryLegs = maxPlannedEntryLegs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getMaxTotalPositionRatio() {
|
|
||||||
return maxTotalPositionRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxTotalPositionRatio(BigDecimal maxTotalPositionRatio) {
|
|
||||||
this.maxTotalPositionRatio = maxTotalPositionRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getMaxSingleLegRatio() {
|
|
||||||
return maxSingleLegRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxSingleLegRatio(BigDecimal maxSingleLegRatio) {
|
|
||||||
this.maxSingleLegRatio = maxSingleLegRatio;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DataSource {
|
public record PositionManager(BigDecimal maxSingleLegRatio, BigDecimal maxTotalPositionRatio) {
|
||||||
private String hashMode = "FULL_HASH_OR_SCHEMA_ROW_TIME_MISSING_SUMMARY";
|
public PositionManager {
|
||||||
|
maxSingleLegRatio = positive(maxSingleLegRatio, "positionManager.maxSingleLegRatio");
|
||||||
public String getHashMode() {
|
maxTotalPositionRatio = positive(maxTotalPositionRatio, "positionManager.maxTotalPositionRatio");
|
||||||
return hashMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHashMode(String hashMode) {
|
|
||||||
this.hashMode = hashMode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
public record TraderApiError(
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
String code,
|
|
||||||
String message
|
public record TraderApiError(TraderErrorCode code, String message) {
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderException;
|
import com.quantai.trader.domain.TraderException;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class TraderApiExceptionHandler {
|
public class TraderApiExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(TraderException.class)
|
@ExceptionHandler(TraderException.class)
|
||||||
public ResponseEntity<TraderApiError> handleTraderException(TraderException ex) {
|
ResponseEntity<TraderApiError> traderException(TraderException exception) {
|
||||||
return ResponseEntity.badRequest().body(new TraderApiError(ex.errorCode().name(), ex.getMessage()));
|
return ResponseEntity.badRequest().body(new TraderApiError(exception.code(), exception.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
public ResponseEntity<TraderApiError> handleIllegalArgument(IllegalArgumentException ex) {
|
ResponseEntity<TraderApiError> illegalArgument(IllegalArgumentException exception) {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new TraderApiError("TRADER_NOT_FOUND", ex.getMessage()));
|
return ResponseEntity.badRequest().body(new TraderApiError(com.quantai.trader.enums.TraderErrorCode.TRADER_REQUEST_INVALID, exception.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,42 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.config.TraderProperties;
|
import com.quantai.trader.config.TraderProperties;
|
||||||
|
import com.quantai.trader.domain.FeedbackValidator;
|
||||||
|
import com.quantai.trader.domain.TraderAppFeedback;
|
||||||
|
import com.quantai.trader.domain.TraderException;
|
||||||
import com.quantai.trader.enums.TraderErrorCode;
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
|
import com.quantai.trader.feedback.TraderFeedbackRepository;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trader/feedback")
|
|
||||||
public class TraderFeedbackController {
|
public class TraderFeedbackController {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class);
|
private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class);
|
||||||
private final TraderProperties properties;
|
private final TraderProperties properties;
|
||||||
|
private final FeedbackValidator feedbackValidator;
|
||||||
|
private final TraderFeedbackRepository feedbackRepository;
|
||||||
|
|
||||||
public TraderFeedbackController(TraderProperties properties) {
|
public TraderFeedbackController(TraderProperties properties, FeedbackValidator feedbackValidator,
|
||||||
|
TraderFeedbackRepository feedbackRepository) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.feedbackValidator = feedbackValidator;
|
||||||
|
this.feedbackRepository = feedbackRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping("/api/trader/feedback")
|
||||||
public ResponseEntity<?> feedback(@RequestBody TraderFeedbackRequest request) {
|
public Map<String, Object> feedback(@RequestBody TraderAppFeedback feedback) {
|
||||||
if (!properties.getIntegration().isHttpFeedbackEnabled()) {
|
if (!properties.feedback().httpEnabled()) {
|
||||||
log.info(
|
throw new TraderException(TraderErrorCode.TRADER_FEEDBACK_INVALID, "HTTP feedback is disabled in P0");
|
||||||
"event=trader.feedback.rejected runId={} cycleId={} actionId={} reason={}",
|
|
||||||
request.runId(),
|
|
||||||
request.cycleId(),
|
|
||||||
request.actionId(),
|
|
||||||
TraderErrorCode.TRADER_FEEDBACK_DISABLED
|
|
||||||
);
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new TraderApiError(
|
|
||||||
TraderErrorCode.TRADER_FEEDBACK_DISABLED.name(),
|
|
||||||
"P0 feedback endpoint is disabled; future App main channel is trader-core jar"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return ResponseEntity.accepted().body(Map.of(
|
feedbackValidator.validateP0(feedback);
|
||||||
"status", "ACCEPTED_CONTRACT_ONLY",
|
feedbackRepository.insert(feedback);
|
||||||
"runId", request.runId(),
|
log.info("event=trader.feedback.accepted runId={} cycleId={} actionId={} source={}",
|
||||||
"actionId", request.actionId()
|
feedback.runId(), feedback.cycleId(), feedback.actionId(), feedback.feedbackSource());
|
||||||
));
|
return Map.of("accepted", true, "feedbackId", feedback.feedbackId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.quantai.trader.controller;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderFeedbackSource;
|
|
||||||
import com.quantai.trader.enums.TraderFeedbackType;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderFeedbackRequest(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String actionId,
|
|
||||||
TraderFeedbackType feedbackType,
|
|
||||||
TraderFeedbackSource feedbackSource,
|
|
||||||
boolean realFill,
|
|
||||||
String proxyMethod,
|
|
||||||
String simulatorVersion,
|
|
||||||
String orderId,
|
|
||||||
String positionId,
|
|
||||||
BigDecimal filledPrice,
|
|
||||||
BigDecimal filledQuantity,
|
|
||||||
BigDecimal fee,
|
|
||||||
BigDecimal slippageBps,
|
|
||||||
Map<String, Object> rawFeedback
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,28 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.config.TraderProperties;
|
import com.quantai.trader.config.TraderProperties;
|
||||||
import com.quantai.trader.playbook.TraderPlaybookCatalog;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trader")
|
|
||||||
public class TraderHealthController {
|
public class TraderHealthController {
|
||||||
|
|
||||||
private final TraderProperties properties;
|
private final TraderProperties properties;
|
||||||
private final TraderPlaybookCatalog catalog;
|
|
||||||
|
|
||||||
public TraderHealthController(TraderProperties properties, TraderPlaybookCatalog catalog) {
|
public TraderHealthController(TraderProperties properties) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.catalog = catalog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/health")
|
@GetMapping("/api/trader/health")
|
||||||
public Map<String, Object> health() {
|
public Map<String, Object> health() {
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"service", properties.getServiceName(),
|
"status", "UP",
|
||||||
"runMode", properties.getRunMode(),
|
"runMode", properties.runMode(),
|
||||||
"symbol", properties.getSymbol(),
|
"executionMode", properties.execution().mode(),
|
||||||
"playbookCount", catalog.list().size(),
|
"modelBundleVersion", properties.artifact().modelBundleVersion(),
|
||||||
"httpFeedbackEnabled", properties.getIntegration().isHttpFeedbackEnabled()
|
"calibrationBundleVersion", properties.artifact().calibrationBundleVersion(),
|
||||||
);
|
"pmConfigVersion", properties.artifact().pmConfigVersion(),
|
||||||
|
"tradingEnabled", properties.runtime().tradingEnabled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.quantai.trader.controller;
|
|
||||||
|
|
||||||
import com.quantai.trader.playbook.TraderPlaybookCatalog;
|
|
||||||
import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/trader/playbooks")
|
|
||||||
public class TraderPlaybookController {
|
|
||||||
|
|
||||||
private final TraderPlaybookCatalog catalog;
|
|
||||||
|
|
||||||
public TraderPlaybookController(TraderPlaybookCatalog catalog) {
|
|
||||||
this.catalog = catalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public List<PlaybookResponse> list() {
|
|
||||||
return catalog.list().stream().map(PlaybookResponse::from).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{playbookId}")
|
|
||||||
public PlaybookResponse get(@PathVariable String playbookId) {
|
|
||||||
return PlaybookResponse.from(catalog.require(playbookId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public record PlaybookResponse(
|
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
String family,
|
|
||||||
String variant,
|
|
||||||
String definitionHashSha256,
|
|
||||||
List<String> outputActions
|
|
||||||
) {
|
|
||||||
|
|
||||||
static PlaybookResponse from(TraderPlaybookDefinitionSnapshot snapshot) {
|
|
||||||
return new PlaybookResponse(
|
|
||||||
snapshot.playbookId(),
|
|
||||||
snapshot.playbookVersion(),
|
|
||||||
snapshot.family(),
|
|
||||||
snapshot.variant(),
|
|
||||||
snapshot.definitionHashSha256(),
|
|
||||||
snapshot.definition().outputActions()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +1,22 @@
|
|||||||
package com.quantai.trader.controller;
|
package com.quantai.trader.controller;
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderReplayReport;
|
import com.quantai.trader.replay.ReplayMarketEvent;
|
||||||
import com.quantai.trader.persistence.ReplayReportRepository;
|
import com.quantai.trader.replay.TraderCycleResult;
|
||||||
import com.quantai.trader.replay.ReplayRun;
|
import com.quantai.trader.replay.TraderP0CycleRunner;
|
||||||
import com.quantai.trader.replay.ReplayRunConfig;
|
|
||||||
import com.quantai.trader.replay.ReplayRunResponse;
|
|
||||||
import com.quantai.trader.replay.ReplayRunService;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trader/replay/runs")
|
|
||||||
public class TraderReplayController {
|
public class TraderReplayController {
|
||||||
|
private final TraderP0CycleRunner runner;
|
||||||
|
|
||||||
private final ReplayRunService replayRunService;
|
public TraderReplayController(TraderP0CycleRunner runner) {
|
||||||
private final ReplayReportRepository reportRepository;
|
this.runner = runner;
|
||||||
|
|
||||||
public TraderReplayController(ReplayRunService replayRunService, ReplayReportRepository reportRepository) {
|
|
||||||
this.replayRunService = replayRunService;
|
|
||||||
this.reportRepository = reportRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping("/api/trader/replay/cycles")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
public TraderCycleResult runOneCycle(@RequestBody ReplayMarketEvent event) {
|
||||||
public ReplayRunResponse create(@RequestBody ReplayRunConfig config) {
|
return runner.runCycle(event);
|
||||||
return replayRunService.createRun(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{runId}")
|
|
||||||
public ReplayRun get(@PathVariable String runId) {
|
|
||||||
return replayRunService.find(runId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("replay run not found: " + runId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{runId}/cancel")
|
|
||||||
public ReplayRun cancel(@PathVariable String runId) {
|
|
||||||
return replayRunService.cancel(runId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{runId}/report")
|
|
||||||
public TraderReplayReport report(@PathVariable String runId) {
|
|
||||||
return reportRepository.findByRunId(runId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("replay report not found: " + runId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.quantai.trader.core;
|
||||||
|
|
||||||
|
import com.quantai.trader.domain.*;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TraderCoreDecisionRequest(
|
||||||
|
String requestId,
|
||||||
|
String runId,
|
||||||
|
String cycleId,
|
||||||
|
String symbol,
|
||||||
|
Instant eventTime,
|
||||||
|
TraderMarketSnapshot snapshot,
|
||||||
|
TraderPositionState positionState,
|
||||||
|
TraderAccountState accountState,
|
||||||
|
TraderExecutionState executionState,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String pmConfigVersion,
|
||||||
|
Map<String, Object> requestContextJson
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.quantai.trader.core;
|
||||||
|
|
||||||
|
import com.quantai.trader.domain.TraderAction;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TraderCoreDecisionResponse(
|
||||||
|
String responseId,
|
||||||
|
String requestId,
|
||||||
|
String cycleId,
|
||||||
|
String modelOutputId,
|
||||||
|
String pmDecisionId,
|
||||||
|
String riskDecisionId,
|
||||||
|
boolean actionAllowed,
|
||||||
|
TraderAction action,
|
||||||
|
String blocker,
|
||||||
|
Map<String, Object> responseContextJson
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.quantai.trader.core;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.FeedbackSource;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TraderCoreFeedbackEvent(
|
||||||
|
String feedbackId,
|
||||||
|
String actionId,
|
||||||
|
FeedbackSource feedbackSource,
|
||||||
|
boolean realFill,
|
||||||
|
String orderId,
|
||||||
|
String orderStatus,
|
||||||
|
BigDecimal filledPrice,
|
||||||
|
BigDecimal filledQuantity,
|
||||||
|
BigDecimal fee,
|
||||||
|
BigDecimal slippageBps,
|
||||||
|
String rejectReason,
|
||||||
|
Instant eventTime,
|
||||||
|
Map<String, Object> rawFeedbackJson
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.quantai.trader.core;
|
||||||
|
|
||||||
|
public record TraderCoreHealth(
|
||||||
|
boolean ready,
|
||||||
|
String runMode,
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
String pmConfigVersion,
|
||||||
|
String blocker
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
public record ContinueOutput(
|
||||||
|
BigDecimal longContinueProb,
|
||||||
|
BigDecimal shortContinueProb,
|
||||||
|
BigDecimal longExpectedContinueEdgeBps,
|
||||||
|
BigDecimal shortExpectedContinueEdgeBps
|
||||||
|
) {
|
||||||
|
public ContinueOutput {
|
||||||
|
longContinueProb = probability(longContinueProb, "continue.longContinueProb");
|
||||||
|
shortContinueProb = probability(shortContinueProb, "continue.shortContinueProb");
|
||||||
|
longExpectedContinueEdgeBps = required(longExpectedContinueEdgeBps, "continue.longExpectedContinueEdgeBps");
|
||||||
|
shortExpectedContinueEdgeBps = required(shortExpectedContinueEdgeBps, "continue.shortExpectedContinueEdgeBps");
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal continueEdgeBpsFor(PositionSide side) {
|
||||||
|
if (side == PositionSide.LONG) {
|
||||||
|
return longExpectedContinueEdgeBps;
|
||||||
|
}
|
||||||
|
if (side == PositionSide.SHORT) {
|
||||||
|
return shortExpectedContinueEdgeBps;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("continue edge requires LONG or SHORT side");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record DirectionOutput(
|
||||||
|
BigDecimal longProb,
|
||||||
|
BigDecimal shortProb,
|
||||||
|
BigDecimal neutralProb
|
||||||
|
) {
|
||||||
|
public DirectionOutput {
|
||||||
|
longProb = probability(longProb, "direction.longProb");
|
||||||
|
shortProb = probability(shortProb, "direction.shortProb");
|
||||||
|
neutralProb = probability(neutralProb, "direction.neutralProb");
|
||||||
|
BigDecimal sum = longProb.add(shortProb).add(neutralProb);
|
||||||
|
if (sum.subtract(BigDecimal.ONE).abs().compareTo(new BigDecimal("0.000001")) > 0) {
|
||||||
|
throw new IllegalArgumentException("direction probabilities must sum to 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal margin() {
|
||||||
|
return longProb.subtract(shortProb).abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal confidence() {
|
||||||
|
return longProb.max(shortProb).max(neutralProb);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
public record EntryOutput(
|
||||||
|
BigDecimal longEntryProb,
|
||||||
|
BigDecimal shortEntryProb,
|
||||||
|
BigDecimal longExpectedNetEdgeBps,
|
||||||
|
BigDecimal shortExpectedNetEdgeBps
|
||||||
|
) {
|
||||||
|
public EntryOutput {
|
||||||
|
longEntryProb = probability(longEntryProb, "entry.longEntryProb");
|
||||||
|
shortEntryProb = probability(shortEntryProb, "entry.shortEntryProb");
|
||||||
|
longExpectedNetEdgeBps = required(longExpectedNetEdgeBps, "entry.longExpectedNetEdgeBps");
|
||||||
|
shortExpectedNetEdgeBps = required(shortExpectedNetEdgeBps, "entry.shortExpectedNetEdgeBps");
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal netEdgeBpsFor(PositionSide side) {
|
||||||
|
if (side == PositionSide.LONG) {
|
||||||
|
return longExpectedNetEdgeBps;
|
||||||
|
}
|
||||||
|
if (side == PositionSide.SHORT) {
|
||||||
|
return shortExpectedNetEdgeBps;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("entry edge requires LONG or SHORT side");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record ExecutionDecision(
|
|
||||||
boolean pass,
|
|
||||||
BigDecimal executionQualityScore,
|
|
||||||
String reason,
|
|
||||||
String blocker,
|
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
|
||||||
|
|
||||||
public ExecutionDecision {
|
|
||||||
details = Maps.immutable(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean blocked() {
|
|
||||||
return !pass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record ExitOutput(
|
||||||
|
BigDecimal longExitProb,
|
||||||
|
BigDecimal shortExitProb,
|
||||||
|
BigDecimal longAdverseMoveBps,
|
||||||
|
BigDecimal shortAdverseMoveBps,
|
||||||
|
Map<String, BigDecimal> exitReasonScores
|
||||||
|
) {
|
||||||
|
private static final Set<String> REQUIRED_REASON_KEYS = Set.of(
|
||||||
|
"adverse_move_prob", "reversal_prob", "stop_hit_prob", "stagnation_prob");
|
||||||
|
|
||||||
|
public ExitOutput {
|
||||||
|
longExitProb = probability(longExitProb, "exit.longExitProb");
|
||||||
|
shortExitProb = probability(shortExitProb, "exit.shortExitProb");
|
||||||
|
longAdverseMoveBps = nonNegative(longAdverseMoveBps, "exit.longAdverseMoveBps");
|
||||||
|
shortAdverseMoveBps = nonNegative(shortAdverseMoveBps, "exit.shortAdverseMoveBps");
|
||||||
|
exitReasonScores = checkedProbabilities(exitReasonScores, "exit.exitReasonScores");
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal reasonScore(String key) {
|
||||||
|
return exitReasonScores.get(requiredText(key, "exit reason key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, BigDecimal> checkedProbabilities(Map<String, BigDecimal> scores, String field) {
|
||||||
|
Map<String, BigDecimal> source = scores == null ? Map.of() : scores;
|
||||||
|
if (!source.keySet().containsAll(REQUIRED_REASON_KEYS)) {
|
||||||
|
throw new IllegalArgumentException(field + " must contain " + REQUIRED_REASON_KEYS);
|
||||||
|
}
|
||||||
|
source.forEach((key, value) -> {
|
||||||
|
requiredText(key, field + ".key");
|
||||||
|
probability(value, field + "." + key);
|
||||||
|
});
|
||||||
|
return Map.copyOf(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class FeedbackValidator {
|
||||||
|
public void validateP0(TraderAppFeedback feedback) {
|
||||||
|
if (!feedback.feedbackSource().p0Allowed() || feedback.realFill()) {
|
||||||
|
throw new TraderException(TraderErrorCode.TRADER_FEEDBACK_INVALID,
|
||||||
|
"P0 rejects PAPER_APP/REAL_APP and any realFill feedback");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record ManagementDecision(
|
|
||||||
TraderActionType actionType,
|
|
||||||
String reason,
|
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
|
||||||
|
|
||||||
public ManagementDecision {
|
|
||||||
details = Maps.immutable(details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
final class Maps {
|
|
||||||
|
|
||||||
private Maps() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, Object> immutable(Map<String, Object> value) {
|
|
||||||
if (value == null || value.isEmpty()) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
return Map.copyOf(new LinkedHashMap<>(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
|
public record OpenOrderState(String orderId, String status) {
|
||||||
|
public OpenOrderState {
|
||||||
|
orderId = requiredText(orderId, "orderId");
|
||||||
|
status = requiredText(status, "status");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderSide;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record PlaybookCandidate(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String candidateId,
|
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
TraderSide side,
|
|
||||||
String variant,
|
|
||||||
Instant candidateTime,
|
|
||||||
TraderPricePlan pricePlan,
|
|
||||||
int maxPlannedEntryLegs,
|
|
||||||
Map<String, Object> setupEvidence
|
|
||||||
) {
|
|
||||||
|
|
||||||
public PlaybookCandidate {
|
|
||||||
setupEvidence = Maps.immutable(setupEvidence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record PositionManagerInput(
|
||||||
|
TraderDecisionCycle cycle,
|
||||||
|
TraderMarketSnapshot snapshot,
|
||||||
|
TraderModelOutput modelOutput,
|
||||||
|
TraderPricePlanContext pricePlanContext,
|
||||||
|
TraderPositionState positionState,
|
||||||
|
TraderAccountState accountState,
|
||||||
|
TraderExecutionState executionState,
|
||||||
|
TraderPmConfig pmConfig
|
||||||
|
) {
|
||||||
|
public PositionManagerInput {
|
||||||
|
cycle = Objects.requireNonNull(cycle, "cycle is required");
|
||||||
|
snapshot = Objects.requireNonNull(snapshot, "snapshot is required");
|
||||||
|
modelOutput = Objects.requireNonNull(modelOutput, "modelOutput is required");
|
||||||
|
pricePlanContext = Objects.requireNonNull(pricePlanContext, "pricePlanContext is required");
|
||||||
|
positionState = Objects.requireNonNull(positionState, "positionState is required");
|
||||||
|
accountState = Objects.requireNonNull(accountState, "accountState is required");
|
||||||
|
executionState = Objects.requireNonNull(executionState, "executionState is required");
|
||||||
|
pmConfig = Objects.requireNonNull(pmConfig, "pmConfig is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
public record PositionSizingPlan(
|
|
||||||
int plannedLegCount,
|
|
||||||
BigDecimal initialLegRatio,
|
|
||||||
BigDecimal nextLegRatio,
|
|
||||||
String sizingMethod,
|
|
||||||
BigDecimal signalStrengthScore,
|
|
||||||
BigDecimal executionQualityScore,
|
|
||||||
BigDecimal riskGateScore
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record RiskDecision(
|
|
||||||
boolean allowAction,
|
|
||||||
String blocker,
|
|
||||||
BigDecimal riskGateScore,
|
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
|
||||||
|
|
||||||
public RiskDecision {
|
|
||||||
details = Maps.immutable(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean blocked() {
|
|
||||||
return !allowAction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record RiskOutput(
|
||||||
|
BigDecimal marketRiskProb,
|
||||||
|
BigDecimal longPositionRiskProb,
|
||||||
|
BigDecimal shortPositionRiskProb,
|
||||||
|
BigDecimal marketPathRiskBps,
|
||||||
|
BigDecimal longPositionPathRiskBps,
|
||||||
|
BigDecimal shortPositionPathRiskBps,
|
||||||
|
Map<String, BigDecimal> riskReasonScores
|
||||||
|
) {
|
||||||
|
private static final Set<String> REQUIRED_REASON_KEYS = Set.of(
|
||||||
|
"market_drawdown_prob", "volatility_expansion_prob", "spike_prob",
|
||||||
|
"liquidity_deterioration_prob", "position_drawdown_prob");
|
||||||
|
|
||||||
|
public RiskOutput {
|
||||||
|
marketRiskProb = probability(marketRiskProb, "risk.marketRiskProb");
|
||||||
|
longPositionRiskProb = probability(longPositionRiskProb, "risk.longPositionRiskProb");
|
||||||
|
shortPositionRiskProb = probability(shortPositionRiskProb, "risk.shortPositionRiskProb");
|
||||||
|
marketPathRiskBps = nonNegative(marketPathRiskBps, "risk.marketPathRiskBps");
|
||||||
|
longPositionPathRiskBps = nonNegative(longPositionPathRiskBps, "risk.longPositionPathRiskBps");
|
||||||
|
shortPositionPathRiskBps = nonNegative(shortPositionPathRiskBps, "risk.shortPositionPathRiskBps");
|
||||||
|
riskReasonScores = checkedProbabilities(riskReasonScores, "risk.riskReasonScores");
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal reasonScore(String key) {
|
||||||
|
return riskReasonScores.get(requiredText(key, "risk reason key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal sideRiskProbFor(PositionSide side) {
|
||||||
|
if (side == PositionSide.LONG) {
|
||||||
|
return longPositionRiskProb;
|
||||||
|
}
|
||||||
|
if (side == PositionSide.SHORT) {
|
||||||
|
return shortPositionRiskProb;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("position risk requires LONG or SHORT side");
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal positionPathRiskBpsFor(PositionSide side) {
|
||||||
|
if (side == PositionSide.LONG) {
|
||||||
|
return longPositionPathRiskBps;
|
||||||
|
}
|
||||||
|
if (side == PositionSide.SHORT) {
|
||||||
|
return shortPositionPathRiskBps;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("position path risk requires LONG or SHORT side");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, BigDecimal> checkedProbabilities(Map<String, BigDecimal> scores, String field) {
|
||||||
|
Map<String, BigDecimal> source = scores == null ? Map.of() : scores;
|
||||||
|
if (!source.keySet().containsAll(REQUIRED_REASON_KEYS)) {
|
||||||
|
throw new IllegalArgumentException(field + " must contain " + REQUIRED_REASON_KEYS);
|
||||||
|
}
|
||||||
|
source.forEach((key, value) -> {
|
||||||
|
requiredText(key, field + ".key");
|
||||||
|
probability(value, field + "." + key);
|
||||||
|
});
|
||||||
|
return Map.copyOf(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record StageDecision(
|
|
||||||
boolean pass,
|
|
||||||
String reason,
|
|
||||||
String blocker,
|
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
|
||||||
|
|
||||||
public StageDecision {
|
|
||||||
details = Maps.immutable(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean blocked() {
|
|
||||||
return !pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StageDecision pass(String reason) {
|
|
||||||
return new StageDecision(true, reason, null, Map.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StageDecision block(String reason, String blocker) {
|
|
||||||
return new StageDecision(false, reason, blocker, Map.of());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record TraderAccountState(
|
||||||
|
String accountStateId,
|
||||||
|
String runId,
|
||||||
|
String cycleId,
|
||||||
|
BigDecimal dailyDrawdownBps,
|
||||||
|
BigDecimal portfolioExposureRatio,
|
||||||
|
BigDecimal remainingSymbolCapacityRatio,
|
||||||
|
int consecutiveLosses
|
||||||
|
) {
|
||||||
|
public TraderAccountState {
|
||||||
|
accountStateId = requiredText(accountStateId, "accountStateId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
dailyDrawdownBps = nonNegative(dailyDrawdownBps, "dailyDrawdownBps");
|
||||||
|
portfolioExposureRatio = nonNegative(portfolioExposureRatio, "portfolioExposureRatio");
|
||||||
|
remainingSymbolCapacityRatio = nonNegative(remainingSymbolCapacityRatio, "remainingSymbolCapacityRatio");
|
||||||
|
if (consecutiveLosses < 0) {
|
||||||
|
throw new IllegalArgumentException("consecutiveLosses must be >= 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,60 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
import com.quantai.trader.enums.TraderSide;
|
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderAction(
|
public record TraderAction(
|
||||||
|
String actionId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String actionId,
|
String modelOutputId,
|
||||||
|
String pmDecisionId,
|
||||||
|
String riskDecisionId,
|
||||||
TraderActionType actionType,
|
TraderActionType actionType,
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
String symbol,
|
String symbol,
|
||||||
TraderSide side,
|
PositionSide side,
|
||||||
BigDecimal price,
|
String pricePlanId,
|
||||||
|
String pricePlanConfigHash,
|
||||||
|
BigDecimal positionRatio,
|
||||||
BigDecimal quantity,
|
BigDecimal quantity,
|
||||||
Instant actionTime,
|
BigDecimal stopPrice,
|
||||||
|
BigDecimal targetPrice,
|
||||||
|
boolean reduceOnly,
|
||||||
|
String idempotencyKey,
|
||||||
String reason,
|
String reason,
|
||||||
Map<String, Object> actionContext,
|
Map<String, Object> actionContextJson
|
||||||
String sendStatus
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderAction {
|
public TraderAction {
|
||||||
actionContext = Maps.immutable(actionContext);
|
actionId = requiredText(actionId, "actionId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
modelOutputId = requiredText(modelOutputId, "modelOutputId");
|
||||||
|
pmDecisionId = requiredText(pmDecisionId, "pmDecisionId");
|
||||||
|
riskDecisionId = requiredText(riskDecisionId, "riskDecisionId");
|
||||||
|
actionType = Objects.requireNonNull(actionType, "actionType is required");
|
||||||
|
symbol = requiredText(symbol, "symbol");
|
||||||
|
side = Objects.requireNonNull(side, "side is required");
|
||||||
|
idempotencyKey = requiredText(idempotencyKey, "idempotencyKey");
|
||||||
|
reason = requiredText(reason, "reason");
|
||||||
|
actionContextJson = Map.copyOf(actionContextJson == null ? Map.of() : actionContextJson);
|
||||||
|
if (actionType.increasesExposure()) {
|
||||||
|
pricePlanId = requiredText(pricePlanId, "pricePlanId");
|
||||||
|
pricePlanConfigHash = requiredText(pricePlanConfigHash, "pricePlanConfigHash");
|
||||||
|
positionRatio = positive(positionRatio, "positionRatio");
|
||||||
|
}
|
||||||
|
if (actionType == TraderActionType.REDUCE_LONG || actionType == TraderActionType.REDUCE_SHORT) {
|
||||||
|
positionRatio = positive(positionRatio, "positionRatio");
|
||||||
|
if (positionRatio.compareTo(ONE) > 0) {
|
||||||
|
throw new IllegalArgumentException("positionRatio must be <= 1 for reduce action");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionType.reducesExposure() && !reduceOnly) {
|
||||||
|
throw new IllegalArgumentException("reduce/close action must be reduceOnly");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TraderActionFactory {
|
||||||
|
public TraderAction create(TraderPositionManagerDecision pmDecision, TraderRiskDecision riskDecision, String symbol) {
|
||||||
|
TraderActionType finalAction = riskDecision.finalAction();
|
||||||
|
PositionSide side = sideFor(finalAction, pmDecision.side());
|
||||||
|
return new TraderAction(
|
||||||
|
"action_" + pmDecision.cycleId(),
|
||||||
|
pmDecision.runId(),
|
||||||
|
pmDecision.cycleId(),
|
||||||
|
pmDecision.modelOutputId(),
|
||||||
|
pmDecision.pmDecisionId(),
|
||||||
|
riskDecision.riskDecisionId(),
|
||||||
|
finalAction,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
finalAction.increasesExposure() ? pmDecision.pricePlanId() : null,
|
||||||
|
finalAction.increasesExposure() ? pmDecision.pricePlanConfigHash() : null,
|
||||||
|
ratioFor(finalAction, pmDecision),
|
||||||
|
null,
|
||||||
|
pmDecision.stopPrice(),
|
||||||
|
pmDecision.targetPrice(),
|
||||||
|
finalAction.reducesExposure(),
|
||||||
|
"idem_" + pmDecision.runId() + "_" + pmDecision.cycleId() + "_" + finalAction,
|
||||||
|
riskDecision.allowAction() ? pmDecision.reason() : riskDecision.blocker(),
|
||||||
|
Map.of("riskAllowed", riskDecision.allowAction()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal ratioFor(TraderActionType action, TraderPositionManagerDecision pmDecision) {
|
||||||
|
return switch (action) {
|
||||||
|
case OPEN_LONG, OPEN_SHORT -> pmDecision.targetPositionRatio();
|
||||||
|
case ADD_LONG, ADD_SHORT -> pmDecision.addRatio();
|
||||||
|
case REDUCE_LONG, REDUCE_SHORT -> pmDecision.reduceRatio();
|
||||||
|
case WAIT, HOLD, CLOSE_LONG, CLOSE_SHORT, MOVE_STOP, CANCEL -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private PositionSide sideFor(TraderActionType action, PositionSide pmSide) {
|
||||||
|
return switch (action) {
|
||||||
|
case OPEN_LONG, ADD_LONG, REDUCE_LONG, CLOSE_LONG -> PositionSide.LONG;
|
||||||
|
case OPEN_SHORT, ADD_SHORT, REDUCE_SHORT, CLOSE_SHORT -> PositionSide.SHORT;
|
||||||
|
case WAIT, CANCEL -> PositionSide.NONE;
|
||||||
|
case HOLD, MOVE_STOP -> pmSide;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderFeedbackSource;
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
import com.quantai.trader.enums.TraderFeedbackType;
|
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.FeedbackSource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderAppFeedback(
|
public record TraderAppFeedback(
|
||||||
|
String feedbackId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String actionId,
|
String actionId,
|
||||||
TraderFeedbackType feedbackType,
|
FeedbackSource feedbackSource,
|
||||||
TraderFeedbackSource feedbackSource,
|
|
||||||
String proxyMethod,
|
|
||||||
String simulatorVersion,
|
|
||||||
boolean realFill,
|
boolean realFill,
|
||||||
String orderId,
|
String orderId,
|
||||||
String positionId,
|
|
||||||
String orderStatus,
|
String orderStatus,
|
||||||
Instant appReceivedTime,
|
Instant appReceivedTime,
|
||||||
Instant exchangeAckTime,
|
Instant exchangeAckTime,
|
||||||
@@ -26,19 +24,24 @@ public record TraderAppFeedback(
|
|||||||
BigDecimal filledQuantity,
|
BigDecimal filledQuantity,
|
||||||
BigDecimal fee,
|
BigDecimal fee,
|
||||||
BigDecimal slippageBps,
|
BigDecimal slippageBps,
|
||||||
String closeReason,
|
String rejectReason,
|
||||||
String closeSignalSource,
|
Map<String, Object> rawFeedbackJson
|
||||||
String exchangeErrorCode,
|
|
||||||
String platformErrorCode,
|
|
||||||
Map<String, Object> rawFeedback
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderAppFeedback {
|
public TraderAppFeedback {
|
||||||
rawFeedback = Maps.immutable(rawFeedback);
|
feedbackId = requiredText(feedbackId, "feedbackId");
|
||||||
boolean sourceCanBeReal = feedbackSource == TraderFeedbackSource.PAPER_APP
|
runId = requiredText(runId, "runId");
|
||||||
|| feedbackSource == TraderFeedbackSource.REAL_APP;
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
if (realFill != sourceCanBeReal) {
|
actionId = requiredText(actionId, "actionId");
|
||||||
throw new IllegalArgumentException("feedback_source and realFill are inconsistent");
|
feedbackSource = Objects.requireNonNull(feedbackSource, "feedbackSource is required");
|
||||||
|
if (realFill && !feedbackSource.canBeRealFill()) {
|
||||||
|
throw new IllegalArgumentException("realFill requires PAPER_APP or REAL_APP");
|
||||||
}
|
}
|
||||||
|
if (!realFill && feedbackSource.canBeRealFill()) {
|
||||||
|
throw new IllegalArgumentException("PAPER_APP/REAL_APP feedback must be realFill");
|
||||||
|
}
|
||||||
|
if (filledQuantity != null && filledQuantity.compareTo(ZERO) > 0) {
|
||||||
|
filledPrice = positive(filledPrice, "filledPrice");
|
||||||
|
}
|
||||||
|
rawFeedbackJson = Map.copyOf(rawFeedbackJson == null ? Map.of() : rawFeedbackJson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderErrorCode;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderDataSourceManifest(
|
|
||||||
String sourceId,
|
|
||||||
String symbol,
|
|
||||||
String sourceType,
|
|
||||||
String exchange,
|
|
||||||
String granularity,
|
|
||||||
String sourcePath,
|
|
||||||
String contentHashSha256,
|
|
||||||
String schemaHashSha256,
|
|
||||||
Instant dataFrom,
|
|
||||||
Instant dataTo,
|
|
||||||
Instant minEventTime,
|
|
||||||
Instant maxEventTime,
|
|
||||||
String timezone,
|
|
||||||
Long rowCount,
|
|
||||||
Map<String, Object> missingSummary,
|
|
||||||
String qualityStatus
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderDataSourceManifest {
|
|
||||||
missingSummary = Maps.immutable(missingSummary);
|
|
||||||
boolean hasFullHash = contentHashSha256 != null && !contentHashSha256.isBlank();
|
|
||||||
boolean hasSchemaTrace = schemaHashSha256 != null
|
|
||||||
&& !schemaHashSha256.isBlank()
|
|
||||||
&& rowCount != null
|
|
||||||
&& minEventTime != null
|
|
||||||
&& maxEventTime != null;
|
|
||||||
if (sourceId == null || sourceId.isBlank() || sourcePath == null || sourcePath.isBlank()) {
|
|
||||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source id and path are required");
|
|
||||||
}
|
|
||||||
if (timezone == null || timezone.isBlank()) {
|
|
||||||
throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source timezone is required");
|
|
||||||
}
|
|
||||||
if (!hasFullHash && !hasSchemaTrace) {
|
|
||||||
throw new TraderException(
|
|
||||||
TraderErrorCode.TRADER_DATA_SOURCE_MISSING,
|
|
||||||
"data source requires content hash or schema/row/time lineage"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,29 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderRunMode;
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
import com.quantai.trader.enums.TraderState;
|
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.TraderRunMode;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderDecisionCycle(
|
public record TraderDecisionCycle(
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String snapshotId,
|
|
||||||
String symbol,
|
String symbol,
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
TraderState state,
|
|
||||||
Instant cycleTime,
|
Instant cycleTime,
|
||||||
TraderRunMode runMode,
|
TraderRunMode runMode,
|
||||||
String decisionStatus,
|
String modelBundleVersion,
|
||||||
String blocker
|
String calibrationBundleVersion,
|
||||||
|
String pmConfigVersion
|
||||||
) {
|
) {
|
||||||
|
public TraderDecisionCycle {
|
||||||
public TraderDecisionCycle withState(TraderState nextState, String nextStatus, String nextBlocker) {
|
runId = requiredText(runId, "runId");
|
||||||
return new TraderDecisionCycle(
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
runId,
|
symbol = requiredText(symbol, "symbol");
|
||||||
cycleId,
|
cycleTime = Objects.requireNonNull(cycleTime, "cycleTime is required");
|
||||||
snapshotId,
|
runMode = Objects.requireNonNull(runMode, "runMode is required");
|
||||||
symbol,
|
modelBundleVersion = requiredText(modelBundleVersion, "modelBundleVersion");
|
||||||
playbookId,
|
calibrationBundleVersion = requiredText(calibrationBundleVersion, "calibrationBundleVersion");
|
||||||
playbookVersion,
|
pmConfigVersion = requiredText(pmConfigVersion, "pmConfigVersion");
|
||||||
nextState,
|
|
||||||
cycleTime,
|
|
||||||
runMode,
|
|
||||||
nextStatus,
|
|
||||||
nextBlocker
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
public record TraderEntryPlan(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String actionId,
|
|
||||||
String entryLegId,
|
|
||||||
String candidateId,
|
|
||||||
TraderActionType entryAction,
|
|
||||||
int plannedLegIndex,
|
|
||||||
int plannedLegCount,
|
|
||||||
BigDecimal plannedLegRatio,
|
|
||||||
String sizingMethod,
|
|
||||||
BigDecimal signalStrengthScore,
|
|
||||||
BigDecimal executionQualityScore,
|
|
||||||
BigDecimal riskGateScore,
|
|
||||||
BigDecimal entryPrice,
|
|
||||||
BigDecimal invalidPrice,
|
|
||||||
BigDecimal stopPrice,
|
|
||||||
BigDecimal targetPrice,
|
|
||||||
BigDecimal partialTakeProfitPrice,
|
|
||||||
long maxEntryWaitMs,
|
|
||||||
long maxHoldMs,
|
|
||||||
String reason
|
|
||||||
) {
|
|
||||||
|
|
||||||
public boolean completeForEntry() {
|
|
||||||
return entryPrice != null
|
|
||||||
&& invalidPrice != null
|
|
||||||
&& stopPrice != null
|
|
||||||
&& targetPrice != null
|
|
||||||
&& maxEntryWaitMs > 0
|
|
||||||
&& maxHoldMs > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderEvidence(
|
public record TraderEvidence(
|
||||||
|
String evidenceId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String evidenceId,
|
|
||||||
String stage,
|
String stage,
|
||||||
boolean pass,
|
boolean pass,
|
||||||
String reason,
|
String reason,
|
||||||
String blocker,
|
String blocker,
|
||||||
Instant evidenceTime,
|
Instant evidenceTime,
|
||||||
Map<String, Object> details
|
Map<String, Object> detailsJson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderEvidence {
|
public TraderEvidence {
|
||||||
details = Maps.immutable(details);
|
evidenceId = requiredText(evidenceId, "evidenceId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
stage = requiredText(stage, "stage");
|
||||||
|
reason = requiredText(reason, "reason");
|
||||||
|
evidenceTime = Objects.requireNonNull(evidenceTime, "evidenceTime is required");
|
||||||
|
detailsJson = Map.copyOf(detailsJson == null ? Map.of() : detailsJson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ package com.quantai.trader.domain;
|
|||||||
import com.quantai.trader.enums.TraderErrorCode;
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
|
|
||||||
public class TraderException extends RuntimeException {
|
public class TraderException extends RuntimeException {
|
||||||
|
private final TraderErrorCode code;
|
||||||
|
|
||||||
private final TraderErrorCode errorCode;
|
public TraderException(TraderErrorCode code, String message) {
|
||||||
|
|
||||||
public TraderException(TraderErrorCode errorCode, String message) {
|
|
||||||
super(message);
|
super(message);
|
||||||
this.errorCode = errorCode;
|
this.code = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TraderErrorCode errorCode() {
|
public TraderErrorCode code() {
|
||||||
return errorCode;
|
return code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record TraderExecutionState(
|
||||||
|
String executionStateId,
|
||||||
|
String runId,
|
||||||
|
String cycleId,
|
||||||
|
String symbol,
|
||||||
|
List<OpenOrderState> openOrders,
|
||||||
|
BigDecimal expectedSlippageBps,
|
||||||
|
long exchangeLatencyMs,
|
||||||
|
int apiErrorCount,
|
||||||
|
BigDecimal makerFeeBps,
|
||||||
|
BigDecimal takerFeeBps,
|
||||||
|
BigDecimal minNotional,
|
||||||
|
BigDecimal priceTickSize,
|
||||||
|
BigDecimal lotSizeStepSize,
|
||||||
|
BigDecimal marketLotSizeStepSize,
|
||||||
|
BigDecimal liquidityCapacityRatio
|
||||||
|
) {
|
||||||
|
public TraderExecutionState {
|
||||||
|
executionStateId = requiredText(executionStateId, "executionStateId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
symbol = requiredText(symbol, "symbol");
|
||||||
|
openOrders = List.copyOf(openOrders == null ? List.of() : openOrders);
|
||||||
|
expectedSlippageBps = nonNegative(expectedSlippageBps, "expectedSlippageBps");
|
||||||
|
if (exchangeLatencyMs < 0) {
|
||||||
|
throw new IllegalArgumentException("exchangeLatencyMs must be >= 0");
|
||||||
|
}
|
||||||
|
if (apiErrorCount < 0) {
|
||||||
|
throw new IllegalArgumentException("apiErrorCount must be >= 0");
|
||||||
|
}
|
||||||
|
makerFeeBps = nonNegative(makerFeeBps, "makerFeeBps");
|
||||||
|
takerFeeBps = nonNegative(takerFeeBps, "takerFeeBps");
|
||||||
|
minNotional = positive(minNotional, "minNotional");
|
||||||
|
priceTickSize = positive(priceTickSize, "priceTickSize");
|
||||||
|
lotSizeStepSize = positive(lotSizeStepSize, "lotSizeStepSize");
|
||||||
|
marketLotSizeStepSize = positive(marketLotSizeStepSize, "marketLotSizeStepSize");
|
||||||
|
liquidityCapacityRatio = nonNegative(liquidityCapacityRatio, "liquidityCapacityRatio");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderManagementAction(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String actionId,
|
|
||||||
String managementActionId,
|
|
||||||
String positionId,
|
|
||||||
TraderActionType managementActionType,
|
|
||||||
Instant actionTime,
|
|
||||||
BigDecimal beforeRiskBps,
|
|
||||||
BigDecimal afterRiskBps,
|
|
||||||
String reason,
|
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderManagementAction {
|
|
||||||
details = Maps.immutable(details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderMarketEvent(
|
|
||||||
String runId,
|
|
||||||
String eventId,
|
|
||||||
String symbol,
|
|
||||||
Instant eventTime,
|
|
||||||
String source,
|
|
||||||
String sourcePath,
|
|
||||||
Map<String, Object> payload
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderMarketEvent {
|
|
||||||
payload = Maps.immutable(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,44 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public record TraderMarketSnapshot(
|
public record TraderMarketSnapshot(
|
||||||
|
String snapshotId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String snapshotId,
|
|
||||||
String symbol,
|
String symbol,
|
||||||
Instant snapshotTime,
|
Instant snapshotTime,
|
||||||
String featureVersion,
|
String featureVersion,
|
||||||
Map<String, Object> contextFeatures,
|
BigDecimal markPrice,
|
||||||
Map<String, Object> setupFeatures,
|
BigDecimal indexPrice,
|
||||||
Map<String, Object> triggerFeatures,
|
BigDecimal spreadBps,
|
||||||
Map<String, Object> executionFeatures,
|
BigDecimal fundingRateBps,
|
||||||
Map<String, Object> dataQuality,
|
BigDecimal depthNotional5Bps,
|
||||||
Map<String, Object> labelInputs
|
BigDecimal depthNotional10Bps,
|
||||||
|
BigDecimal depthNotional25Bps,
|
||||||
|
boolean dataReady,
|
||||||
|
Map<String, Object> featureJson,
|
||||||
|
Map<String, Object> dataQualityJson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderMarketSnapshot {
|
public TraderMarketSnapshot {
|
||||||
contextFeatures = Maps.immutable(contextFeatures);
|
snapshotId = requiredText(snapshotId, "snapshotId");
|
||||||
setupFeatures = Maps.immutable(setupFeatures);
|
runId = requiredText(runId, "runId");
|
||||||
triggerFeatures = Maps.immutable(triggerFeatures);
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
executionFeatures = Maps.immutable(executionFeatures);
|
symbol = requiredText(symbol, "symbol");
|
||||||
dataQuality = Maps.immutable(dataQuality);
|
snapshotTime = java.util.Objects.requireNonNull(snapshotTime, "snapshotTime is required");
|
||||||
labelInputs = Maps.immutable(labelInputs);
|
featureVersion = requiredText(featureVersion, "featureVersion");
|
||||||
|
markPrice = positive(markPrice, "markPrice");
|
||||||
|
indexPrice = positive(indexPrice, "indexPrice");
|
||||||
|
spreadBps = nonNegative(spreadBps, "spreadBps");
|
||||||
|
fundingRateBps = required(fundingRateBps, "fundingRateBps");
|
||||||
|
depthNotional5Bps = nonNegative(depthNotional5Bps, "depthNotional5Bps");
|
||||||
|
depthNotional10Bps = nonNegative(depthNotional10Bps, "depthNotional10Bps");
|
||||||
|
depthNotional25Bps = nonNegative(depthNotional25Bps, "depthNotional25Bps");
|
||||||
|
featureJson = Map.copyOf(featureJson == null ? Map.of() : featureJson);
|
||||||
|
dataQualityJson = Map.copyOf(dataQualityJson == null ? Map.of() : dataQualityJson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderModelManifest(
|
|
||||||
String modelName,
|
|
||||||
String modelVersion,
|
|
||||||
String featureVersion,
|
|
||||||
String labelVersion,
|
|
||||||
String artifactPath,
|
|
||||||
Instant trainedAt,
|
|
||||||
Map<String, Object> metrics,
|
|
||||||
String status
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderModelManifest {
|
|
||||||
metrics = Maps.immutable(metrics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,29 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderModelOutput(
|
public record TraderModelOutput(
|
||||||
String modelName,
|
String modelOutputId,
|
||||||
String modelVersion,
|
String runId,
|
||||||
BigDecimal score,
|
String cycleId,
|
||||||
BigDecimal uncertainty,
|
TraderModelOutputMetadata metadata,
|
||||||
BigDecimal oodScore,
|
DirectionOutput direction,
|
||||||
Instant predictedAt,
|
EntryOutput entry,
|
||||||
Map<String, Object> details
|
ContinueOutput continuation,
|
||||||
|
ExitOutput exit,
|
||||||
|
RiskOutput risk
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderModelOutput {
|
public TraderModelOutput {
|
||||||
details = Maps.immutable(details);
|
modelOutputId = requiredText(modelOutputId, "modelOutputId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
metadata = Objects.requireNonNull(metadata, "metadata is required");
|
||||||
|
direction = Objects.requireNonNull(direction, "direction is required");
|
||||||
|
entry = Objects.requireNonNull(entry, "entry is required");
|
||||||
|
continuation = Objects.requireNonNull(continuation, "continuation is required");
|
||||||
|
exit = Objects.requireNonNull(exit, "exit is required");
|
||||||
|
risk = Objects.requireNonNull(risk, "risk is required");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TraderModelOutputMetadata(
|
||||||
|
String modelBundleVersion,
|
||||||
|
String calibrationBundleVersion,
|
||||||
|
Map<String, String> modelVersions,
|
||||||
|
Map<String, String> calibrationVersions,
|
||||||
|
String featureSchemaHash,
|
||||||
|
String featureOrderHash,
|
||||||
|
String outputSchemaHash,
|
||||||
|
BigDecimal uncertainty,
|
||||||
|
BigDecimal oodScore
|
||||||
|
) {
|
||||||
|
public TraderModelOutputMetadata {
|
||||||
|
modelBundleVersion = requiredText(modelBundleVersion, "metadata.modelBundleVersion");
|
||||||
|
calibrationBundleVersion = requiredText(calibrationBundleVersion, "metadata.calibrationBundleVersion");
|
||||||
|
modelVersions = checkedTextMap(modelVersions, "metadata.modelVersions");
|
||||||
|
calibrationVersions = checkedTextMap(calibrationVersions, "metadata.calibrationVersions");
|
||||||
|
featureSchemaHash = requiredText(featureSchemaHash, "metadata.featureSchemaHash");
|
||||||
|
featureOrderHash = requiredText(featureOrderHash, "metadata.featureOrderHash");
|
||||||
|
outputSchemaHash = requiredText(outputSchemaHash, "metadata.outputSchemaHash");
|
||||||
|
uncertainty = probability(uncertainty, "metadata.uncertainty");
|
||||||
|
oodScore = probability(oodScore, "metadata.oodScore");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> checkedTextMap(Map<String, String> values, String field) {
|
||||||
|
Map<String, String> source = values == null ? Map.of() : values;
|
||||||
|
source.forEach((key, value) -> {
|
||||||
|
requiredText(key, field + ".key");
|
||||||
|
requiredText(value, field + "." + key);
|
||||||
|
});
|
||||||
|
return Map.copyOf(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record TraderPmConfig(
|
||||||
|
String pmConfigVersion,
|
||||||
|
OpenRuleConfig open,
|
||||||
|
AddRuleConfig add,
|
||||||
|
ExitRuleConfig exit,
|
||||||
|
SizingConfig sizing
|
||||||
|
) {
|
||||||
|
public TraderPmConfig {
|
||||||
|
pmConfigVersion = requiredText(pmConfigVersion, "pmConfigVersion");
|
||||||
|
open = java.util.Objects.requireNonNull(open, "open config is required");
|
||||||
|
add = java.util.Objects.requireNonNull(add, "add config is required");
|
||||||
|
exit = java.util.Objects.requireNonNull(exit, "exit config is required");
|
||||||
|
sizing = java.util.Objects.requireNonNull(sizing, "sizing config is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OpenRuleConfig(
|
||||||
|
BigDecimal longOpenProb,
|
||||||
|
BigDecimal shortOpenProb,
|
||||||
|
BigDecimal minLongEntryProb,
|
||||||
|
BigDecimal minShortEntryProb,
|
||||||
|
BigDecimal maxMarketRiskProb,
|
||||||
|
BigDecimal minExpectedEdgeBps,
|
||||||
|
BigDecimal minDirectionMargin,
|
||||||
|
BigDecimal minLiquidityCapacityRatio,
|
||||||
|
BigDecimal maxOodScore
|
||||||
|
) {
|
||||||
|
public OpenRuleConfig {
|
||||||
|
longOpenProb = probability(longOpenProb, "open.longOpenProb");
|
||||||
|
shortOpenProb = probability(shortOpenProb, "open.shortOpenProb");
|
||||||
|
minLongEntryProb = probability(minLongEntryProb, "open.minLongEntryProb");
|
||||||
|
minShortEntryProb = probability(minShortEntryProb, "open.minShortEntryProb");
|
||||||
|
maxMarketRiskProb = probability(maxMarketRiskProb, "open.maxMarketRiskProb");
|
||||||
|
minExpectedEdgeBps = required(minExpectedEdgeBps, "open.minExpectedEdgeBps");
|
||||||
|
minDirectionMargin = nonNegative(minDirectionMargin, "open.minDirectionMargin");
|
||||||
|
minLiquidityCapacityRatio = nonNegative(minLiquidityCapacityRatio, "open.minLiquidityCapacityRatio");
|
||||||
|
maxOodScore = probability(maxOodScore, "open.maxOodScore");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AddRuleConfig(
|
||||||
|
BigDecimal minLongProb,
|
||||||
|
BigDecimal minShortProb,
|
||||||
|
BigDecimal minContinueProb,
|
||||||
|
BigDecimal minEntryProb,
|
||||||
|
BigDecimal maxExitProb,
|
||||||
|
BigDecimal maxMarketRiskProb,
|
||||||
|
BigDecimal maxPositionRiskProb,
|
||||||
|
BigDecimal minExpectedEdgeBps,
|
||||||
|
BigDecimal minContinueVsExitEdgeBps,
|
||||||
|
BigDecimal minLiquidityCapacityRatio,
|
||||||
|
BigDecimal minPostTradeLiquidationBufferBps,
|
||||||
|
int maxAddCount,
|
||||||
|
long cooldownMinutes
|
||||||
|
) {
|
||||||
|
public AddRuleConfig {
|
||||||
|
minLongProb = probability(minLongProb, "add.minLongProb");
|
||||||
|
minShortProb = probability(minShortProb, "add.minShortProb");
|
||||||
|
minContinueProb = probability(minContinueProb, "add.minContinueProb");
|
||||||
|
minEntryProb = probability(minEntryProb, "add.minEntryProb");
|
||||||
|
maxExitProb = probability(maxExitProb, "add.maxExitProb");
|
||||||
|
maxMarketRiskProb = probability(maxMarketRiskProb, "add.maxMarketRiskProb");
|
||||||
|
maxPositionRiskProb = probability(maxPositionRiskProb, "add.maxPositionRiskProb");
|
||||||
|
minExpectedEdgeBps = required(minExpectedEdgeBps, "add.minExpectedEdgeBps");
|
||||||
|
minContinueVsExitEdgeBps = required(minContinueVsExitEdgeBps, "add.minContinueVsExitEdgeBps");
|
||||||
|
minLiquidityCapacityRatio = nonNegative(minLiquidityCapacityRatio, "add.minLiquidityCapacityRatio");
|
||||||
|
minPostTradeLiquidationBufferBps = nonNegative(minPostTradeLiquidationBufferBps, "add.minPostTradeLiquidationBufferBps");
|
||||||
|
if (maxAddCount < 0 || cooldownMinutes < 0) {
|
||||||
|
throw new IllegalArgumentException("add count and cooldown must be >= 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ExitRuleConfig(
|
||||||
|
BigDecimal closeExitProb,
|
||||||
|
BigDecimal closePositionRiskProb,
|
||||||
|
BigDecimal closeMarketRiskProb,
|
||||||
|
BigDecimal closeContinueMax,
|
||||||
|
BigDecimal reduceAdverseMoveProb,
|
||||||
|
BigDecimal reduceContinueMin,
|
||||||
|
BigDecimal reduceContinueMax,
|
||||||
|
BigDecimal minProfitForReduceBps,
|
||||||
|
BigDecimal maxPositionPathRiskBps
|
||||||
|
) {
|
||||||
|
public ExitRuleConfig {
|
||||||
|
closeExitProb = probability(closeExitProb, "exit.closeExitProb");
|
||||||
|
closePositionRiskProb = probability(closePositionRiskProb, "exit.closePositionRiskProb");
|
||||||
|
closeMarketRiskProb = probability(closeMarketRiskProb, "exit.closeMarketRiskProb");
|
||||||
|
closeContinueMax = probability(closeContinueMax, "exit.closeContinueMax");
|
||||||
|
reduceAdverseMoveProb = probability(reduceAdverseMoveProb, "exit.reduceAdverseMoveProb");
|
||||||
|
reduceContinueMin = probability(reduceContinueMin, "exit.reduceContinueMin");
|
||||||
|
reduceContinueMax = probability(reduceContinueMax, "exit.reduceContinueMax");
|
||||||
|
minProfitForReduceBps = nonNegative(minProfitForReduceBps, "exit.minProfitForReduceBps");
|
||||||
|
maxPositionPathRiskBps = nonNegative(maxPositionPathRiskBps, "exit.maxPositionPathRiskBps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SizingConfig(
|
||||||
|
BigDecimal baseRatio,
|
||||||
|
BigDecimal minInitialRatio,
|
||||||
|
BigDecimal maxSingleLegRatio,
|
||||||
|
BigDecimal minAddRatio,
|
||||||
|
BigDecimal maxAddRatio,
|
||||||
|
BigDecimal maxTotalPositionRatio,
|
||||||
|
BigDecimal minEdgeBps,
|
||||||
|
BigDecimal maxLossPerTradeBps,
|
||||||
|
BigDecimal maxLiquidityUsageRatio,
|
||||||
|
BigDecimal uncertaintyPenaltyMultiplier,
|
||||||
|
BigDecimal minPostTradeLiquidationBufferBps
|
||||||
|
) {
|
||||||
|
public SizingConfig {
|
||||||
|
baseRatio = positive(baseRatio, "sizing.baseRatio");
|
||||||
|
minInitialRatio = nonNegative(minInitialRatio, "sizing.minInitialRatio");
|
||||||
|
maxSingleLegRatio = positive(maxSingleLegRatio, "sizing.maxSingleLegRatio");
|
||||||
|
minAddRatio = nonNegative(minAddRatio, "sizing.minAddRatio");
|
||||||
|
maxAddRatio = nonNegative(maxAddRatio, "sizing.maxAddRatio");
|
||||||
|
maxTotalPositionRatio = positive(maxTotalPositionRatio, "sizing.maxTotalPositionRatio");
|
||||||
|
minEdgeBps = required(minEdgeBps, "sizing.minEdgeBps");
|
||||||
|
maxLossPerTradeBps = positive(maxLossPerTradeBps, "sizing.maxLossPerTradeBps");
|
||||||
|
maxLiquidityUsageRatio = positive(maxLiquidityUsageRatio, "sizing.maxLiquidityUsageRatio");
|
||||||
|
uncertaintyPenaltyMultiplier = nonNegative(uncertaintyPenaltyMultiplier, "sizing.uncertaintyPenaltyMultiplier");
|
||||||
|
minPostTradeLiquidationBufferBps = nonNegative(minPostTradeLiquidationBufferBps, "sizing.minPostTradeLiquidationBufferBps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
public record TraderPositionLeg(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String positionId,
|
|
||||||
String legId,
|
|
||||||
TraderActionType actionType,
|
|
||||||
BigDecimal quantity,
|
|
||||||
BigDecimal price,
|
|
||||||
BigDecimal legRatio,
|
|
||||||
BigDecimal riskDeltaBps,
|
|
||||||
Instant actionTime,
|
|
||||||
String reason
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TraderPositionManagerDecision(
|
||||||
|
String pmDecisionId,
|
||||||
|
String runId,
|
||||||
|
String cycleId,
|
||||||
|
String modelOutputId,
|
||||||
|
String positionStateId,
|
||||||
|
String accountStateId,
|
||||||
|
String executionStateId,
|
||||||
|
TraderActionType candidateAction,
|
||||||
|
PositionSide side,
|
||||||
|
String pricePlanId,
|
||||||
|
String pricePlanConfigHash,
|
||||||
|
BigDecimal targetPositionRatio,
|
||||||
|
BigDecimal addRatio,
|
||||||
|
BigDecimal reduceRatio,
|
||||||
|
BigDecimal stopPrice,
|
||||||
|
BigDecimal targetPrice,
|
||||||
|
String reason,
|
||||||
|
Map<String, Object> decisionJson
|
||||||
|
) {
|
||||||
|
public TraderPositionManagerDecision {
|
||||||
|
pmDecisionId = requiredText(pmDecisionId, "pmDecisionId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
modelOutputId = requiredText(modelOutputId, "modelOutputId");
|
||||||
|
positionStateId = requiredText(positionStateId, "positionStateId");
|
||||||
|
accountStateId = requiredText(accountStateId, "accountStateId");
|
||||||
|
executionStateId = requiredText(executionStateId, "executionStateId");
|
||||||
|
candidateAction = Objects.requireNonNull(candidateAction, "candidateAction is required");
|
||||||
|
side = Objects.requireNonNull(side, "side is required");
|
||||||
|
reason = requiredText(reason, "reason");
|
||||||
|
decisionJson = Map.copyOf(decisionJson == null ? Map.of() : decisionJson);
|
||||||
|
if (candidateAction.increasesExposure()) {
|
||||||
|
pricePlanId = requiredText(pricePlanId, "pricePlanId");
|
||||||
|
pricePlanConfigHash = requiredText(pricePlanConfigHash, "pricePlanConfigHash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderSide;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderPositionPath(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String actionId,
|
|
||||||
String positionId,
|
|
||||||
TraderSide side,
|
|
||||||
Instant entryTime,
|
|
||||||
Instant lastEventTime,
|
|
||||||
BigDecimal entryPrice,
|
|
||||||
BigDecimal currentPrice,
|
|
||||||
BigDecimal mfeBps,
|
|
||||||
BigDecimal maeBps,
|
|
||||||
Long timeToTargetMs,
|
|
||||||
Long timeToInvalidMs,
|
|
||||||
boolean targetBeforeStop,
|
|
||||||
boolean stagnationTimeoutHit,
|
|
||||||
boolean proxyOnly,
|
|
||||||
boolean reduceSeen,
|
|
||||||
int filledLegCount,
|
|
||||||
BigDecimal totalPositionRatio,
|
|
||||||
Map<String, Object> pathSummary
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderPositionPath {
|
|
||||||
pathSummary = Maps.immutable(pathSummary);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean opened() {
|
|
||||||
return positionId != null && filledLegCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean fullSize() {
|
|
||||||
return totalPositionRatio != null && totalPositionRatio.compareTo(BigDecimal.ONE) >= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import com.quantai.trader.enums.PositionSide;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TraderPositionState(
|
||||||
|
String positionStateId,
|
||||||
|
String runId,
|
||||||
|
String cycleId,
|
||||||
|
String symbol,
|
||||||
|
PositionSide side,
|
||||||
|
BigDecimal positionRatio,
|
||||||
|
BigDecimal averageEntryPrice,
|
||||||
|
BigDecimal currentPrice,
|
||||||
|
BigDecimal unrealizedPnlBps,
|
||||||
|
BigDecimal liquidationBufferBps,
|
||||||
|
int addCount,
|
||||||
|
BigDecimal remainingAddCapacity,
|
||||||
|
Instant lastAddTime
|
||||||
|
) {
|
||||||
|
public TraderPositionState {
|
||||||
|
positionStateId = requiredText(positionStateId, "positionStateId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
symbol = requiredText(symbol, "symbol");
|
||||||
|
side = Objects.requireNonNull(side, "side is required");
|
||||||
|
positionRatio = nonNegative(positionRatio, "positionRatio");
|
||||||
|
currentPrice = positive(currentPrice, "currentPrice");
|
||||||
|
if (side == PositionSide.NONE && positionRatio.compareTo(ZERO) != 0) {
|
||||||
|
throw new IllegalArgumentException("flat position must have zero ratio");
|
||||||
|
}
|
||||||
|
if (side != PositionSide.NONE) {
|
||||||
|
averageEntryPrice = positive(averageEntryPrice, "averageEntryPrice");
|
||||||
|
}
|
||||||
|
unrealizedPnlBps = required(unrealizedPnlBps, "unrealizedPnlBps");
|
||||||
|
liquidationBufferBps = nonNegative(liquidationBufferBps, "liquidationBufferBps");
|
||||||
|
remainingAddCapacity = nonNegative(remainingAddCapacity, "remainingAddCapacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFlat() {
|
||||||
|
return side == PositionSide.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
public record TraderPricePlan(
|
|
||||||
BigDecimal entryPrice,
|
|
||||||
BigDecimal invalidPrice,
|
|
||||||
BigDecimal stopPrice,
|
|
||||||
BigDecimal targetPrice,
|
|
||||||
BigDecimal partialTakeProfitPrice,
|
|
||||||
long maxEntryWaitMs,
|
|
||||||
long maxHoldMs
|
|
||||||
) {
|
|
||||||
|
|
||||||
public boolean completeForEntry() {
|
|
||||||
return entryPrice != null
|
|
||||||
&& invalidPrice != null
|
|
||||||
&& stopPrice != null
|
|
||||||
&& targetPrice != null
|
|
||||||
&& maxEntryWaitMs > 0
|
|
||||||
&& maxHoldMs > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
|
import static com.quantai.trader.util.TraderNumbers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record TraderPricePlanContext(
|
||||||
|
String pricePlanId,
|
||||||
|
String pricePlanConfigHash,
|
||||||
|
BigDecimal stopDistanceBps,
|
||||||
|
BigDecimal targetDistanceBps,
|
||||||
|
int maxHoldMinutes,
|
||||||
|
BigDecimal costBps
|
||||||
|
) {
|
||||||
|
public TraderPricePlanContext {
|
||||||
|
pricePlanId = requiredText(pricePlanId, "pricePlan.pricePlanId");
|
||||||
|
pricePlanConfigHash = requiredText(pricePlanConfigHash, "pricePlan.pricePlanConfigHash");
|
||||||
|
stopDistanceBps = positive(stopDistanceBps, "pricePlan.stopDistanceBps");
|
||||||
|
targetDistanceBps = positive(targetDistanceBps, "pricePlan.targetDistanceBps");
|
||||||
|
if (maxHoldMinutes <= 0) {
|
||||||
|
throw new IllegalArgumentException("pricePlan.maxHoldMinutes must be > 0");
|
||||||
|
}
|
||||||
|
costBps = nonNegative(costBps, "pricePlan.costBps");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderReplayReport(
|
|
||||||
String runId,
|
|
||||||
String reportId,
|
|
||||||
String symbol,
|
|
||||||
String playbookId,
|
|
||||||
String playbookVersion,
|
|
||||||
int candidateEvents,
|
|
||||||
int monthsCovered,
|
|
||||||
BigDecimal baseNetReturnBps1x,
|
|
||||||
BigDecimal leveragedNetReturnBps10x,
|
|
||||||
BigDecimal holdoutReturnBps10x,
|
|
||||||
Map<String, Object> strictVsLoose,
|
|
||||||
List<String> failureRisks,
|
|
||||||
String conclusion,
|
|
||||||
String reportPath,
|
|
||||||
Instant createdAt
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderReplayReport {
|
|
||||||
strictVsLoose = Maps.immutable(strictVsLoose);
|
|
||||||
failureRisks = failureRisks == null ? List.of() : List.copyOf(failureRisks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
package com.quantai.trader.domain;
|
package com.quantai.trader.domain;
|
||||||
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
import static com.quantai.trader.util.TraderNumbers.requiredText;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import com.quantai.trader.enums.TraderActionType;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record TraderRiskDecision(
|
public record TraderRiskDecision(
|
||||||
|
String riskDecisionId,
|
||||||
String runId,
|
String runId,
|
||||||
String cycleId,
|
String cycleId,
|
||||||
String actionId,
|
String pmDecisionId,
|
||||||
String accountStateId,
|
|
||||||
TraderActionType actionType,
|
|
||||||
BigDecimal leverageScreen,
|
|
||||||
BigDecimal plannedTotalPositionRatio,
|
|
||||||
BigDecimal maxLossBps,
|
|
||||||
BigDecimal liquidationBufferBps,
|
|
||||||
BigDecimal expectedValueBps1x,
|
|
||||||
BigDecimal expectedValueBps10x,
|
|
||||||
BigDecimal uncertainty,
|
|
||||||
BigDecimal oodScore,
|
|
||||||
boolean allowAction,
|
boolean allowAction,
|
||||||
|
TraderActionType originalAction,
|
||||||
|
TraderActionType finalAction,
|
||||||
String blocker,
|
String blocker,
|
||||||
Map<String, Object> decision,
|
Map<String, Object> decisionJson
|
||||||
Instant createdAt
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public TraderRiskDecision {
|
public TraderRiskDecision {
|
||||||
decision = Maps.immutable(decision);
|
riskDecisionId = requiredText(riskDecisionId, "riskDecisionId");
|
||||||
|
runId = requiredText(runId, "runId");
|
||||||
|
cycleId = requiredText(cycleId, "cycleId");
|
||||||
|
pmDecisionId = requiredText(pmDecisionId, "pmDecisionId");
|
||||||
|
originalAction = Objects.requireNonNull(originalAction, "originalAction is required");
|
||||||
|
finalAction = Objects.requireNonNull(finalAction, "finalAction is required");
|
||||||
|
if (!allowAction) {
|
||||||
|
blocker = requiredText(blocker, "blocker");
|
||||||
|
}
|
||||||
|
decisionJson = Map.copyOf(decisionJson == null ? Map.of() : decisionJson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TraderTrainingSample(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String sampleId,
|
|
||||||
String actionId,
|
|
||||||
String positionId,
|
|
||||||
String featureVersion,
|
|
||||||
String labelVersion,
|
|
||||||
Instant sampleTime,
|
|
||||||
Map<String, Object> features,
|
|
||||||
Map<String, Object> labels,
|
|
||||||
BigDecimal netReturnBps1x,
|
|
||||||
BigDecimal netReturnBps10x,
|
|
||||||
boolean proxyOnly
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TraderTrainingSample {
|
|
||||||
features = Maps.immutable(features);
|
|
||||||
labels = Maps.immutable(labels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TriggerDecision(
|
|
||||||
boolean pass,
|
|
||||||
BigDecimal signalStrengthScore,
|
|
||||||
String reason,
|
|
||||||
String blocker,
|
|
||||||
Map<String, Object> details
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TriggerDecision {
|
|
||||||
details = Maps.immutable(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean blocked() {
|
|
||||||
return !pass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.quantai.trader.domain;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record TriggerEvent(
|
|
||||||
String runId,
|
|
||||||
String cycleId,
|
|
||||||
String candidateId,
|
|
||||||
String triggerId,
|
|
||||||
Instant triggerTime,
|
|
||||||
String triggerTimeframe,
|
|
||||||
String featureVersion,
|
|
||||||
Map<String, Object> triggerEvidence,
|
|
||||||
Map<String, Object> markout
|
|
||||||
) {
|
|
||||||
|
|
||||||
public TriggerEvent {
|
|
||||||
triggerEvidence = Maps.immutable(triggerEvidence);
|
|
||||||
markout = Maps.immutable(markout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
|
public enum FeedbackSource {
|
||||||
|
REPLAY_SIMULATOR,
|
||||||
|
SHADOW_APP,
|
||||||
|
PAPER_APP,
|
||||||
|
REAL_APP;
|
||||||
|
|
||||||
|
public boolean p0Allowed() {
|
||||||
|
return this == REPLAY_SIMULATOR || this == SHADOW_APP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canBeRealFill() {
|
||||||
|
return this == PAPER_APP || this == REAL_APP;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
|
public enum PositionSide {
|
||||||
|
NONE,
|
||||||
|
LONG,
|
||||||
|
SHORT;
|
||||||
|
|
||||||
|
public boolean isLong() {
|
||||||
|
return this == LONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShort() {
|
||||||
|
return this == SHORT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.quantai.trader.enums;
|
|
||||||
|
|
||||||
public enum ReplayRunStatus {
|
|
||||||
CREATED,
|
|
||||||
RUNNING,
|
|
||||||
CANCEL_REQUESTED,
|
|
||||||
CANCELLED,
|
|
||||||
COMPLETED,
|
|
||||||
FAILED
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,23 @@ package com.quantai.trader.enums;
|
|||||||
|
|
||||||
public enum TraderActionType {
|
public enum TraderActionType {
|
||||||
WAIT,
|
WAIT,
|
||||||
OPEN_INITIAL,
|
OPEN_LONG,
|
||||||
OPEN_PLANNED_LEG,
|
OPEN_SHORT,
|
||||||
|
ADD_LONG,
|
||||||
|
ADD_SHORT,
|
||||||
HOLD,
|
HOLD,
|
||||||
REDUCE,
|
REDUCE_LONG,
|
||||||
|
REDUCE_SHORT,
|
||||||
MOVE_STOP,
|
MOVE_STOP,
|
||||||
CLOSE,
|
CLOSE_LONG,
|
||||||
CANCEL,
|
CLOSE_SHORT,
|
||||||
REQUOTE
|
CANCEL;
|
||||||
|
|
||||||
|
public boolean increasesExposure() {
|
||||||
|
return this == OPEN_LONG || this == OPEN_SHORT || this == ADD_LONG || this == ADD_SHORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean reducesExposure() {
|
||||||
|
return this == REDUCE_LONG || this == REDUCE_SHORT || this == CLOSE_LONG || this == CLOSE_SHORT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package com.quantai.trader.enums;
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
public enum TraderErrorCode {
|
public enum TraderErrorCode {
|
||||||
TRADER_PLAYBOOK_VERSION_CONFLICT,
|
TRADER_DATA_NOT_READY,
|
||||||
TRADER_DATA_SOURCE_MISSING,
|
TRADER_MODEL_ARTIFACT_MISSING,
|
||||||
TRADER_DATA_QUALITY_FAILED,
|
TRADER_CALIBRATION_MISMATCH,
|
||||||
TRADER_ENTRY_PLAN_INCOMPLETE,
|
TRADER_PM_CONFIG_MISMATCH,
|
||||||
TRADER_ILLEGAL_ACTION_TRANSITION,
|
TRADER_MODEL_OUTPUT_INVALID,
|
||||||
TRADER_PLANNED_LEG_AFTER_REDUCE,
|
|
||||||
TRADER_RISK_BLOCKED,
|
TRADER_RISK_BLOCKED,
|
||||||
TRADER_FEEDBACK_DISABLED,
|
TRADER_EXECUTION_BLOCKED,
|
||||||
TRADER_SAMPLE_EXPORT_FAILED
|
TRADER_FEEDBACK_INVALID,
|
||||||
|
TRADER_REQUEST_INVALID,
|
||||||
|
TRADER_P0_MODE_BLOCKED,
|
||||||
|
TRADER_KILL_SWITCH_ACTIVE,
|
||||||
|
TRADER_RUNTIME_CONTROL_BLOCKED,
|
||||||
|
TRADER_OUTBOX_BLOCKED,
|
||||||
|
TRADER_ACTIVE_POINTER_MISMATCH,
|
||||||
|
TRADER_PERSISTENCE_FAILED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
|
public enum TraderExecutionMode {
|
||||||
|
REPLAY_SIM,
|
||||||
|
SHADOW,
|
||||||
|
PAPER,
|
||||||
|
REAL;
|
||||||
|
|
||||||
|
public boolean p0Allowed() {
|
||||||
|
return this == REPLAY_SIM || this == SHADOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.quantai.trader.enums;
|
|
||||||
|
|
||||||
public enum TraderFeedbackSource {
|
|
||||||
MARKET_PROXY,
|
|
||||||
SHADOW_APP,
|
|
||||||
PAPER_APP,
|
|
||||||
REAL_APP
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.quantai.trader.enums;
|
|
||||||
|
|
||||||
public enum TraderFeedbackType {
|
|
||||||
FILL_EVENT,
|
|
||||||
CANCEL_EVENT,
|
|
||||||
CLOSE_EVENT,
|
|
||||||
REJECT_EVENT
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.quantai.trader.enums;
|
|
||||||
|
|
||||||
public enum TraderPlaybookId {
|
|
||||||
BREAKOUT_RETEST_CONTINUATION,
|
|
||||||
SUPPORT_PULLBACK_CONTINUATION,
|
|
||||||
FALSE_BREAK_RECLAIM
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
package com.quantai.trader.enums;
|
package com.quantai.trader.enums;
|
||||||
|
|
||||||
public enum TraderRunMode {
|
public enum TraderRunMode {
|
||||||
REPLAY,
|
REPLAY_SIM,
|
||||||
SHADOW,
|
SHADOW,
|
||||||
PAPER
|
PAPER,
|
||||||
|
REAL;
|
||||||
|
|
||||||
|
public boolean p0Allowed() {
|
||||||
|
return this == REPLAY_SIM || this == SHADOW;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.quantai.trader.enums;
|
|
||||||
|
|
||||||
public enum TraderSide {
|
|
||||||
LONG,
|
|
||||||
SHORT
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.quantai.trader.enums;
|
|
||||||
|
|
||||||
public enum TraderState {
|
|
||||||
CONTEXT_CHECK,
|
|
||||||
SETUP_ARMED,
|
|
||||||
TRIGGER_WAIT,
|
|
||||||
ENTRY_PLANNED,
|
|
||||||
ENTRY_SENT_SHADOW,
|
|
||||||
ENTRY_FILLED_PROXY,
|
|
||||||
ENTRY_MISSED_PROXY,
|
|
||||||
PLANNED_LEG_WAIT,
|
|
||||||
PLANNED_LEG_SENT_SHADOW,
|
|
||||||
PLANNED_LEG_FILLED_PROXY,
|
|
||||||
PLANNED_LEG_MISSED_PROXY,
|
|
||||||
MANAGING,
|
|
||||||
SAMPLE_EXPORTED,
|
|
||||||
BLOCKED
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,30 @@
|
|||||||
package com.quantai.trader.evidence;
|
package com.quantai.trader.evidence;
|
||||||
|
|
||||||
import com.quantai.trader.domain.StageDecision;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderEvidence;
|
import com.quantai.trader.domain.TraderEvidence;
|
||||||
import com.quantai.trader.persistence.TraderEvidenceRepository;
|
|
||||||
import com.quantai.trader.util.Ids;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class EvidenceAppender {
|
public class EvidenceAppender {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class);
|
private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class);
|
||||||
|
private final AtomicLong sequence = new AtomicLong();
|
||||||
private final TraderEvidenceRepository repository;
|
private final TraderEvidenceRepository repository;
|
||||||
|
|
||||||
public EvidenceAppender(TraderEvidenceRepository repository) {
|
public EvidenceAppender(TraderEvidenceRepository repository) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TraderEvidence append(TraderDecisionCycle cycle, String stage, StageDecision decision) {
|
public TraderEvidence append(String runId, String cycleId, String stage, boolean pass, String reason, String blocker, Map<String, Object> details) {
|
||||||
TraderEvidence evidence = new TraderEvidence(
|
TraderEvidence item = new TraderEvidence("evidence_" + cycleId + "_" + sequence.getAndIncrement(), runId, cycleId,
|
||||||
cycle.runId(),
|
stage, pass, reason, blocker, Instant.now(), details);
|
||||||
cycle.cycleId(),
|
repository.insert(item);
|
||||||
Ids.evidenceId(cycle, stage),
|
log.info("event=trader.evidence.appended runId={} cycleId={} stage={} pass={} reason={} blocker={}",
|
||||||
stage,
|
runId, cycleId, stage, pass, reason, blocker);
|
||||||
decision.pass(),
|
return item;
|
||||||
decision.reason(),
|
|
||||||
decision.blocker(),
|
|
||||||
cycle.cycleTime(),
|
|
||||||
decision.details()
|
|
||||||
);
|
|
||||||
repository.insert(evidence);
|
|
||||||
log.info(
|
|
||||||
"event=trader.evidence runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} stage={} pass={} reason={} blocker={}",
|
|
||||||
cycle.runId(),
|
|
||||||
cycle.cycleId(),
|
|
||||||
cycle.symbol(),
|
|
||||||
cycle.playbookId(),
|
|
||||||
cycle.playbookVersion(),
|
|
||||||
cycle.state(),
|
|
||||||
stage,
|
|
||||||
decision.pass(),
|
|
||||||
decision.reason(),
|
|
||||||
decision.blocker()
|
|
||||||
);
|
|
||||||
return evidence;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.quantai.trader.evidence;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.quantai.trader.domain.TraderEvidence;
|
||||||
|
import com.quantai.trader.persistence.TraderJsonCodec;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class JdbcTraderEvidenceRepository implements TraderEvidenceRepository {
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final TraderJsonCodec jsonCodec;
|
||||||
|
|
||||||
|
public JdbcTraderEvidenceRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.jsonCodec = new TraderJsonCodec(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insert(TraderEvidence evidence) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into trader_evidence
|
||||||
|
(run_id, cycle_id, evidence_id, stage, pass, reason, blocker, evidence_time, details_json)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
evidence.runId(), evidence.cycleId(), evidence.evidenceId(), evidence.stage(), evidence.pass(),
|
||||||
|
evidence.reason(), evidence.blocker(), Timestamp.from(evidence.evidenceTime()),
|
||||||
|
jsonCodec.toJson(evidence.detailsJson()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-6
@@ -1,12 +1,7 @@
|
|||||||
package com.quantai.trader.persistence;
|
package com.quantai.trader.evidence;
|
||||||
|
|
||||||
import com.quantai.trader.domain.TraderEvidence;
|
import com.quantai.trader.domain.TraderEvidence;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface TraderEvidenceRepository {
|
public interface TraderEvidenceRepository {
|
||||||
|
|
||||||
void insert(TraderEvidence evidence);
|
void insert(TraderEvidence evidence);
|
||||||
|
|
||||||
List<TraderEvidence> findByCycleId(String runId, String cycleId);
|
|
||||||
}
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.quantai.trader.execution;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.ExecutionDecision;
|
|
||||||
import com.quantai.trader.domain.TraderEntryPlan;
|
|
||||||
import com.quantai.trader.domain.TraderMarketSnapshot;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class ExecutionQualityGate {
|
|
||||||
|
|
||||||
public ExecutionDecision evaluate(TraderMarketSnapshot snapshot, TraderEntryPlan entryPlan) {
|
|
||||||
BigDecimal score = entryPlan.executionQualityScore() == null
|
|
||||||
? new BigDecimal("0.50")
|
|
||||||
: entryPlan.executionQualityScore();
|
|
||||||
if (!entryPlan.completeForEntry()) {
|
|
||||||
return new ExecutionDecision(false, score, "ENTRY_PLAN_INCOMPLETE", "TRADER_ENTRY_PLAN_INCOMPLETE", Map.of());
|
|
||||||
}
|
|
||||||
if (score.compareTo(new BigDecimal("0.20")) < 0) {
|
|
||||||
return new ExecutionDecision(false, score, "EXECUTION_QUALITY_TOO_LOW", "TRADER_RISK_BLOCKED", Map.of(
|
|
||||||
"executionQualityScore", score
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return new ExecutionDecision(true, score, "EXECUTION_PROXY_PASS", null, Map.of(
|
|
||||||
"executionQualityScore", score,
|
|
||||||
"proxyOnly", true
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package com.quantai.trader.execution;
|
|
||||||
|
|
||||||
import com.quantai.trader.domain.PlaybookCandidate;
|
|
||||||
import com.quantai.trader.domain.PositionSizingPlan;
|
|
||||||
import com.quantai.trader.domain.TraderDecisionCycle;
|
|
||||||
import com.quantai.trader.domain.TraderEntryPlan;
|
|
||||||
import com.quantai.trader.domain.TraderException;
|
|
||||||
import com.quantai.trader.domain.TraderPositionPath;
|
|
||||||
import com.quantai.trader.domain.TraderPricePlan;
|
|
||||||
import com.quantai.trader.domain.TriggerDecision;
|
|
||||||
import com.quantai.trader.enums.TraderActionType;
|
|
||||||
import com.quantai.trader.enums.TraderErrorCode;
|
|
||||||
import com.quantai.trader.risk.TraderPositionSizer;
|
|
||||||
import com.quantai.trader.util.Ids;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class TraderEntryPlanner {
|
|
||||||
|
|
||||||
private final TraderPositionSizer positionSizer;
|
|
||||||
|
|
||||||
public TraderEntryPlanner(TraderPositionSizer positionSizer) {
|
|
||||||
this.positionSizer = positionSizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TraderEntryPlan planInitialEntry(
|
|
||||||
TraderDecisionCycle cycle,
|
|
||||||
PlaybookCandidate candidate,
|
|
||||||
TriggerDecision trigger
|
|
||||||
) {
|
|
||||||
TraderPricePlan pricePlan = candidate.pricePlan();
|
|
||||||
validatePricePlan(pricePlan);
|
|
||||||
PositionSizingPlan sizingPlan = positionSizer.sizeInitialPlan(cycle, candidate, trigger, pricePlan);
|
|
||||||
return new TraderEntryPlan(
|
|
||||||
cycle.runId(),
|
|
||||||
cycle.cycleId(),
|
|
||||||
Ids.actionId(cycle, 1),
|
|
||||||
Ids.entryLegId(cycle, 0),
|
|
||||||
candidate.candidateId(),
|
|
||||||
TraderActionType.OPEN_INITIAL,
|
|
||||||
0,
|
|
||||||
sizingPlan.plannedLegCount(),
|
|
||||||
sizingPlan.initialLegRatio(),
|
|
||||||
sizingPlan.sizingMethod(),
|
|
||||||
sizingPlan.signalStrengthScore(),
|
|
||||||
sizingPlan.executionQualityScore(),
|
|
||||||
sizingPlan.riskGateScore(),
|
|
||||||
pricePlan.entryPrice(),
|
|
||||||
pricePlan.invalidPrice(),
|
|
||||||
pricePlan.stopPrice(),
|
|
||||||
pricePlan.targetPrice(),
|
|
||||||
pricePlan.partialTakeProfitPrice(),
|
|
||||||
pricePlan.maxEntryWaitMs(),
|
|
||||||
pricePlan.maxHoldMs(),
|
|
||||||
"INITIAL_ENTRY_FROM_PLAYBOOK_TRIGGER"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<TraderEntryPlan> planNextDeclaredLeg(
|
|
||||||
TraderDecisionCycle cycle,
|
|
||||||
PlaybookCandidate candidate,
|
|
||||||
TraderPositionPath path
|
|
||||||
) {
|
|
||||||
if (path == null || !path.opened() || path.reduceSeen() || path.fullSize()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
int nextIndex = path.filledLegCount();
|
|
||||||
if (nextIndex >= candidate.maxPlannedEntryLegs()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
TraderPricePlan pricePlan = candidate.pricePlan();
|
|
||||||
validatePricePlan(pricePlan);
|
|
||||||
PositionSizingPlan sizingPlan = positionSizer.sizeNextPlannedLeg(cycle, candidate, path, pricePlan, nextIndex);
|
|
||||||
if (sizingPlan.nextLegRatio().compareTo(BigDecimal.ZERO) <= 0) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(new TraderEntryPlan(
|
|
||||||
cycle.runId(),
|
|
||||||
cycle.cycleId(),
|
|
||||||
Ids.actionId(cycle, nextIndex + 1),
|
|
||||||
Ids.entryLegId(cycle, nextIndex),
|
|
||||||
candidate.candidateId(),
|
|
||||||
TraderActionType.OPEN_PLANNED_LEG,
|
|
||||||
nextIndex,
|
|
||||||
sizingPlan.plannedLegCount(),
|
|
||||||
sizingPlan.nextLegRatio(),
|
|
||||||
sizingPlan.sizingMethod(),
|
|
||||||
sizingPlan.signalStrengthScore(),
|
|
||||||
sizingPlan.executionQualityScore(),
|
|
||||||
sizingPlan.riskGateScore(),
|
|
||||||
pricePlan.entryPrice(),
|
|
||||||
pricePlan.invalidPrice(),
|
|
||||||
pricePlan.stopPrice(),
|
|
||||||
pricePlan.targetPrice(),
|
|
||||||
pricePlan.partialTakeProfitPrice(),
|
|
||||||
pricePlan.maxEntryWaitMs(),
|
|
||||||
pricePlan.maxHoldMs(),
|
|
||||||
"DECLARED_PLANNED_ENTRY_LEG"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validatePricePlan(TraderPricePlan pricePlan) {
|
|
||||||
if (pricePlan == null || !pricePlan.completeForEntry()) {
|
|
||||||
throw new TraderException(
|
|
||||||
TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE,
|
|
||||||
"entry/invalid/stop/target/maxHold are required before an entry action"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.quantai.trader.feature;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.quantai.trader.artifact.TraderArtifactBundle;
|
||||||
|
import com.quantai.trader.artifact.TraderModelManifest;
|
||||||
|
import com.quantai.trader.config.TraderProperties;
|
||||||
|
import com.quantai.trader.domain.TraderException;
|
||||||
|
import com.quantai.trader.domain.TraderMarketSnapshot;
|
||||||
|
import com.quantai.trader.enums.TraderErrorCode;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TraderFeatureVectorBuilder {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TraderFeatureVectorBuilder.class);
|
||||||
|
private static final int REQUIRED_FEATURE_COUNT = 54;
|
||||||
|
|
||||||
|
private final TraderProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final ConcurrentMap<String, List<String>> featureOrderCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public TraderFeatureVectorBuilder(TraderProperties properties, ObjectMapper objectMapper) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] build(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle) {
|
||||||
|
TraderModelManifest referenceManifest = referenceManifest(bundle);
|
||||||
|
if (!referenceManifest.featureVersion().equals(snapshot.featureVersion())) {
|
||||||
|
log.warn("event=trader.features.version_mismatch runId={} cycleId={} snapshotFeatureVersion={} modelFeatureVersion={}",
|
||||||
|
snapshot.runId(), snapshot.cycleId(), snapshot.featureVersion(), referenceManifest.featureVersion());
|
||||||
|
throw modelInputException("snapshot feature version does not match model feature version: "
|
||||||
|
+ snapshot.featureVersion() + " != " + referenceManifest.featureVersion());
|
||||||
|
}
|
||||||
|
List<String> featureOrder = featureOrder(referenceManifest);
|
||||||
|
rejectUnexpectedFeatures(snapshot, featureOrder);
|
||||||
|
float[] values = new float[featureOrder.size()];
|
||||||
|
for (int index = 0; index < featureOrder.size(); index++) {
|
||||||
|
String featureName = featureOrder.get(index);
|
||||||
|
Object rawValue = snapshot.featureJson().get(featureName);
|
||||||
|
values[index] = toFloat(snapshot, rawValue, featureName, index);
|
||||||
|
}
|
||||||
|
log.debug("event=trader.features.vector_built runId={} cycleId={} featureVersion={} featureCount={} featureOrderHash={}",
|
||||||
|
snapshot.runId(), snapshot.cycleId(), snapshot.featureVersion(), values.length, referenceManifest.featureOrderHash());
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> featureOrder(TraderArtifactBundle bundle) {
|
||||||
|
return featureOrder(referenceManifest(bundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TraderModelManifest referenceManifest(TraderArtifactBundle bundle) {
|
||||||
|
return bundle.modelManifests().stream()
|
||||||
|
.filter(manifest -> "DIRECTION".equals(manifest.modelType()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> modelInputException("DIRECTION model manifest is required for feature order"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> featureOrder(TraderModelManifest manifest) {
|
||||||
|
String cacheKey = manifest.featureOrderHash() + "|" + manifest.featureOrderPath();
|
||||||
|
// 特征顺序是模型包契约的一部分,按 hash 缓存,避免每轮重复读文件。
|
||||||
|
return featureOrderCache.computeIfAbsent(cacheKey, ignored -> readFeatureOrder(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> readFeatureOrder(TraderModelManifest manifest) {
|
||||||
|
Path path = Path.of(properties.artifact().artifactRoot()).resolve(manifest.featureOrderPath());
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
throw modelInputException("feature_order.json is missing: " + path);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(path.toFile());
|
||||||
|
if (!root.isArray() || root.size() != REQUIRED_FEATURE_COUNT) {
|
||||||
|
throw modelInputException("feature_order.json must contain exactly " + REQUIRED_FEATURE_COUNT + " fields: " + path);
|
||||||
|
}
|
||||||
|
List<String> order = StreamSupport.stream(root.spliterator(), false)
|
||||||
|
.map(JsonNode::asText)
|
||||||
|
.toList();
|
||||||
|
Set<String> unique = new LinkedHashSet<>(order);
|
||||||
|
if (unique.size() != order.size() || order.stream().anyMatch(String::isBlank)) {
|
||||||
|
throw modelInputException("feature_order.json contains duplicate or blank feature names: " + path);
|
||||||
|
}
|
||||||
|
log.info("event=trader.features.order_loaded featureOrderPath={} featureOrderHash={} featureCount={}",
|
||||||
|
manifest.featureOrderPath(), manifest.featureOrderHash(), order.size());
|
||||||
|
return order;
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw modelInputException("feature_order.json cannot be read: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectUnexpectedFeatures(TraderMarketSnapshot snapshot, List<String> featureOrder) {
|
||||||
|
Set<String> allowed = Set.copyOf(featureOrder);
|
||||||
|
List<String> unexpected = snapshot.featureJson().keySet().stream()
|
||||||
|
.filter(key -> !allowed.contains(key))
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
if (!unexpected.isEmpty()) {
|
||||||
|
log.warn("event=trader.features.unexpected_fields runId={} cycleId={} unexpectedFields={}",
|
||||||
|
snapshot.runId(), snapshot.cycleId(), unexpected);
|
||||||
|
throw modelInputException("snapshot featureJson contains fields outside feature_order.json: " + unexpected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float toFloat(TraderMarketSnapshot snapshot, Object rawValue, String featureName, int index) {
|
||||||
|
if (rawValue == null) {
|
||||||
|
log.warn("event=trader.features.missing runId={} cycleId={} featureIndex={} featureName={}",
|
||||||
|
snapshot.runId(), snapshot.cycleId(), index + 1, featureName);
|
||||||
|
throw modelInputException("snapshot feature is missing: " + featureName);
|
||||||
|
}
|
||||||
|
double value;
|
||||||
|
if (rawValue instanceof BigDecimal decimal) {
|
||||||
|
value = decimal.doubleValue();
|
||||||
|
} else if (rawValue instanceof Number number) {
|
||||||
|
value = number.doubleValue();
|
||||||
|
} else {
|
||||||
|
log.warn("event=trader.features.non_numeric runId={} cycleId={} featureIndex={} featureName={} valueType={}",
|
||||||
|
snapshot.runId(), snapshot.cycleId(), index + 1, featureName, rawValue.getClass().getName());
|
||||||
|
throw modelInputException("snapshot feature must be numeric: " + featureName);
|
||||||
|
}
|
||||||
|
if (!Double.isFinite(value)) {
|
||||||
|
log.warn("event=trader.features.non_finite runId={} cycleId={} featureIndex={} featureName={} value={}",
|
||||||
|
snapshot.runId(), snapshot.cycleId(), index + 1, featureName, value);
|
||||||
|
throw modelInputException("snapshot feature must be finite: " + featureName);
|
||||||
|
}
|
||||||
|
return (float) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TraderException modelInputException(String message) {
|
||||||
|
return new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.quantai.trader.feedback;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.quantai.trader.domain.TraderAppFeedback;
|
||||||
|
import com.quantai.trader.persistence.TraderJsonCodec;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class JdbcTraderFeedbackRepository implements TraderFeedbackRepository {
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final TraderJsonCodec jsonCodec;
|
||||||
|
|
||||||
|
public JdbcTraderFeedbackRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.jsonCodec = new TraderJsonCodec(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insert(TraderAppFeedback feedback) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into trader_app_feedback
|
||||||
|
(run_id, cycle_id, feedback_id, action_id, feedback_source, is_real_fill,
|
||||||
|
order_id, order_status, app_received_time, exchange_ack_time, filled_time,
|
||||||
|
filled_price, filled_quantity, fee, slippage_bps, reject_reason, raw_feedback_json)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
feedback.runId(), feedback.cycleId(), feedback.feedbackId(), feedback.actionId(),
|
||||||
|
feedback.feedbackSource().name(), feedback.realFill(), feedback.orderId(), feedback.orderStatus(),
|
||||||
|
feedback.appReceivedTime() == null ? null : Timestamp.from(feedback.appReceivedTime()),
|
||||||
|
feedback.exchangeAckTime() == null ? null : Timestamp.from(feedback.exchangeAckTime()),
|
||||||
|
feedback.filledTime() == null ? null : Timestamp.from(feedback.filledTime()),
|
||||||
|
feedback.filledPrice(), feedback.filledQuantity(), feedback.fee(), feedback.slippageBps(),
|
||||||
|
feedback.rejectReason(), jsonCodec.toJson(feedback.rawFeedbackJson()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.quantai.trader.feedback;
|
||||||
|
|
||||||
|
import com.quantai.trader.domain.TraderAppFeedback;
|
||||||
|
|
||||||
|
public interface TraderFeedbackRepository {
|
||||||
|
void insert(TraderAppFeedback feedback);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.quantai.trader.infrastructure.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("trader_evidence")
|
|
||||||
public class TraderEvidenceEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String runId;
|
|
||||||
private String cycleId;
|
|
||||||
private String evidenceId;
|
|
||||||
private String stage;
|
|
||||||
private Boolean pass;
|
|
||||||
private String reason;
|
|
||||||
private String blocker;
|
|
||||||
private LocalDateTime evidenceTime;
|
|
||||||
private String detailsJson;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
package com.quantai.trader.infrastructure.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("trader_playbook_definition")
|
|
||||||
public class TraderPlaybookDefinitionEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String playbookId;
|
|
||||||
private String playbookVersion;
|
|
||||||
private String family;
|
|
||||||
private String variant;
|
|
||||||
private String sideMode;
|
|
||||||
private String sourcePath;
|
|
||||||
private String definitionHashSha256;
|
|
||||||
private String definitionJson;
|
|
||||||
private LocalDateTime loadedAt;
|
|
||||||
private String status;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.quantai.trader.infrastructure.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("trader_replay_report")
|
|
||||||
public class TraderReplayReportEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String runId;
|
|
||||||
private String reportId;
|
|
||||||
private String symbol;
|
|
||||||
private String playbookId;
|
|
||||||
private String playbookVersion;
|
|
||||||
private Integer candidateEvents;
|
|
||||||
private Integer monthsCovered;
|
|
||||||
@TableField("base_net_return_bps_1x")
|
|
||||||
private BigDecimal baseNetReturnBps1x;
|
|
||||||
@TableField("leveraged_net_return_bps_10x")
|
|
||||||
private BigDecimal leveragedNetReturnBps10x;
|
|
||||||
@TableField("holdout_return_bps_10x")
|
|
||||||
private BigDecimal holdoutReturnBps10x;
|
|
||||||
private String strictVsLooseJson;
|
|
||||||
private String failureRisksJson;
|
|
||||||
private String conclusion;
|
|
||||||
private String reportPath;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.quantai.trader.infrastructure.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("trader_replay_run")
|
|
||||||
public class TraderReplayRunEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String runId;
|
|
||||||
private String runMode;
|
|
||||||
private String symbol;
|
|
||||||
private String playbookId;
|
|
||||||
private String playbookVersion;
|
|
||||||
private String playbookDefinitionHash;
|
|
||||||
private LocalDateTime dataFrom;
|
|
||||||
private LocalDateTime dataTo;
|
|
||||||
private String featureVersion;
|
|
||||||
private String labelVersion;
|
|
||||||
private String dataSourceManifestJson;
|
|
||||||
private String status;
|
|
||||||
private String configJson;
|
|
||||||
private LocalDateTime startedAt;
|
|
||||||
private LocalDateTime finishedAt;
|
|
||||||
private String failureReason;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user