From 5d210053d0537a7ef3ce9ed80092376250adb6f5 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 21:53:22 +0800 Subject: [PATCH] Rewrite trader service for V4 P0 --- pom.xml | 58 +- .../trader/QuantTraderServiceApplication.java | 2 - .../trader/artifact/TraderArtifactBundle.java | 21 + .../trader/artifact/TraderArtifactLoader.java | 71 +++ .../brain/LifecycleActionValueService.java | 27 - .../trader/brain/PlaybookCandidateEngine.java | 85 --- .../trader/brain/TraderContextGate.java | 26 - .../trader/brain/TraderCycleResult.java | 14 - .../brain/TraderDecisionCycleRunner.java | 165 ------ .../trader/brain/TraderLifecycleResult.java | 12 - .../trader/brain/TriggerMarkoutService.java | 39 -- .../trader/config/TraderProperties.java | 331 +++--------- .../trader/controller/TraderApiError.java | 7 +- .../controller/TraderApiExceptionHandler.java | 10 +- .../controller/TraderFeedbackController.java | 39 +- .../controller/TraderFeedbackRequest.java | 26 - .../controller/TraderHealthController.java | 23 +- .../controller/TraderPlaybookController.java | 52 -- .../controller/TraderReplayController.java | 48 +- .../core/TraderCoreDecisionRequest.java | 23 + .../core/TraderCoreDecisionResponse.java | 19 + .../trader/core/TraderCoreFeedbackEvent.java | 24 + .../quantai/trader/core/TraderCoreHealth.java | 11 + .../quantai/trader/domain/ContinueOutput.java | 28 + .../trader/domain/DirectionOutput.java | 34 ++ .../quantai/trader/domain/EntryOutput.java | 40 ++ .../trader/domain/ExecutionDecision.java | 21 - .../com/quantai/trader/domain/ExitOutput.java | 32 ++ .../trader/domain/FeedbackValidator.java | 14 + .../trader/domain/ManagementDecision.java | 16 - .../java/com/quantai/trader/domain/Maps.java | 17 - .../quantai/trader/domain/OpenOrderState.java | 10 + .../trader/domain/PlaybookCandidate.java | 25 - .../trader/domain/PositionManagerInput.java | 23 + .../trader/domain/PositionSizingPlan.java | 14 - .../quantai/trader/domain/RiskDecision.java | 20 - .../com/quantai/trader/domain/RiskOutput.java | 38 ++ .../quantai/trader/domain/StageDecision.java | 27 - .../trader/domain/TraderAccountState.java | 27 + .../quantai/trader/domain/TraderAction.java | 50 +- .../trader/domain/TraderActionFactory.java | 44 ++ .../trader/domain/TraderAppFeedback.java | 39 +- .../domain/TraderDataSourceManifest.java | 48 -- .../trader/domain/TraderDecisionCycle.java | 38 +- .../trader/domain/TraderEntryPlan.java | 39 -- .../quantai/trader/domain/TraderEvidence.java | 16 +- .../trader/domain/TraderException.java | 11 +- .../trader/domain/TraderExecutionState.java | 46 ++ .../trader/domain/TraderManagementAction.java | 26 - .../trader/domain/TraderMarketEvent.java | 19 - .../trader/domain/TraderMarketSnapshot.java | 43 +- .../trader/domain/TraderModelManifest.java | 20 - .../trader/domain/TraderModelOutput.java | 35 +- .../quantai/trader/domain/TraderPmConfig.java | 130 +++++ .../trader/domain/TraderPositionLeg.java | 21 - .../domain/TraderPositionManagerDecision.java | 48 ++ .../trader/domain/TraderPositionPath.java | 43 -- .../trader/domain/TraderPositionState.java | 47 ++ .../trader/domain/TraderPricePlan.java | 23 - .../trader/domain/TraderReplayReport.java | 30 -- .../trader/domain/TraderRiskDecision.java | 36 +- .../trader/domain/TraderTrainingSample.java | 27 - .../trader/domain/TriggerDecision.java | 21 - .../quantai/trader/domain/TriggerEvent.java | 22 - .../quantai/trader/enums/FeedbackSource.java | 16 + .../quantai/trader/enums/PositionSide.java | 15 + .../quantai/trader/enums/ReplayRunStatus.java | 10 - .../trader/enums/TraderActionType.java | 23 +- .../quantai/trader/enums/TraderErrorCode.java | 18 +- .../trader/enums/TraderExecutionMode.java | 12 + .../trader/enums/TraderFeedbackSource.java | 8 - .../trader/enums/TraderFeedbackType.java | 8 - .../trader/enums/TraderPlaybookId.java | 7 - .../quantai/trader/enums/TraderRunMode.java | 9 +- .../com/quantai/trader/enums/TraderSide.java | 6 - .../com/quantai/trader/enums/TraderState.java | 18 - .../trader/evidence/EvidenceAppender.java | 51 +- .../execution/ExecutionQualityGate.java | 31 -- .../trader/execution/TraderEntryPlanner.java | 113 ---- .../entity/TraderEvidenceEntity.java | 25 - .../TraderPlaybookDefinitionEntity.java | 27 - .../entity/TraderReplayReportEntity.java | 35 -- .../entity/TraderReplayRunEntity.java | 33 -- .../entity/TraderRiskDecisionEntity.java | 36 -- .../entity/TraderTrainingSampleEntity.java | 33 -- .../mapper/TraderEvidenceMapper.java | 7 - .../TraderPlaybookDefinitionMapper.java | 7 - .../mapper/TraderReplayReportMapper.java | 7 - .../mapper/TraderReplayRunMapper.java | 7 - .../mapper/TraderRiskDecisionMapper.java | 7 - .../mapper/TraderTrainingSampleMapper.java | 7 - .../trader/market/SnapshotBuilder.java | 46 -- .../DeterministicTraderModelService.java | 50 ++ .../com/quantai/trader/model/TraderModel.java | 9 - .../trader/model/TraderModelService.java | 9 + .../outbox/InMemoryOutboxRepository.java | 30 ++ .../trader/outbox/TraderOutboxEvent.java | 34 ++ .../MybatisReplayReportRepository.java | 75 --- .../MybatisReplayRunRepository.java | 88 ---- .../MybatisTraderEvidenceRepository.java | 61 --- ...tisTraderPlaybookDefinitionRepository.java | 83 --- .../MybatisTraderRiskDecisionRepository.java | 76 --- .../MybatisTraderSampleRepository.java | 68 --- .../persistence/ReplayReportRepository.java | 12 - .../persistence/ReplayRunRepository.java | 14 - .../persistence/TraderEvidenceRepository.java | 12 - .../persistence/TraderPersistenceCodec.java | 86 --- .../TraderPlaybookDefinitionRepository.java | 12 - .../TraderRiskDecisionRepository.java | 12 - .../persistence/TraderSampleRepository.java | 12 - .../trader/playbook/RuleDefinition.java | 19 - .../playbook/TraderPlaybookCatalog.java | 140 ----- .../playbook/TraderPlaybookDefinition.java | 54 -- .../TraderPlaybookDefinitionSnapshot.java | 17 - .../TraderPlaybookValidationException.java | 8 - .../playbook/TraderPlaybookValidator.java | 88 ---- .../position/TraderPositionManager.java | 399 +++++++------- .../CryptoLakeReplayCsvMarketEventReader.java | 498 ------------------ .../quantai/trader/replay/DataSourceSpec.java | 22 - .../replay/JsonlReplayMarketEventReader.java | 114 ---- .../trader/replay/ReplayClockTick.java | 34 -- .../trader/replay/ReplayMarketEvent.java | 15 + .../replay/ReplayMarketEventReader.java | 12 - .../com/quantai/trader/replay/ReplayRun.java | 44 -- .../trader/replay/ReplayRunConfig.java | 34 -- .../trader/replay/ReplayRunResponse.java | 9 - .../trader/replay/ReplayRunService.java | 225 -------- .../trader/replay/TraderCycleResult.java | 14 + .../trader/replay/TraderP0CycleRunner.java | 108 ++++ .../trader/report/ReplayReportWriter.java | 153 ------ .../quantai/trader/risk/RiskGateInput.java | 13 + .../com/quantai/trader/risk/RiskLimits.java | 14 + .../trader/risk/TraderPositionSizer.java | 136 ----- .../quantai/trader/risk/TraderRiskGate.java | 186 ++----- .../trader/runtime/P0RuntimeGuard.java | 40 ++ .../runtime/StartupValidationRunner.java | 19 + .../trader/sample/TrainingLabelSet.java | 12 - .../trader/sample/TrainingSampleExporter.java | 114 ---- .../trader/sample/TriggerMarkoutLabeler.java | 90 ---- .../state/TraderDecisionCycleFactory.java | 31 -- .../trader/state/TraderRuntimeState.java | 11 - .../trader/state/TraderStateMachine.java | 182 ------- .../java/com/quantai/trader/util/Ids.java | 66 --- .../quantai/trader/util/TraderNumbers.java | 73 +++ src/main/resources/application.yml | 71 +-- .../db/migration/V1__trader_p0_schema.sql | 441 ---------------- .../db/migration/V1__trader_v4_p0_schema.sql | 234 ++++++++ .../V2__trader_persistence_id_lengths.sql | 8 - .../breakout-retest-intraday-5m-60m.yml | 61 --- .../QuantTraderServiceApplicationTest.java | 12 - .../java/com/quantai/trader/TestFixtures.java | 302 ++++++----- .../artifact/TraderArtifactLoaderTest.java | 38 ++ .../brain/TraderDecisionCycleRunnerTest.java | 90 ---- .../controller/TraderControllerTest.java | 175 +++--- .../trader/core/TraderCoreDtoTest.java | 48 ++ .../domain/ModelOutputContractTest.java | 69 +++ .../trader/domain/TraderAppFeedbackTest.java | 53 -- .../domain/TraderDataSourceManifestTest.java | 51 -- .../trader/evidence/EvidenceAppenderTest.java | 45 +- .../execution/TraderEntryPlannerTest.java | 67 --- .../outbox/InMemoryOutboxRepositoryTest.java | 40 ++ .../MybatisReplayPersistenceTest.java | 102 ---- ...raderPlaybookDefinitionRepositoryTest.java | 97 ---- .../playbook/TraderPlaybookValidatorTest.java | 93 ---- .../position/TraderPositionManagerTest.java | 117 ++-- .../quality/NoLegacyTraderTermsTest.java | 48 ++ ...ptoLakeReplayCsvMarketEventReaderTest.java | 76 --- .../replay/TraderP0CycleRunnerTest.java | 68 +++ .../TraderReplayFixtureAcceptanceTest.java | 121 ----- .../trader/risk/TraderPositionSizerTest.java | 50 -- .../trader/risk/TraderRiskGateTest.java | 100 ++-- .../trader/runtime/P0RuntimeGuardTest.java | 38 ++ .../sample/TrainingSampleExporterTest.java | 51 -- .../trader/state/TraderStateMachineTest.java | 70 --- src/test/resources/application.yml | 39 +- src/test/resources/replay-fixtures/README.md | 22 - .../crypto-lake-candidate-events-mini.csv | 3 - .../crypto-lake-replay-mini.csv | 19 - .../false-breakout-trigger-wait.jsonl | 1 - .../incomplete-entry-plan-hard-fail.jsonl | 1 - .../missing-features-data-quality.jsonl | 1 - .../sideways-range-no-setup.jsonl | 1 - .../replay-fixtures/trend-down-no-setup.jsonl | 1 - .../trend-up-breakout-happy.jsonl | 1 - 184 files changed, 2780 insertions(+), 6945 deletions(-) create mode 100644 src/main/java/com/quantai/trader/artifact/TraderArtifactBundle.java create mode 100644 src/main/java/com/quantai/trader/artifact/TraderArtifactLoader.java delete mode 100644 src/main/java/com/quantai/trader/brain/LifecycleActionValueService.java delete mode 100644 src/main/java/com/quantai/trader/brain/PlaybookCandidateEngine.java delete mode 100644 src/main/java/com/quantai/trader/brain/TraderContextGate.java delete mode 100644 src/main/java/com/quantai/trader/brain/TraderCycleResult.java delete mode 100644 src/main/java/com/quantai/trader/brain/TraderDecisionCycleRunner.java delete mode 100644 src/main/java/com/quantai/trader/brain/TraderLifecycleResult.java delete mode 100644 src/main/java/com/quantai/trader/brain/TriggerMarkoutService.java delete mode 100644 src/main/java/com/quantai/trader/controller/TraderFeedbackRequest.java delete mode 100644 src/main/java/com/quantai/trader/controller/TraderPlaybookController.java create mode 100644 src/main/java/com/quantai/trader/core/TraderCoreDecisionRequest.java create mode 100644 src/main/java/com/quantai/trader/core/TraderCoreDecisionResponse.java create mode 100644 src/main/java/com/quantai/trader/core/TraderCoreFeedbackEvent.java create mode 100644 src/main/java/com/quantai/trader/core/TraderCoreHealth.java create mode 100644 src/main/java/com/quantai/trader/domain/ContinueOutput.java create mode 100644 src/main/java/com/quantai/trader/domain/DirectionOutput.java create mode 100644 src/main/java/com/quantai/trader/domain/EntryOutput.java delete mode 100644 src/main/java/com/quantai/trader/domain/ExecutionDecision.java create mode 100644 src/main/java/com/quantai/trader/domain/ExitOutput.java create mode 100644 src/main/java/com/quantai/trader/domain/FeedbackValidator.java delete mode 100644 src/main/java/com/quantai/trader/domain/ManagementDecision.java delete mode 100644 src/main/java/com/quantai/trader/domain/Maps.java create mode 100644 src/main/java/com/quantai/trader/domain/OpenOrderState.java delete mode 100644 src/main/java/com/quantai/trader/domain/PlaybookCandidate.java create mode 100644 src/main/java/com/quantai/trader/domain/PositionManagerInput.java delete mode 100644 src/main/java/com/quantai/trader/domain/PositionSizingPlan.java delete mode 100644 src/main/java/com/quantai/trader/domain/RiskDecision.java create mode 100644 src/main/java/com/quantai/trader/domain/RiskOutput.java delete mode 100644 src/main/java/com/quantai/trader/domain/StageDecision.java create mode 100644 src/main/java/com/quantai/trader/domain/TraderAccountState.java create mode 100644 src/main/java/com/quantai/trader/domain/TraderActionFactory.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderDataSourceManifest.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderEntryPlan.java create mode 100644 src/main/java/com/quantai/trader/domain/TraderExecutionState.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderManagementAction.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderMarketEvent.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderModelManifest.java create mode 100644 src/main/java/com/quantai/trader/domain/TraderPmConfig.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderPositionLeg.java create mode 100644 src/main/java/com/quantai/trader/domain/TraderPositionManagerDecision.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderPositionPath.java create mode 100644 src/main/java/com/quantai/trader/domain/TraderPositionState.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderPricePlan.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderReplayReport.java delete mode 100644 src/main/java/com/quantai/trader/domain/TraderTrainingSample.java delete mode 100644 src/main/java/com/quantai/trader/domain/TriggerDecision.java delete mode 100644 src/main/java/com/quantai/trader/domain/TriggerEvent.java create mode 100644 src/main/java/com/quantai/trader/enums/FeedbackSource.java create mode 100644 src/main/java/com/quantai/trader/enums/PositionSide.java delete mode 100644 src/main/java/com/quantai/trader/enums/ReplayRunStatus.java create mode 100644 src/main/java/com/quantai/trader/enums/TraderExecutionMode.java delete mode 100644 src/main/java/com/quantai/trader/enums/TraderFeedbackSource.java delete mode 100644 src/main/java/com/quantai/trader/enums/TraderFeedbackType.java delete mode 100644 src/main/java/com/quantai/trader/enums/TraderPlaybookId.java delete mode 100644 src/main/java/com/quantai/trader/enums/TraderSide.java delete mode 100644 src/main/java/com/quantai/trader/enums/TraderState.java delete mode 100644 src/main/java/com/quantai/trader/execution/ExecutionQualityGate.java delete mode 100644 src/main/java/com/quantai/trader/execution/TraderEntryPlanner.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/entity/TraderEvidenceEntity.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/entity/TraderPlaybookDefinitionEntity.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayReportEntity.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayRunEntity.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/entity/TraderRiskDecisionEntity.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/entity/TraderTrainingSampleEntity.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/mapper/TraderEvidenceMapper.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/mapper/TraderPlaybookDefinitionMapper.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayReportMapper.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayRunMapper.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/mapper/TraderRiskDecisionMapper.java delete mode 100644 src/main/java/com/quantai/trader/infrastructure/mapper/TraderTrainingSampleMapper.java delete mode 100644 src/main/java/com/quantai/trader/market/SnapshotBuilder.java create mode 100644 src/main/java/com/quantai/trader/model/DeterministicTraderModelService.java delete mode 100644 src/main/java/com/quantai/trader/model/TraderModel.java create mode 100644 src/main/java/com/quantai/trader/model/TraderModelService.java create mode 100644 src/main/java/com/quantai/trader/outbox/InMemoryOutboxRepository.java create mode 100644 src/main/java/com/quantai/trader/outbox/TraderOutboxEvent.java delete mode 100644 src/main/java/com/quantai/trader/persistence/MybatisReplayReportRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/MybatisReplayRunRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/MybatisTraderEvidenceRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/MybatisTraderPlaybookDefinitionRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/MybatisTraderRiskDecisionRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/MybatisTraderSampleRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/ReplayReportRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/ReplayRunRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/TraderEvidenceRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/TraderPersistenceCodec.java delete mode 100644 src/main/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/TraderRiskDecisionRepository.java delete mode 100644 src/main/java/com/quantai/trader/persistence/TraderSampleRepository.java delete mode 100644 src/main/java/com/quantai/trader/playbook/RuleDefinition.java delete mode 100644 src/main/java/com/quantai/trader/playbook/TraderPlaybookCatalog.java delete mode 100644 src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinition.java delete mode 100644 src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinitionSnapshot.java delete mode 100644 src/main/java/com/quantai/trader/playbook/TraderPlaybookValidationException.java delete mode 100644 src/main/java/com/quantai/trader/playbook/TraderPlaybookValidator.java delete mode 100644 src/main/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReader.java delete mode 100644 src/main/java/com/quantai/trader/replay/DataSourceSpec.java delete mode 100644 src/main/java/com/quantai/trader/replay/JsonlReplayMarketEventReader.java delete mode 100644 src/main/java/com/quantai/trader/replay/ReplayClockTick.java create mode 100644 src/main/java/com/quantai/trader/replay/ReplayMarketEvent.java delete mode 100644 src/main/java/com/quantai/trader/replay/ReplayMarketEventReader.java delete mode 100644 src/main/java/com/quantai/trader/replay/ReplayRun.java delete mode 100644 src/main/java/com/quantai/trader/replay/ReplayRunConfig.java delete mode 100644 src/main/java/com/quantai/trader/replay/ReplayRunResponse.java delete mode 100644 src/main/java/com/quantai/trader/replay/ReplayRunService.java create mode 100644 src/main/java/com/quantai/trader/replay/TraderCycleResult.java create mode 100644 src/main/java/com/quantai/trader/replay/TraderP0CycleRunner.java delete mode 100644 src/main/java/com/quantai/trader/report/ReplayReportWriter.java create mode 100644 src/main/java/com/quantai/trader/risk/RiskGateInput.java create mode 100644 src/main/java/com/quantai/trader/risk/RiskLimits.java delete mode 100644 src/main/java/com/quantai/trader/risk/TraderPositionSizer.java create mode 100644 src/main/java/com/quantai/trader/runtime/P0RuntimeGuard.java create mode 100644 src/main/java/com/quantai/trader/runtime/StartupValidationRunner.java delete mode 100644 src/main/java/com/quantai/trader/sample/TrainingLabelSet.java delete mode 100644 src/main/java/com/quantai/trader/sample/TrainingSampleExporter.java delete mode 100644 src/main/java/com/quantai/trader/sample/TriggerMarkoutLabeler.java delete mode 100644 src/main/java/com/quantai/trader/state/TraderDecisionCycleFactory.java delete mode 100644 src/main/java/com/quantai/trader/state/TraderRuntimeState.java delete mode 100644 src/main/java/com/quantai/trader/state/TraderStateMachine.java delete mode 100644 src/main/java/com/quantai/trader/util/Ids.java create mode 100644 src/main/java/com/quantai/trader/util/TraderNumbers.java delete mode 100644 src/main/resources/db/migration/V1__trader_p0_schema.sql create mode 100644 src/main/resources/db/migration/V1__trader_v4_p0_schema.sql delete mode 100644 src/main/resources/db/migration/V2__trader_persistence_id_lengths.sql delete mode 100644 src/main/resources/playbooks/breakout-retest-intraday-5m-60m.yml delete mode 100644 src/test/java/com/quantai/trader/QuantTraderServiceApplicationTest.java create mode 100644 src/test/java/com/quantai/trader/artifact/TraderArtifactLoaderTest.java delete mode 100644 src/test/java/com/quantai/trader/brain/TraderDecisionCycleRunnerTest.java create mode 100644 src/test/java/com/quantai/trader/core/TraderCoreDtoTest.java create mode 100644 src/test/java/com/quantai/trader/domain/ModelOutputContractTest.java delete mode 100644 src/test/java/com/quantai/trader/domain/TraderAppFeedbackTest.java delete mode 100644 src/test/java/com/quantai/trader/domain/TraderDataSourceManifestTest.java delete mode 100644 src/test/java/com/quantai/trader/execution/TraderEntryPlannerTest.java create mode 100644 src/test/java/com/quantai/trader/outbox/InMemoryOutboxRepositoryTest.java delete mode 100644 src/test/java/com/quantai/trader/persistence/MybatisReplayPersistenceTest.java delete mode 100644 src/test/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepositoryTest.java delete mode 100644 src/test/java/com/quantai/trader/playbook/TraderPlaybookValidatorTest.java create mode 100644 src/test/java/com/quantai/trader/quality/NoLegacyTraderTermsTest.java delete mode 100644 src/test/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReaderTest.java create mode 100644 src/test/java/com/quantai/trader/replay/TraderP0CycleRunnerTest.java delete mode 100644 src/test/java/com/quantai/trader/replay/TraderReplayFixtureAcceptanceTest.java delete mode 100644 src/test/java/com/quantai/trader/risk/TraderPositionSizerTest.java create mode 100644 src/test/java/com/quantai/trader/runtime/P0RuntimeGuardTest.java delete mode 100644 src/test/java/com/quantai/trader/sample/TrainingSampleExporterTest.java delete mode 100644 src/test/java/com/quantai/trader/state/TraderStateMachineTest.java delete mode 100644 src/test/resources/replay-fixtures/README.md delete mode 100644 src/test/resources/replay-fixtures/crypto-lake-candidate-events-mini.csv delete mode 100644 src/test/resources/replay-fixtures/crypto-lake-replay-mini.csv delete mode 100644 src/test/resources/replay-fixtures/false-breakout-trigger-wait.jsonl delete mode 100644 src/test/resources/replay-fixtures/incomplete-entry-plan-hard-fail.jsonl delete mode 100644 src/test/resources/replay-fixtures/missing-features-data-quality.jsonl delete mode 100644 src/test/resources/replay-fixtures/sideways-range-no-setup.jsonl delete mode 100644 src/test/resources/replay-fixtures/trend-down-no-setup.jsonl delete mode 100644 src/test/resources/replay-fixtures/trend-up-breakout-happy.jsonl diff --git a/pom.xml b/pom.xml index ef9ecd9..d4e2f78 100644 --- a/pom.xml +++ b/pom.xml @@ -15,11 +15,11 @@ quant-trader-service 0.0.1-SNAPSHOT quant-trader-service - Clean P0 rebuild of the Trader-style strategy service. + Trader V4 P0 decision service: REPLAY_SIM and SHADOW only. 21 - 3.5.16 + 0.8.13 @@ -35,15 +35,6 @@ org.springframework.boot spring-boot-starter-webmvc - - org.springframework.boot - spring-boot-starter-jdbc - - - com.baomidou - mybatis-plus-spring-boot4-starter - ${mybatis-plus.version} - com.fasterxml.jackson.dataformat jackson-dataformat-yaml @@ -53,9 +44,8 @@ jackson-datatype-jsr310 - org.apache.commons - commons-csv - 1.10.0 + org.springframework.boot + spring-boot-starter-jdbc org.springframework.boot @@ -99,6 +89,46 @@ org.springframework.boot spring-boot-maven-plugin + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + prepare-agent + + + + report + test + + report + + + + check-line-coverage + test + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.90 + + + + + + + + diff --git a/src/main/java/com/quantai/trader/QuantTraderServiceApplication.java b/src/main/java/com/quantai/trader/QuantTraderServiceApplication.java index 437ef3e..e0d6aff 100644 --- a/src/main/java/com/quantai/trader/QuantTraderServiceApplication.java +++ b/src/main/java/com/quantai/trader/QuantTraderServiceApplication.java @@ -1,11 +1,9 @@ package com.quantai.trader; -import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -@MapperScan("com.quantai.trader.infrastructure.mapper") @SpringBootApplication @ConfigurationPropertiesScan public class QuantTraderServiceApplication { diff --git a/src/main/java/com/quantai/trader/artifact/TraderArtifactBundle.java b/src/main/java/com/quantai/trader/artifact/TraderArtifactBundle.java new file mode 100644 index 0000000..5e15381 --- /dev/null +++ b/src/main/java/com/quantai/trader/artifact/TraderArtifactBundle.java @@ -0,0 +1,21 @@ +package com.quantai.trader.artifact; + +import com.quantai.trader.domain.TraderPmConfig; + +import java.util.Set; + +public record TraderArtifactBundle( + String modelBundleVersion, + String calibrationBundleVersion, + String pmConfigVersion, + String bundleHashSha256, + Set providedModels, + TraderPmConfig pmConfig +) { + public TraderArtifactBundle { + if (providedModels == null || !providedModels.containsAll(Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"))) { + throw new IllegalArgumentException("artifact bundle must provide all five V4 models"); + } + pmConfig = java.util.Objects.requireNonNull(pmConfig, "pmConfig is required"); + } +} diff --git a/src/main/java/com/quantai/trader/artifact/TraderArtifactLoader.java b/src/main/java/com/quantai/trader/artifact/TraderArtifactLoader.java new file mode 100644 index 0000000..610e983 --- /dev/null +++ b/src/main/java/com/quantai/trader/artifact/TraderArtifactLoader.java @@ -0,0 +1,71 @@ +package com.quantai.trader.artifact; + +import com.quantai.trader.config.TraderProperties; +import com.quantai.trader.domain.TraderException; +import com.quantai.trader.domain.TraderPmConfig; +import com.quantai.trader.enums.TraderErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Set; + +@Component +public class TraderArtifactLoader { + private static final Logger log = LoggerFactory.getLogger(TraderArtifactLoader.class); + + private final TraderProperties properties; + + public TraderArtifactLoader(TraderProperties properties) { + this.properties = properties; + } + + public TraderArtifactBundle loadActiveBundle() { + TraderProperties.Artifact artifact = properties.artifact(); + if (artifact.modelBundleVersion().isBlank() + || artifact.calibrationBundleVersion().isBlank() + || artifact.pmConfigVersion().isBlank()) { + throw new TraderException(TraderErrorCode.TRADER_MODEL_ARTIFACT_MISSING, + "model/calibration/pm version is required"); + } + TraderArtifactBundle bundle = deterministicP0Bundle(artifact); + log.info("event=trader.artifact.loaded modelBundleVersion={} calibrationBundleVersion={} pmConfigVersion={} providedModels={}", + bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), bundle.providedModels()); + return bundle; + } + + private TraderArtifactBundle deterministicP0Bundle(TraderProperties.Artifact artifact) { + TraderPmConfig pmConfig = new TraderPmConfig( + artifact.pmConfigVersion(), + new TraderPmConfig.OpenRuleConfig( + new BigDecimal("0.58"), new BigDecimal("0.58"), + new BigDecimal("0.55"), new BigDecimal("0.55"), + new BigDecimal("0.45"), new BigDecimal("1.0"), + new BigDecimal("0.03"), new BigDecimal("0.10"), new BigDecimal("0.80")), + new TraderPmConfig.AddRuleConfig( + new BigDecimal("0.60"), new BigDecimal("0.60"), + new BigDecimal("0.58"), new BigDecimal("0.55"), new BigDecimal("0.45"), + new BigDecimal("0.45"), new BigDecimal("0.50"), + new BigDecimal("1.0"), BigDecimal.ZERO, new BigDecimal("0.10"), + new BigDecimal("500"), 3, 5), + new TraderPmConfig.ExitRuleConfig( + new BigDecimal("0.70"), new BigDecimal("0.70"), new BigDecimal("0.70"), + new BigDecimal("0.25"), new BigDecimal("0.62"), + new BigDecimal("0.35"), new BigDecimal("0.70"), + new BigDecimal("5.0"), new BigDecimal("80")), + new TraderPmConfig.SizingConfig( + new BigDecimal("0.80"), new BigDecimal("0.05"), BigDecimal.ONE, + new BigDecimal("0.02"), new BigDecimal("0.25"), BigDecimal.ONE, + new BigDecimal("1.0"), new BigDecimal("80"), + new BigDecimal("0.20"), new BigDecimal("0.50"), new BigDecimal("500")) + ); + return new TraderArtifactBundle( + artifact.modelBundleVersion(), + artifact.calibrationBundleVersion(), + artifact.pmConfigVersion(), + "deterministic-p0-fixture", + Set.of("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"), + pmConfig); + } +} diff --git a/src/main/java/com/quantai/trader/brain/LifecycleActionValueService.java b/src/main/java/com/quantai/trader/brain/LifecycleActionValueService.java deleted file mode 100644 index 3df168d..0000000 --- a/src/main/java/com/quantai/trader/brain/LifecycleActionValueService.java +++ /dev/null @@ -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)); - } -} diff --git a/src/main/java/com/quantai/trader/brain/PlaybookCandidateEngine.java b/src/main/java/com/quantai/trader/brain/PlaybookCandidateEngine.java deleted file mode 100644 index 0cce383..0000000 --- a/src/main/java/com/quantai/trader/brain/PlaybookCandidateEngine.java +++ /dev/null @@ -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 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 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 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 - ); - } -} diff --git a/src/main/java/com/quantai/trader/brain/TraderContextGate.java b/src/main/java/com/quantai/trader/brain/TraderContextGate.java deleted file mode 100644 index c8d4a83..0000000 --- a/src/main/java/com/quantai/trader/brain/TraderContextGate.java +++ /dev/null @@ -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"); - } -} diff --git a/src/main/java/com/quantai/trader/brain/TraderCycleResult.java b/src/main/java/com/quantai/trader/brain/TraderCycleResult.java deleted file mode 100644 index 3d274a3..0000000 --- a/src/main/java/com/quantai/trader/brain/TraderCycleResult.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/com/quantai/trader/brain/TraderDecisionCycleRunner.java b/src/main/java/com/quantai/trader/brain/TraderDecisionCycleRunner.java deleted file mode 100644 index bb5dad9..0000000 --- a/src/main/java/com/quantai/trader/brain/TraderDecisionCycleRunner.java +++ /dev/null @@ -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 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 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); - } -} diff --git a/src/main/java/com/quantai/trader/brain/TraderLifecycleResult.java b/src/main/java/com/quantai/trader/brain/TraderLifecycleResult.java deleted file mode 100644 index 6fca9c3..0000000 --- a/src/main/java/com/quantai/trader/brain/TraderLifecycleResult.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/com/quantai/trader/brain/TriggerMarkoutService.java b/src/main/java/com/quantai/trader/brain/TriggerMarkoutService.java deleted file mode 100644 index a4d45a6..0000000 --- a/src/main/java/com/quantai/trader/brain/TriggerMarkoutService.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/quantai/trader/config/TraderProperties.java b/src/main/java/com/quantai/trader/config/TraderProperties.java index 52e60d6..11c0462 100644 --- a/src/main/java/com/quantai/trader/config/TraderProperties.java +++ b/src/main/java/com/quantai/trader/config/TraderProperties.java @@ -1,299 +1,92 @@ package com.quantai.trader.config; +import com.quantai.trader.enums.TraderExecutionMode; import com.quantai.trader.enums.TraderRunMode; import org.springframework.boot.context.properties.ConfigurationProperties; import java.math.BigDecimal; +import static com.quantai.trader.util.TraderNumbers.requiredText; + @ConfigurationProperties(prefix = "trader") -public class TraderProperties { - - private String serviceName = "quant-trader-service"; - private TraderRunMode runMode = TraderRunMode.REPLAY; - private String symbol = "BTCUSDT"; - private String featureVersion = "trader_feature_v0"; - private String labelVersion = "trader_label_v0"; - private Playbook playbook = new Playbook(); - private Replay replay = new Replay(); - private Integration integration = new Integration(); - private Risk risk = new Risk(); - private Sizing sizing = new Sizing(); - private DataSource dataSource = new DataSource(); - - public String getServiceName() { - return serviceName; +public record TraderProperties( + String serviceName, + TraderRunMode runMode, + String symbol, + Artifact artifact, + Feedback feedback, + Execution execution, + Runtime runtime, + Outbox outbox, + Release release, + Risk risk, + PositionManager positionManager +) { + public TraderProperties { + serviceName = defaultText(serviceName, "quant-trader-service"); + runMode = runMode == null ? TraderRunMode.SHADOW : runMode; + symbol = defaultText(symbol, "BTC-USDT-PERP"); + artifact = artifact == null ? new Artifact("trader-v4-btc-p0", "cal-v4-btc-p0", "pm-v4-btc-p0", ".") : artifact; + feedback = feedback == null ? new Feedback(false) : feedback; + execution = execution == null ? new Execution(TraderExecutionMode.SHADOW, 3, 1500) : execution; + runtime = runtime == null ? new Runtime("trader:v4", true, false) : runtime; + outbox = outbox == null ? new Outbox(true, 5) : outbox; + release = release == null ? new Release(true, true, true) : release; + risk = risk == null ? new Risk(new BigDecimal("200"), BigDecimal.ONE, new BigDecimal("500")) : risk; + positionManager = positionManager == null ? new PositionManager(BigDecimal.ONE, BigDecimal.ONE) : positionManager; } - public void setServiceName(String serviceName) { - this.serviceName = serviceName; + private static String defaultText(String value, String defaultValue) { + return value == null || value.isBlank() ? defaultValue : value; } - public TraderRunMode getRunMode() { - return runMode; - } - - public void setRunMode(TraderRunMode runMode) { - this.runMode = runMode; - } - - public String getSymbol() { - return symbol; - } - - public void setSymbol(String symbol) { - this.symbol = symbol; - } - - public String getFeatureVersion() { - return featureVersion; - } - - public void setFeatureVersion(String featureVersion) { - this.featureVersion = featureVersion; - } - - public String getLabelVersion() { - return labelVersion; - } - - public void setLabelVersion(String labelVersion) { - this.labelVersion = labelVersion; - } - - public Playbook getPlaybook() { - return playbook; - } - - public void setPlaybook(Playbook playbook) { - this.playbook = playbook; - } - - public Replay getReplay() { - return replay; - } - - public void setReplay(Replay replay) { - this.replay = replay; - } - - public Integration getIntegration() { - return integration; - } - - public void setIntegration(Integration integration) { - this.integration = integration; - } - - public Risk getRisk() { - return risk; - } - - public void setRisk(Risk risk) { - this.risk = risk; - } - - public Sizing getSizing() { - return sizing; - } - - public void setSizing(Sizing sizing) { - this.sizing = sizing; - } - - public DataSource getDataSource() { - return dataSource; - } - - public void setDataSource(DataSource dataSource) { - this.dataSource = dataSource; - } - - public static class Playbook { - private String locationPattern = "classpath:/playbooks/*.yml"; - - public String getLocationPattern() { - return locationPattern; - } - - public void setLocationPattern(String locationPattern) { - this.locationPattern = locationPattern; + public record Artifact( + String modelBundleVersion, + String calibrationBundleVersion, + String pmConfigVersion, + String artifactRoot + ) { + public Artifact { + modelBundleVersion = requiredText(modelBundleVersion, "artifact.modelBundleVersion"); + calibrationBundleVersion = requiredText(calibrationBundleVersion, "artifact.calibrationBundleVersion"); + pmConfigVersion = requiredText(pmConfigVersion, "artifact.pmConfigVersion"); + artifactRoot = requiredText(artifactRoot, "artifact.artifactRoot"); } } - public static class Replay { - private String outputDir = "/Users/zach/Desktop/app/trader/replay-output"; - private boolean failOnDataMissing = true; + public record Feedback(boolean httpEnabled) { + } - public String getOutputDir() { - return outputDir; - } - - public void setOutputDir(String outputDir) { - this.outputDir = outputDir; - } - - public boolean isFailOnDataMissing() { - return failOnDataMissing; - } - - public void setFailOnDataMissing(boolean failOnDataMissing) { - this.failOnDataMissing = failOnDataMissing; + public record Execution(TraderExecutionMode mode, int maxApiErrorCount, long maxExchangeLatencyMs) { + public Execution { + mode = mode == null ? TraderExecutionMode.SHADOW : mode; } } - public static class Integration { - private String appActionChannel = "JAR_FUTURE"; - private boolean httpFeedbackEnabled = false; - - public String getAppActionChannel() { - return appActionChannel; - } - - public void setAppActionChannel(String appActionChannel) { - this.appActionChannel = appActionChannel; - } - - public boolean isHttpFeedbackEnabled() { - return httpFeedbackEnabled; - } - - public void setHttpFeedbackEnabled(boolean httpFeedbackEnabled) { - this.httpFeedbackEnabled = httpFeedbackEnabled; + public record Runtime(String redisKeyPrefix, boolean requireRedisForOpenAdd, boolean tradingEnabled) { + public Runtime { + redisKeyPrefix = defaultText(redisKeyPrefix, "trader:v4"); } } - public static class Risk { - private BigDecimal leverageScreen = BigDecimal.TEN; - private boolean requireOneXNotNegative = true; - private int maxPlannedEntryLegs = 3; - private boolean allowFreeScaleIn = false; - private boolean allowReduceThenAdd = false; - private boolean requireStop = true; - private boolean requireTarget = true; - private boolean requireInvalid = true; + public record Outbox(boolean enabled, int maxRetryCount) { + } - public BigDecimal getLeverageScreen() { - return leverageScreen; - } + public record Release(boolean requireReviewForPaper, boolean requireReviewForLiveProbe, boolean activePointerCheckEnabled) { + } - 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 record Risk(BigDecimal maxDailyLossBps, BigDecimal maxTotalExposureRatio, BigDecimal minLiquidationBufferBps) { + public Risk { + maxDailyLossBps = maxDailyLossBps == null ? new BigDecimal("200") : maxDailyLossBps; + maxTotalExposureRatio = maxTotalExposureRatio == null ? BigDecimal.ONE : maxTotalExposureRatio; + minLiquidationBufferBps = minLiquidationBufferBps == null ? new BigDecimal("500") : minLiquidationBufferBps; } } - public static class Sizing { - private String method = "SIGNAL_EXECUTION_RISK_DYNAMIC"; - private boolean allowFullInitialEntry = true; - private int maxPlannedEntryLegs = 3; - private BigDecimal maxTotalPositionRatio = BigDecimal.ONE; - private BigDecimal maxSingleLegRatio = BigDecimal.ONE; - - public String getMethod() { - return method; - } - - public void setMethod(String method) { - this.method = method; - } - - public boolean isAllowFullInitialEntry() { - return allowFullInitialEntry; - } - - public void setAllowFullInitialEntry(boolean allowFullInitialEntry) { - this.allowFullInitialEntry = allowFullInitialEntry; - } - - public int getMaxPlannedEntryLegs() { - return maxPlannedEntryLegs; - } - - public void setMaxPlannedEntryLegs(int maxPlannedEntryLegs) { - this.maxPlannedEntryLegs = maxPlannedEntryLegs; - } - - public BigDecimal getMaxTotalPositionRatio() { - return maxTotalPositionRatio; - } - - public void setMaxTotalPositionRatio(BigDecimal maxTotalPositionRatio) { - this.maxTotalPositionRatio = maxTotalPositionRatio; - } - - public BigDecimal getMaxSingleLegRatio() { - return maxSingleLegRatio; - } - - public void setMaxSingleLegRatio(BigDecimal maxSingleLegRatio) { - this.maxSingleLegRatio = maxSingleLegRatio; - } - } - - public static class DataSource { - private String hashMode = "FULL_HASH_OR_SCHEMA_ROW_TIME_MISSING_SUMMARY"; - - public String getHashMode() { - return hashMode; - } - - public void setHashMode(String hashMode) { - this.hashMode = hashMode; + public record PositionManager(BigDecimal maxSingleLegRatio, BigDecimal maxTotalPositionRatio) { + public PositionManager { + maxSingleLegRatio = maxSingleLegRatio == null ? BigDecimal.ONE : maxSingleLegRatio; + maxTotalPositionRatio = maxTotalPositionRatio == null ? BigDecimal.ONE : maxTotalPositionRatio; } } } diff --git a/src/main/java/com/quantai/trader/controller/TraderApiError.java b/src/main/java/com/quantai/trader/controller/TraderApiError.java index b778893..f1863d1 100644 --- a/src/main/java/com/quantai/trader/controller/TraderApiError.java +++ b/src/main/java/com/quantai/trader/controller/TraderApiError.java @@ -1,7 +1,6 @@ package com.quantai.trader.controller; -public record TraderApiError( - String code, - String message -) { +import com.quantai.trader.enums.TraderErrorCode; + +public record TraderApiError(TraderErrorCode code, String message) { } diff --git a/src/main/java/com/quantai/trader/controller/TraderApiExceptionHandler.java b/src/main/java/com/quantai/trader/controller/TraderApiExceptionHandler.java index 26425f8..89d3fe6 100644 --- a/src/main/java/com/quantai/trader/controller/TraderApiExceptionHandler.java +++ b/src/main/java/com/quantai/trader/controller/TraderApiExceptionHandler.java @@ -1,21 +1,19 @@ package com.quantai.trader.controller; import com.quantai.trader.domain.TraderException; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class TraderApiExceptionHandler { - @ExceptionHandler(TraderException.class) - public ResponseEntity handleTraderException(TraderException ex) { - return ResponseEntity.badRequest().body(new TraderApiError(ex.errorCode().name(), ex.getMessage())); + ResponseEntity traderException(TraderException exception) { + return ResponseEntity.badRequest().body(new TraderApiError(exception.code(), exception.getMessage())); } @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgument(IllegalArgumentException ex) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new TraderApiError("TRADER_NOT_FOUND", ex.getMessage())); + ResponseEntity illegalArgument(IllegalArgumentException exception) { + return ResponseEntity.badRequest().body(new TraderApiError(com.quantai.trader.enums.TraderErrorCode.TRADER_MODEL_OUTPUT_INVALID, exception.getMessage())); } } diff --git a/src/main/java/com/quantai/trader/controller/TraderFeedbackController.java b/src/main/java/com/quantai/trader/controller/TraderFeedbackController.java index a85163d..61f3adb 100644 --- a/src/main/java/com/quantai/trader/controller/TraderFeedbackController.java +++ b/src/main/java/com/quantai/trader/controller/TraderFeedbackController.java @@ -1,48 +1,37 @@ package com.quantai.trader.controller; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController -@RequestMapping("/api/trader/feedback") public class TraderFeedbackController { - private static final Logger log = LoggerFactory.getLogger(TraderFeedbackController.class); private final TraderProperties properties; + private final FeedbackValidator feedbackValidator; - public TraderFeedbackController(TraderProperties properties) { + public TraderFeedbackController(TraderProperties properties, FeedbackValidator feedbackValidator) { this.properties = properties; + this.feedbackValidator = feedbackValidator; } - @PostMapping - public ResponseEntity feedback(@RequestBody TraderFeedbackRequest request) { - if (!properties.getIntegration().isHttpFeedbackEnabled()) { - log.info( - "event=trader.feedback.rejected runId={} cycleId={} actionId={} reason={}", - request.runId(), - request.cycleId(), - request.actionId(), - TraderErrorCode.TRADER_FEEDBACK_DISABLED - ); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new TraderApiError( - TraderErrorCode.TRADER_FEEDBACK_DISABLED.name(), - "P0 feedback endpoint is disabled; future App main channel is trader-core jar" - )); + @PostMapping("/api/trader/feedback") + public Map feedback(@RequestBody TraderAppFeedback feedback) { + if (!properties.feedback().httpEnabled()) { + throw new TraderException(TraderErrorCode.TRADER_FEEDBACK_INVALID, "HTTP feedback is disabled in P0"); } - return ResponseEntity.accepted().body(Map.of( - "status", "ACCEPTED_CONTRACT_ONLY", - "runId", request.runId(), - "actionId", request.actionId() - )); + feedbackValidator.validateP0(feedback); + log.info("event=trader.feedback.accepted runId={} cycleId={} actionId={} source={}", + feedback.runId(), feedback.cycleId(), feedback.actionId(), feedback.feedbackSource()); + return Map.of("accepted", true, "feedbackId", feedback.feedbackId()); } } diff --git a/src/main/java/com/quantai/trader/controller/TraderFeedbackRequest.java b/src/main/java/com/quantai/trader/controller/TraderFeedbackRequest.java deleted file mode 100644 index d8ad943..0000000 --- a/src/main/java/com/quantai/trader/controller/TraderFeedbackRequest.java +++ /dev/null @@ -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 rawFeedback -) { -} diff --git a/src/main/java/com/quantai/trader/controller/TraderHealthController.java b/src/main/java/com/quantai/trader/controller/TraderHealthController.java index a5afde1..fc999ed 100644 --- a/src/main/java/com/quantai/trader/controller/TraderHealthController.java +++ b/src/main/java/com/quantai/trader/controller/TraderHealthController.java @@ -1,33 +1,28 @@ package com.quantai.trader.controller; import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.playbook.TraderPlaybookCatalog; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController -@RequestMapping("/api/trader") public class TraderHealthController { - private final TraderProperties properties; - private final TraderPlaybookCatalog catalog; - public TraderHealthController(TraderProperties properties, TraderPlaybookCatalog catalog) { + public TraderHealthController(TraderProperties properties) { this.properties = properties; - this.catalog = catalog; } - @GetMapping("/health") + @GetMapping("/api/trader/health") public Map health() { return Map.of( - "service", properties.getServiceName(), - "runMode", properties.getRunMode(), - "symbol", properties.getSymbol(), - "playbookCount", catalog.list().size(), - "httpFeedbackEnabled", properties.getIntegration().isHttpFeedbackEnabled() - ); + "status", "UP", + "runMode", properties.runMode(), + "executionMode", properties.execution().mode(), + "modelBundleVersion", properties.artifact().modelBundleVersion(), + "calibrationBundleVersion", properties.artifact().calibrationBundleVersion(), + "pmConfigVersion", properties.artifact().pmConfigVersion(), + "tradingEnabled", properties.runtime().tradingEnabled()); } } diff --git a/src/main/java/com/quantai/trader/controller/TraderPlaybookController.java b/src/main/java/com/quantai/trader/controller/TraderPlaybookController.java deleted file mode 100644 index 12477e5..0000000 --- a/src/main/java/com/quantai/trader/controller/TraderPlaybookController.java +++ /dev/null @@ -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 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 outputActions - ) { - - static PlaybookResponse from(TraderPlaybookDefinitionSnapshot snapshot) { - return new PlaybookResponse( - snapshot.playbookId(), - snapshot.playbookVersion(), - snapshot.family(), - snapshot.variant(), - snapshot.definitionHashSha256(), - snapshot.definition().outputActions() - ); - } - } -} diff --git a/src/main/java/com/quantai/trader/controller/TraderReplayController.java b/src/main/java/com/quantai/trader/controller/TraderReplayController.java index 76653ce..c33e344 100644 --- a/src/main/java/com/quantai/trader/controller/TraderReplayController.java +++ b/src/main/java/com/quantai/trader/controller/TraderReplayController.java @@ -1,52 +1,22 @@ package com.quantai.trader.controller; -import com.quantai.trader.domain.TraderReplayReport; -import com.quantai.trader.persistence.ReplayReportRepository; -import com.quantai.trader.replay.ReplayRun; -import com.quantai.trader.replay.ReplayRunConfig; -import com.quantai.trader.replay.ReplayRunResponse; -import com.quantai.trader.replay.ReplayRunService; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import com.quantai.trader.replay.ReplayMarketEvent; +import com.quantai.trader.replay.TraderCycleResult; +import com.quantai.trader.replay.TraderP0CycleRunner; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/trader/replay/runs") public class TraderReplayController { + private final TraderP0CycleRunner runner; - private final ReplayRunService replayRunService; - private final ReplayReportRepository reportRepository; - - public TraderReplayController(ReplayRunService replayRunService, ReplayReportRepository reportRepository) { - this.replayRunService = replayRunService; - this.reportRepository = reportRepository; + public TraderReplayController(TraderP0CycleRunner runner) { + this.runner = runner; } - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public ReplayRunResponse create(@RequestBody ReplayRunConfig config) { - return replayRunService.createRun(config); - } - - @GetMapping("/{runId}") - public ReplayRun get(@PathVariable String runId) { - return replayRunService.find(runId) - .orElseThrow(() -> new IllegalArgumentException("replay run not found: " + runId)); - } - - @PostMapping("/{runId}/cancel") - public ReplayRun cancel(@PathVariable String runId) { - return replayRunService.cancel(runId); - } - - @GetMapping("/{runId}/report") - public TraderReplayReport report(@PathVariable String runId) { - return reportRepository.findByRunId(runId) - .orElseThrow(() -> new IllegalArgumentException("replay report not found: " + runId)); + @PostMapping("/api/trader/replay/cycles") + public TraderCycleResult runOneCycle(@RequestBody ReplayMarketEvent event) { + return runner.runFlatCycle(event); } } diff --git a/src/main/java/com/quantai/trader/core/TraderCoreDecisionRequest.java b/src/main/java/com/quantai/trader/core/TraderCoreDecisionRequest.java new file mode 100644 index 0000000..370dbca --- /dev/null +++ b/src/main/java/com/quantai/trader/core/TraderCoreDecisionRequest.java @@ -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 requestContextJson +) { +} diff --git a/src/main/java/com/quantai/trader/core/TraderCoreDecisionResponse.java b/src/main/java/com/quantai/trader/core/TraderCoreDecisionResponse.java new file mode 100644 index 0000000..af647a2 --- /dev/null +++ b/src/main/java/com/quantai/trader/core/TraderCoreDecisionResponse.java @@ -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 responseContextJson +) { +} diff --git a/src/main/java/com/quantai/trader/core/TraderCoreFeedbackEvent.java b/src/main/java/com/quantai/trader/core/TraderCoreFeedbackEvent.java new file mode 100644 index 0000000..0b6753b --- /dev/null +++ b/src/main/java/com/quantai/trader/core/TraderCoreFeedbackEvent.java @@ -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 rawFeedbackJson +) { +} diff --git a/src/main/java/com/quantai/trader/core/TraderCoreHealth.java b/src/main/java/com/quantai/trader/core/TraderCoreHealth.java new file mode 100644 index 0000000..db486f7 --- /dev/null +++ b/src/main/java/com/quantai/trader/core/TraderCoreHealth.java @@ -0,0 +1,11 @@ +package com.quantai.trader.core; + +public record TraderCoreHealth( + boolean ready, + String runMode, + String modelBundleVersion, + String calibrationBundleVersion, + String pmConfigVersion, + String blocker +) { +} diff --git a/src/main/java/com/quantai/trader/domain/ContinueOutput.java b/src/main/java/com/quantai/trader/domain/ContinueOutput.java new file mode 100644 index 0000000..9dbf164 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/ContinueOutput.java @@ -0,0 +1,28 @@ +package com.quantai.trader.domain; + +import static com.quantai.trader.util.TraderNumbers.*; + +import java.math.BigDecimal; +import java.util.Map; + +public record ContinueOutput( + BigDecimal longContinueProb, + BigDecimal shortContinueProb, + BigDecimal trendPersistenceProb, + BigDecimal holdEdgeBps, + BigDecimal continueVsExitEdgeBps, + String modelVersion, + String calibrationVersion, + Map explanation +) { + public ContinueOutput { + longContinueProb = probability(longContinueProb, "continue.longContinueProb"); + shortContinueProb = probability(shortContinueProb, "continue.shortContinueProb"); + trendPersistenceProb = probability(trendPersistenceProb, "continue.trendPersistenceProb"); + holdEdgeBps = required(holdEdgeBps, "continue.holdEdgeBps"); + continueVsExitEdgeBps = required(continueVsExitEdgeBps, "continue.continueVsExitEdgeBps"); + modelVersion = requiredText(modelVersion, "continue.modelVersion"); + calibrationVersion = requiredText(calibrationVersion, "continue.calibrationVersion"); + explanation = Map.copyOf(explanation == null ? Map.of() : explanation); + } +} diff --git a/src/main/java/com/quantai/trader/domain/DirectionOutput.java b/src/main/java/com/quantai/trader/domain/DirectionOutput.java new file mode 100644 index 0000000..f98ad9c --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/DirectionOutput.java @@ -0,0 +1,34 @@ +package com.quantai.trader.domain; + +import static com.quantai.trader.util.TraderNumbers.*; + +import java.math.BigDecimal; +import java.util.Map; + +public record DirectionOutput( + BigDecimal longProb, + BigDecimal shortProb, + BigDecimal neutralProb, + BigDecimal directionConfidence, + BigDecimal directionMargin, + BigDecimal expectedReturnBps, + Integer horizonMinutes, + String modelVersion, + String calibrationVersion, + Map explanation +) { + public DirectionOutput { + longProb = probability(longProb, "direction.longProb"); + shortProb = probability(shortProb, "direction.shortProb"); + neutralProb = probability(neutralProb, "direction.neutralProb"); + directionConfidence = probability(directionConfidence, "direction.directionConfidence"); + directionMargin = nonNegative(directionMargin, "direction.directionMargin"); + expectedReturnBps = required(expectedReturnBps, "direction.expectedReturnBps"); + if (horizonMinutes == null || horizonMinutes <= 0) { + throw new IllegalArgumentException("direction.horizonMinutes must be > 0"); + } + modelVersion = requiredText(modelVersion, "direction.modelVersion"); + calibrationVersion = requiredText(calibrationVersion, "direction.calibrationVersion"); + explanation = Map.copyOf(explanation == null ? Map.of() : explanation); + } +} diff --git a/src/main/java/com/quantai/trader/domain/EntryOutput.java b/src/main/java/com/quantai/trader/domain/EntryOutput.java new file mode 100644 index 0000000..752933d --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/EntryOutput.java @@ -0,0 +1,40 @@ +package com.quantai.trader.domain; + +import static com.quantai.trader.util.TraderNumbers.*; + +import java.math.BigDecimal; +import java.util.Map; + +public record EntryOutput( + BigDecimal longEntryProb, + BigDecimal shortEntryProb, + BigDecimal entryQualityScore, + BigDecimal expectedEdgeBps, + String pricePlanId, + String pricePlanConfigHash, + BigDecimal stopDistanceBps, + BigDecimal targetDistanceBps, + Integer maxHoldMinutes, + BigDecimal costBps, + String modelVersion, + String calibrationVersion, + Map explanation +) { + public EntryOutput { + longEntryProb = probability(longEntryProb, "entry.longEntryProb"); + shortEntryProb = probability(shortEntryProb, "entry.shortEntryProb"); + entryQualityScore = probability(entryQualityScore, "entry.entryQualityScore"); + expectedEdgeBps = required(expectedEdgeBps, "entry.expectedEdgeBps"); + pricePlanId = requiredText(pricePlanId, "entry.pricePlanId"); + pricePlanConfigHash = requiredText(pricePlanConfigHash, "entry.pricePlanConfigHash"); + stopDistanceBps = positive(stopDistanceBps, "entry.stopDistanceBps"); + targetDistanceBps = positive(targetDistanceBps, "entry.targetDistanceBps"); + if (maxHoldMinutes == null || maxHoldMinutes <= 0) { + throw new IllegalArgumentException("entry.maxHoldMinutes must be > 0"); + } + costBps = nonNegative(costBps, "entry.costBps"); + modelVersion = requiredText(modelVersion, "entry.modelVersion"); + calibrationVersion = requiredText(calibrationVersion, "entry.calibrationVersion"); + explanation = Map.copyOf(explanation == null ? Map.of() : explanation); + } +} diff --git a/src/main/java/com/quantai/trader/domain/ExecutionDecision.java b/src/main/java/com/quantai/trader/domain/ExecutionDecision.java deleted file mode 100644 index 976a876..0000000 --- a/src/main/java/com/quantai/trader/domain/ExecutionDecision.java +++ /dev/null @@ -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 details -) { - - public ExecutionDecision { - details = Maps.immutable(details); - } - - public boolean blocked() { - return !pass; - } -} diff --git a/src/main/java/com/quantai/trader/domain/ExitOutput.java b/src/main/java/com/quantai/trader/domain/ExitOutput.java new file mode 100644 index 0000000..0828b10 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/ExitOutput.java @@ -0,0 +1,32 @@ +package com.quantai.trader.domain; + +import static com.quantai.trader.util.TraderNumbers.*; + +import java.math.BigDecimal; +import java.util.Map; + +public record ExitOutput( + BigDecimal longExitProb, + BigDecimal shortExitProb, + BigDecimal profitGivebackProb, + BigDecimal reversalProb, + BigDecimal stopRiskProb, + BigDecimal stagnationProb, + BigDecimal expectedGivebackBps, + String modelVersion, + String calibrationVersion, + Map explanation +) { + public ExitOutput { + longExitProb = probability(longExitProb, "exit.longExitProb"); + shortExitProb = probability(shortExitProb, "exit.shortExitProb"); + profitGivebackProb = probability(profitGivebackProb, "exit.profitGivebackProb"); + reversalProb = probability(reversalProb, "exit.reversalProb"); + stopRiskProb = probability(stopRiskProb, "exit.stopRiskProb"); + stagnationProb = probability(stagnationProb, "exit.stagnationProb"); + expectedGivebackBps = nonNegative(expectedGivebackBps, "exit.expectedGivebackBps"); + modelVersion = requiredText(modelVersion, "exit.modelVersion"); + calibrationVersion = requiredText(calibrationVersion, "exit.calibrationVersion"); + explanation = Map.copyOf(explanation == null ? Map.of() : explanation); + } +} diff --git a/src/main/java/com/quantai/trader/domain/FeedbackValidator.java b/src/main/java/com/quantai/trader/domain/FeedbackValidator.java new file mode 100644 index 0000000..d7e0af1 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/FeedbackValidator.java @@ -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"); + } + } +} diff --git a/src/main/java/com/quantai/trader/domain/ManagementDecision.java b/src/main/java/com/quantai/trader/domain/ManagementDecision.java deleted file mode 100644 index 3799530..0000000 --- a/src/main/java/com/quantai/trader/domain/ManagementDecision.java +++ /dev/null @@ -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 details -) { - - public ManagementDecision { - details = Maps.immutable(details); - } -} diff --git a/src/main/java/com/quantai/trader/domain/Maps.java b/src/main/java/com/quantai/trader/domain/Maps.java deleted file mode 100644 index d1a79ab..0000000 --- a/src/main/java/com/quantai/trader/domain/Maps.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.quantai.trader.domain; - -import java.util.LinkedHashMap; -import java.util.Map; - -final class Maps { - - private Maps() { - } - - static Map immutable(Map value) { - if (value == null || value.isEmpty()) { - return Map.of(); - } - return Map.copyOf(new LinkedHashMap<>(value)); - } -} diff --git a/src/main/java/com/quantai/trader/domain/OpenOrderState.java b/src/main/java/com/quantai/trader/domain/OpenOrderState.java new file mode 100644 index 0000000..42b7ef5 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/OpenOrderState.java @@ -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"); + } +} diff --git a/src/main/java/com/quantai/trader/domain/PlaybookCandidate.java b/src/main/java/com/quantai/trader/domain/PlaybookCandidate.java deleted file mode 100644 index 7e2f9a5..0000000 --- a/src/main/java/com/quantai/trader/domain/PlaybookCandidate.java +++ /dev/null @@ -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 setupEvidence -) { - - public PlaybookCandidate { - setupEvidence = Maps.immutable(setupEvidence); - } -} diff --git a/src/main/java/com/quantai/trader/domain/PositionManagerInput.java b/src/main/java/com/quantai/trader/domain/PositionManagerInput.java new file mode 100644 index 0000000..29a1060 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/PositionManagerInput.java @@ -0,0 +1,23 @@ +package com.quantai.trader.domain; + +import java.util.Objects; + +public record PositionManagerInput( + TraderDecisionCycle cycle, + TraderMarketSnapshot snapshot, + TraderModelOutput modelOutput, + 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"); + 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"); + } +} diff --git a/src/main/java/com/quantai/trader/domain/PositionSizingPlan.java b/src/main/java/com/quantai/trader/domain/PositionSizingPlan.java deleted file mode 100644 index 22e9e5f..0000000 --- a/src/main/java/com/quantai/trader/domain/PositionSizingPlan.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/com/quantai/trader/domain/RiskDecision.java b/src/main/java/com/quantai/trader/domain/RiskDecision.java deleted file mode 100644 index 5cc9746..0000000 --- a/src/main/java/com/quantai/trader/domain/RiskDecision.java +++ /dev/null @@ -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 details -) { - - public RiskDecision { - details = Maps.immutable(details); - } - - public boolean blocked() { - return !allowAction; - } -} diff --git a/src/main/java/com/quantai/trader/domain/RiskOutput.java b/src/main/java/com/quantai/trader/domain/RiskOutput.java new file mode 100644 index 0000000..99b8828 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/RiskOutput.java @@ -0,0 +1,38 @@ +package com.quantai.trader.domain; + +import static com.quantai.trader.util.TraderNumbers.*; + +import java.math.BigDecimal; +import java.util.Map; + +public record RiskOutput( + BigDecimal marketRiskProb, + BigDecimal positionRiskProb, + BigDecimal marketRiskSeverityBps, + BigDecimal positionRiskSeverityBps, + BigDecimal drawdownProb, + BigDecimal expectedShortfallBps, + BigDecimal volatilityExpansionProb, + BigDecimal spikeProb, + BigDecimal liquidityRiskProb, + BigDecimal liquidityCapacityRatio, + String modelVersion, + String calibrationVersion, + Map explanation +) { + public RiskOutput { + marketRiskProb = probability(marketRiskProb, "risk.marketRiskProb"); + positionRiskProb = probability(positionRiskProb, "risk.positionRiskProb"); + marketRiskSeverityBps = nonNegative(marketRiskSeverityBps, "risk.marketRiskSeverityBps"); + positionRiskSeverityBps = nonNegative(positionRiskSeverityBps, "risk.positionRiskSeverityBps"); + drawdownProb = probability(drawdownProb, "risk.drawdownProb"); + expectedShortfallBps = nonNegative(expectedShortfallBps, "risk.expectedShortfallBps"); + volatilityExpansionProb = probability(volatilityExpansionProb, "risk.volatilityExpansionProb"); + spikeProb = probability(spikeProb, "risk.spikeProb"); + liquidityRiskProb = probability(liquidityRiskProb, "risk.liquidityRiskProb"); + liquidityCapacityRatio = nonNegative(liquidityCapacityRatio, "risk.liquidityCapacityRatio"); + modelVersion = requiredText(modelVersion, "risk.modelVersion"); + calibrationVersion = requiredText(calibrationVersion, "risk.calibrationVersion"); + explanation = Map.copyOf(explanation == null ? Map.of() : explanation); + } +} diff --git a/src/main/java/com/quantai/trader/domain/StageDecision.java b/src/main/java/com/quantai/trader/domain/StageDecision.java deleted file mode 100644 index cfc6bc7..0000000 --- a/src/main/java/com/quantai/trader/domain/StageDecision.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.quantai.trader.domain; - -import java.util.Map; - -public record StageDecision( - boolean pass, - String reason, - String blocker, - Map 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()); - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderAccountState.java b/src/main/java/com/quantai/trader/domain/TraderAccountState.java new file mode 100644 index 0000000..501e9cb --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/TraderAccountState.java @@ -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"); + } + } +} diff --git a/src/main/java/com/quantai/trader/domain/TraderAction.java b/src/main/java/com/quantai/trader/domain/TraderAction.java index 82d7a2b..e2bf39d 100644 --- a/src/main/java/com/quantai/trader/domain/TraderAction.java +++ b/src/main/java/com/quantai/trader/domain/TraderAction.java @@ -1,30 +1,54 @@ package com.quantai.trader.domain; -import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.enums.TraderSide; +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.time.Instant; import java.util.Map; +import java.util.Objects; public record TraderAction( + String actionId, String runId, String cycleId, - String actionId, + String modelOutputId, + String pmDecisionId, + String riskDecisionId, TraderActionType actionType, - String playbookId, - String playbookVersion, String symbol, - TraderSide side, - BigDecimal price, + PositionSide side, + String pricePlanId, + String pricePlanConfigHash, + BigDecimal positionRatio, BigDecimal quantity, - Instant actionTime, + BigDecimal stopPrice, + BigDecimal targetPrice, + boolean reduceOnly, + String idempotencyKey, String reason, - Map actionContext, - String sendStatus + Map actionContextJson ) { - 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.reducesExposure() && !reduceOnly) { + throw new IllegalArgumentException("reduce/close action must be reduceOnly"); + } } } diff --git a/src/main/java/com/quantai/trader/domain/TraderActionFactory.java b/src/main/java/com/quantai/trader/domain/TraderActionFactory.java new file mode 100644 index 0000000..c3a4cfe --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/TraderActionFactory.java @@ -0,0 +1,44 @@ +package com.quantai.trader.domain; + +import com.quantai.trader.enums.PositionSide; +import com.quantai.trader.enums.TraderActionType; +import org.springframework.stereotype.Component; + +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, + finalAction == TraderActionType.OPEN_LONG || finalAction == TraderActionType.OPEN_SHORT ? pmDecision.targetPositionRatio() : pmDecision.addRatio(), + 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 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; + }; + } +} diff --git a/src/main/java/com/quantai/trader/domain/TraderAppFeedback.java b/src/main/java/com/quantai/trader/domain/TraderAppFeedback.java index cda5a4a..258315e 100644 --- a/src/main/java/com/quantai/trader/domain/TraderAppFeedback.java +++ b/src/main/java/com/quantai/trader/domain/TraderAppFeedback.java @@ -1,23 +1,21 @@ package com.quantai.trader.domain; -import com.quantai.trader.enums.TraderFeedbackSource; -import com.quantai.trader.enums.TraderFeedbackType; +import static com.quantai.trader.util.TraderNumbers.*; +import com.quantai.trader.enums.FeedbackSource; import java.math.BigDecimal; import java.time.Instant; import java.util.Map; +import java.util.Objects; public record TraderAppFeedback( + String feedbackId, String runId, String cycleId, String actionId, - TraderFeedbackType feedbackType, - TraderFeedbackSource feedbackSource, - String proxyMethod, - String simulatorVersion, + FeedbackSource feedbackSource, boolean realFill, String orderId, - String positionId, String orderStatus, Instant appReceivedTime, Instant exchangeAckTime, @@ -26,19 +24,24 @@ public record TraderAppFeedback( BigDecimal filledQuantity, BigDecimal fee, BigDecimal slippageBps, - String closeReason, - String closeSignalSource, - String exchangeErrorCode, - String platformErrorCode, - Map rawFeedback + String rejectReason, + Map rawFeedbackJson ) { - public TraderAppFeedback { - rawFeedback = Maps.immutable(rawFeedback); - boolean sourceCanBeReal = feedbackSource == TraderFeedbackSource.PAPER_APP - || feedbackSource == TraderFeedbackSource.REAL_APP; - if (realFill != sourceCanBeReal) { - throw new IllegalArgumentException("feedback_source and realFill are inconsistent"); + feedbackId = requiredText(feedbackId, "feedbackId"); + runId = requiredText(runId, "runId"); + cycleId = requiredText(cycleId, "cycleId"); + actionId = requiredText(actionId, "actionId"); + 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); } } diff --git a/src/main/java/com/quantai/trader/domain/TraderDataSourceManifest.java b/src/main/java/com/quantai/trader/domain/TraderDataSourceManifest.java deleted file mode 100644 index 653114b..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderDataSourceManifest.java +++ /dev/null @@ -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 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" - ); - } - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderDecisionCycle.java b/src/main/java/com/quantai/trader/domain/TraderDecisionCycle.java index c7d808f..da7bd26 100644 --- a/src/main/java/com/quantai/trader/domain/TraderDecisionCycle.java +++ b/src/main/java/com/quantai/trader/domain/TraderDecisionCycle.java @@ -1,37 +1,29 @@ package com.quantai.trader.domain; -import com.quantai.trader.enums.TraderRunMode; -import com.quantai.trader.enums.TraderState; +import static com.quantai.trader.util.TraderNumbers.requiredText; +import com.quantai.trader.enums.TraderRunMode; import java.time.Instant; +import java.util.Objects; public record TraderDecisionCycle( String runId, String cycleId, - String snapshotId, String symbol, - String playbookId, - String playbookVersion, - TraderState state, Instant cycleTime, TraderRunMode runMode, - String decisionStatus, - String blocker + String modelBundleVersion, + String calibrationBundleVersion, + String pmConfigVersion ) { - - public TraderDecisionCycle withState(TraderState nextState, String nextStatus, String nextBlocker) { - return new TraderDecisionCycle( - runId, - cycleId, - snapshotId, - symbol, - playbookId, - playbookVersion, - nextState, - cycleTime, - runMode, - nextStatus, - nextBlocker - ); + public TraderDecisionCycle { + runId = requiredText(runId, "runId"); + cycleId = requiredText(cycleId, "cycleId"); + symbol = requiredText(symbol, "symbol"); + cycleTime = Objects.requireNonNull(cycleTime, "cycleTime is required"); + runMode = Objects.requireNonNull(runMode, "runMode is required"); + modelBundleVersion = requiredText(modelBundleVersion, "modelBundleVersion"); + calibrationBundleVersion = requiredText(calibrationBundleVersion, "calibrationBundleVersion"); + pmConfigVersion = requiredText(pmConfigVersion, "pmConfigVersion"); } } diff --git a/src/main/java/com/quantai/trader/domain/TraderEntryPlan.java b/src/main/java/com/quantai/trader/domain/TraderEntryPlan.java deleted file mode 100644 index aa5f11e..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderEntryPlan.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderEvidence.java b/src/main/java/com/quantai/trader/domain/TraderEvidence.java index cf5bd2f..514ed09 100644 --- a/src/main/java/com/quantai/trader/domain/TraderEvidence.java +++ b/src/main/java/com/quantai/trader/domain/TraderEvidence.java @@ -1,21 +1,29 @@ package com.quantai.trader.domain; +import static com.quantai.trader.util.TraderNumbers.requiredText; + import java.time.Instant; import java.util.Map; +import java.util.Objects; public record TraderEvidence( + String evidenceId, String runId, String cycleId, - String evidenceId, String stage, boolean pass, String reason, String blocker, Instant evidenceTime, - Map details + Map detailsJson ) { - 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); } } diff --git a/src/main/java/com/quantai/trader/domain/TraderException.java b/src/main/java/com/quantai/trader/domain/TraderException.java index 2a97bf2..2635030 100644 --- a/src/main/java/com/quantai/trader/domain/TraderException.java +++ b/src/main/java/com/quantai/trader/domain/TraderException.java @@ -3,15 +3,14 @@ package com.quantai.trader.domain; import com.quantai.trader.enums.TraderErrorCode; public class TraderException extends RuntimeException { + private final TraderErrorCode code; - private final TraderErrorCode errorCode; - - public TraderException(TraderErrorCode errorCode, String message) { + public TraderException(TraderErrorCode code, String message) { super(message); - this.errorCode = errorCode; + this.code = code; } - public TraderErrorCode errorCode() { - return errorCode; + public TraderErrorCode code() { + return code; } } diff --git a/src/main/java/com/quantai/trader/domain/TraderExecutionState.java b/src/main/java/com/quantai/trader/domain/TraderExecutionState.java new file mode 100644 index 0000000..8eb4fd0 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/TraderExecutionState.java @@ -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 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"); + } +} diff --git a/src/main/java/com/quantai/trader/domain/TraderManagementAction.java b/src/main/java/com/quantai/trader/domain/TraderManagementAction.java deleted file mode 100644 index 2e94ce6..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderManagementAction.java +++ /dev/null @@ -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 details -) { - - public TraderManagementAction { - details = Maps.immutable(details); - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderMarketEvent.java b/src/main/java/com/quantai/trader/domain/TraderMarketEvent.java deleted file mode 100644 index 7f19e2b..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderMarketEvent.java +++ /dev/null @@ -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 payload -) { - - public TraderMarketEvent { - payload = Maps.immutable(payload); - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderMarketSnapshot.java b/src/main/java/com/quantai/trader/domain/TraderMarketSnapshot.java index 294b935..cba33e1 100644 --- a/src/main/java/com/quantai/trader/domain/TraderMarketSnapshot.java +++ b/src/main/java/com/quantai/trader/domain/TraderMarketSnapshot.java @@ -1,29 +1,44 @@ package com.quantai.trader.domain; +import static com.quantai.trader.util.TraderNumbers.*; + +import java.math.BigDecimal; import java.time.Instant; import java.util.Map; public record TraderMarketSnapshot( + String snapshotId, String runId, String cycleId, - String snapshotId, String symbol, Instant snapshotTime, String featureVersion, - Map contextFeatures, - Map setupFeatures, - Map triggerFeatures, - Map executionFeatures, - Map dataQuality, - Map labelInputs + BigDecimal markPrice, + BigDecimal indexPrice, + BigDecimal spreadBps, + BigDecimal fundingRateBps, + BigDecimal depthNotional5Bps, + BigDecimal depthNotional10Bps, + BigDecimal depthNotional25Bps, + boolean dataReady, + Map featureJson, + Map dataQualityJson ) { - public TraderMarketSnapshot { - contextFeatures = Maps.immutable(contextFeatures); - setupFeatures = Maps.immutable(setupFeatures); - triggerFeatures = Maps.immutable(triggerFeatures); - executionFeatures = Maps.immutable(executionFeatures); - dataQuality = Maps.immutable(dataQuality); - labelInputs = Maps.immutable(labelInputs); + snapshotId = requiredText(snapshotId, "snapshotId"); + runId = requiredText(runId, "runId"); + cycleId = requiredText(cycleId, "cycleId"); + symbol = requiredText(symbol, "symbol"); + snapshotTime = java.util.Objects.requireNonNull(snapshotTime, "snapshotTime is required"); + 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); } } diff --git a/src/main/java/com/quantai/trader/domain/TraderModelManifest.java b/src/main/java/com/quantai/trader/domain/TraderModelManifest.java deleted file mode 100644 index 2a464b2..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderModelManifest.java +++ /dev/null @@ -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 metrics, - String status -) { - - public TraderModelManifest { - metrics = Maps.immutable(metrics); - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderModelOutput.java b/src/main/java/com/quantai/trader/domain/TraderModelOutput.java index 00eeabb..61800e3 100644 --- a/src/main/java/com/quantai/trader/domain/TraderModelOutput.java +++ b/src/main/java/com/quantai/trader/domain/TraderModelOutput.java @@ -1,20 +1,39 @@ package com.quantai.trader.domain; +import static com.quantai.trader.util.TraderNumbers.*; + import java.math.BigDecimal; -import java.time.Instant; import java.util.Map; +import java.util.Objects; public record TraderModelOutput( - String modelName, - String modelVersion, - BigDecimal score, + String modelOutputId, + String runId, + String cycleId, + String modelBundleVersion, + String calibrationBundleVersion, + DirectionOutput direction, + EntryOutput entry, + ContinueOutput continuation, + ExitOutput exit, + RiskOutput risk, BigDecimal uncertainty, BigDecimal oodScore, - Instant predictedAt, - Map details + Map explanation ) { - public TraderModelOutput { - details = Maps.immutable(details); + modelOutputId = requiredText(modelOutputId, "modelOutputId"); + runId = requiredText(runId, "runId"); + cycleId = requiredText(cycleId, "cycleId"); + modelBundleVersion = requiredText(modelBundleVersion, "modelBundleVersion"); + calibrationBundleVersion = requiredText(calibrationBundleVersion, "calibrationBundleVersion"); + 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"); + uncertainty = probability(uncertainty, "uncertainty"); + oodScore = probability(oodScore, "oodScore"); + explanation = Map.copyOf(explanation == null ? Map.of() : explanation); } } diff --git a/src/main/java/com/quantai/trader/domain/TraderPmConfig.java b/src/main/java/com/quantai/trader/domain/TraderPmConfig.java new file mode 100644 index 0000000..6a71b87 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/TraderPmConfig.java @@ -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 reduceGivebackProb, + BigDecimal reduceContinueMin, + BigDecimal reduceContinueMax, + BigDecimal minProfitForReduceBps, + BigDecimal maxExpectedShortfallBps + ) { + public ExitRuleConfig { + closeExitProb = probability(closeExitProb, "exit.closeExitProb"); + closePositionRiskProb = probability(closePositionRiskProb, "exit.closePositionRiskProb"); + closeMarketRiskProb = probability(closeMarketRiskProb, "exit.closeMarketRiskProb"); + closeContinueMax = probability(closeContinueMax, "exit.closeContinueMax"); + reduceGivebackProb = probability(reduceGivebackProb, "exit.reduceGivebackProb"); + reduceContinueMin = probability(reduceContinueMin, "exit.reduceContinueMin"); + reduceContinueMax = probability(reduceContinueMax, "exit.reduceContinueMax"); + minProfitForReduceBps = nonNegative(minProfitForReduceBps, "exit.minProfitForReduceBps"); + maxExpectedShortfallBps = nonNegative(maxExpectedShortfallBps, "exit.maxExpectedShortfallBps"); + } + } + + 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"); + } + } +} diff --git a/src/main/java/com/quantai/trader/domain/TraderPositionLeg.java b/src/main/java/com/quantai/trader/domain/TraderPositionLeg.java deleted file mode 100644 index cde2c9f..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderPositionLeg.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/com/quantai/trader/domain/TraderPositionManagerDecision.java b/src/main/java/com/quantai/trader/domain/TraderPositionManagerDecision.java new file mode 100644 index 0000000..e5729c0 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/TraderPositionManagerDecision.java @@ -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 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"); + } + } +} diff --git a/src/main/java/com/quantai/trader/domain/TraderPositionPath.java b/src/main/java/com/quantai/trader/domain/TraderPositionPath.java deleted file mode 100644 index afbf053..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderPositionPath.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderPositionState.java b/src/main/java/com/quantai/trader/domain/TraderPositionState.java new file mode 100644 index 0000000..5d11145 --- /dev/null +++ b/src/main/java/com/quantai/trader/domain/TraderPositionState.java @@ -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; + } +} diff --git a/src/main/java/com/quantai/trader/domain/TraderPricePlan.java b/src/main/java/com/quantai/trader/domain/TraderPricePlan.java deleted file mode 100644 index 1210cf0..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderPricePlan.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderReplayReport.java b/src/main/java/com/quantai/trader/domain/TraderReplayReport.java deleted file mode 100644 index 120bca8..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderReplayReport.java +++ /dev/null @@ -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 strictVsLoose, - List failureRisks, - String conclusion, - String reportPath, - Instant createdAt -) { - - public TraderReplayReport { - strictVsLoose = Maps.immutable(strictVsLoose); - failureRisks = failureRisks == null ? List.of() : List.copyOf(failureRisks); - } -} diff --git a/src/main/java/com/quantai/trader/domain/TraderRiskDecision.java b/src/main/java/com/quantai/trader/domain/TraderRiskDecision.java index 503fdb2..66bda46 100644 --- a/src/main/java/com/quantai/trader/domain/TraderRiskDecision.java +++ b/src/main/java/com/quantai/trader/domain/TraderRiskDecision.java @@ -1,32 +1,32 @@ package com.quantai.trader.domain; -import com.quantai.trader.enums.TraderActionType; +import static com.quantai.trader.util.TraderNumbers.requiredText; -import java.math.BigDecimal; -import java.time.Instant; +import com.quantai.trader.enums.TraderActionType; import java.util.Map; +import java.util.Objects; public record TraderRiskDecision( + String riskDecisionId, String runId, String cycleId, - String actionId, - String accountStateId, - TraderActionType actionType, - BigDecimal leverageScreen, - BigDecimal plannedTotalPositionRatio, - BigDecimal maxLossBps, - BigDecimal liquidationBufferBps, - BigDecimal expectedValueBps1x, - BigDecimal expectedValueBps10x, - BigDecimal uncertainty, - BigDecimal oodScore, + String pmDecisionId, boolean allowAction, + TraderActionType originalAction, + TraderActionType finalAction, String blocker, - Map decision, - Instant createdAt + Map decisionJson ) { - 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); } } diff --git a/src/main/java/com/quantai/trader/domain/TraderTrainingSample.java b/src/main/java/com/quantai/trader/domain/TraderTrainingSample.java deleted file mode 100644 index fe5ea90..0000000 --- a/src/main/java/com/quantai/trader/domain/TraderTrainingSample.java +++ /dev/null @@ -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 features, - Map labels, - BigDecimal netReturnBps1x, - BigDecimal netReturnBps10x, - boolean proxyOnly -) { - - public TraderTrainingSample { - features = Maps.immutable(features); - labels = Maps.immutable(labels); - } -} diff --git a/src/main/java/com/quantai/trader/domain/TriggerDecision.java b/src/main/java/com/quantai/trader/domain/TriggerDecision.java deleted file mode 100644 index ee12444..0000000 --- a/src/main/java/com/quantai/trader/domain/TriggerDecision.java +++ /dev/null @@ -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 details -) { - - public TriggerDecision { - details = Maps.immutable(details); - } - - public boolean blocked() { - return !pass; - } -} diff --git a/src/main/java/com/quantai/trader/domain/TriggerEvent.java b/src/main/java/com/quantai/trader/domain/TriggerEvent.java deleted file mode 100644 index f9a546e..0000000 --- a/src/main/java/com/quantai/trader/domain/TriggerEvent.java +++ /dev/null @@ -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 triggerEvidence, - Map markout -) { - - public TriggerEvent { - triggerEvidence = Maps.immutable(triggerEvidence); - markout = Maps.immutable(markout); - } -} diff --git a/src/main/java/com/quantai/trader/enums/FeedbackSource.java b/src/main/java/com/quantai/trader/enums/FeedbackSource.java new file mode 100644 index 0000000..fdc5ceb --- /dev/null +++ b/src/main/java/com/quantai/trader/enums/FeedbackSource.java @@ -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; + } +} diff --git a/src/main/java/com/quantai/trader/enums/PositionSide.java b/src/main/java/com/quantai/trader/enums/PositionSide.java new file mode 100644 index 0000000..cf7a7b6 --- /dev/null +++ b/src/main/java/com/quantai/trader/enums/PositionSide.java @@ -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; + } +} diff --git a/src/main/java/com/quantai/trader/enums/ReplayRunStatus.java b/src/main/java/com/quantai/trader/enums/ReplayRunStatus.java deleted file mode 100644 index 9eef8b5..0000000 --- a/src/main/java/com/quantai/trader/enums/ReplayRunStatus.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.quantai.trader.enums; - -public enum ReplayRunStatus { - CREATED, - RUNNING, - CANCEL_REQUESTED, - CANCELLED, - COMPLETED, - FAILED -} diff --git a/src/main/java/com/quantai/trader/enums/TraderActionType.java b/src/main/java/com/quantai/trader/enums/TraderActionType.java index fa18293..6e9a59e 100644 --- a/src/main/java/com/quantai/trader/enums/TraderActionType.java +++ b/src/main/java/com/quantai/trader/enums/TraderActionType.java @@ -2,12 +2,23 @@ package com.quantai.trader.enums; public enum TraderActionType { WAIT, - OPEN_INITIAL, - OPEN_PLANNED_LEG, + OPEN_LONG, + OPEN_SHORT, + ADD_LONG, + ADD_SHORT, HOLD, - REDUCE, + REDUCE_LONG, + REDUCE_SHORT, MOVE_STOP, - CLOSE, - CANCEL, - REQUOTE + CLOSE_LONG, + CLOSE_SHORT, + 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; + } } diff --git a/src/main/java/com/quantai/trader/enums/TraderErrorCode.java b/src/main/java/com/quantai/trader/enums/TraderErrorCode.java index 14709ac..1028013 100644 --- a/src/main/java/com/quantai/trader/enums/TraderErrorCode.java +++ b/src/main/java/com/quantai/trader/enums/TraderErrorCode.java @@ -1,13 +1,15 @@ package com.quantai.trader.enums; public enum TraderErrorCode { - TRADER_PLAYBOOK_VERSION_CONFLICT, - TRADER_DATA_SOURCE_MISSING, - TRADER_DATA_QUALITY_FAILED, - TRADER_ENTRY_PLAN_INCOMPLETE, - TRADER_ILLEGAL_ACTION_TRANSITION, - TRADER_PLANNED_LEG_AFTER_REDUCE, + TRADER_DATA_NOT_READY, + TRADER_MODEL_ARTIFACT_MISSING, + TRADER_CALIBRATION_MISMATCH, + TRADER_PM_CONFIG_MISMATCH, + TRADER_MODEL_OUTPUT_INVALID, TRADER_RISK_BLOCKED, - TRADER_FEEDBACK_DISABLED, - TRADER_SAMPLE_EXPORT_FAILED + TRADER_EXECUTION_BLOCKED, + TRADER_FEEDBACK_INVALID, + TRADER_P0_MODE_BLOCKED, + TRADER_KILL_SWITCH_ACTIVE, + TRADER_ACTIVE_POINTER_MISMATCH } diff --git a/src/main/java/com/quantai/trader/enums/TraderExecutionMode.java b/src/main/java/com/quantai/trader/enums/TraderExecutionMode.java new file mode 100644 index 0000000..98001da --- /dev/null +++ b/src/main/java/com/quantai/trader/enums/TraderExecutionMode.java @@ -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; + } +} diff --git a/src/main/java/com/quantai/trader/enums/TraderFeedbackSource.java b/src/main/java/com/quantai/trader/enums/TraderFeedbackSource.java deleted file mode 100644 index b1a2c2f..0000000 --- a/src/main/java/com/quantai/trader/enums/TraderFeedbackSource.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.quantai.trader.enums; - -public enum TraderFeedbackSource { - MARKET_PROXY, - SHADOW_APP, - PAPER_APP, - REAL_APP -} diff --git a/src/main/java/com/quantai/trader/enums/TraderFeedbackType.java b/src/main/java/com/quantai/trader/enums/TraderFeedbackType.java deleted file mode 100644 index bf9edbe..0000000 --- a/src/main/java/com/quantai/trader/enums/TraderFeedbackType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.quantai.trader.enums; - -public enum TraderFeedbackType { - FILL_EVENT, - CANCEL_EVENT, - CLOSE_EVENT, - REJECT_EVENT -} diff --git a/src/main/java/com/quantai/trader/enums/TraderPlaybookId.java b/src/main/java/com/quantai/trader/enums/TraderPlaybookId.java deleted file mode 100644 index 23d127c..0000000 --- a/src/main/java/com/quantai/trader/enums/TraderPlaybookId.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.enums; - -public enum TraderPlaybookId { - BREAKOUT_RETEST_CONTINUATION, - SUPPORT_PULLBACK_CONTINUATION, - FALSE_BREAK_RECLAIM -} diff --git a/src/main/java/com/quantai/trader/enums/TraderRunMode.java b/src/main/java/com/quantai/trader/enums/TraderRunMode.java index 31226c8..c9f122c 100644 --- a/src/main/java/com/quantai/trader/enums/TraderRunMode.java +++ b/src/main/java/com/quantai/trader/enums/TraderRunMode.java @@ -1,7 +1,12 @@ package com.quantai.trader.enums; public enum TraderRunMode { - REPLAY, + REPLAY_SIM, SHADOW, - PAPER + PAPER, + REAL; + + public boolean p0Allowed() { + return this == REPLAY_SIM || this == SHADOW; + } } diff --git a/src/main/java/com/quantai/trader/enums/TraderSide.java b/src/main/java/com/quantai/trader/enums/TraderSide.java deleted file mode 100644 index c7caa6a..0000000 --- a/src/main/java/com/quantai/trader/enums/TraderSide.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.quantai.trader.enums; - -public enum TraderSide { - LONG, - SHORT -} diff --git a/src/main/java/com/quantai/trader/enums/TraderState.java b/src/main/java/com/quantai/trader/enums/TraderState.java deleted file mode 100644 index c8d75dc..0000000 --- a/src/main/java/com/quantai/trader/enums/TraderState.java +++ /dev/null @@ -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 -} diff --git a/src/main/java/com/quantai/trader/evidence/EvidenceAppender.java b/src/main/java/com/quantai/trader/evidence/EvidenceAppender.java index 15daca4..398c21c 100644 --- a/src/main/java/com/quantai/trader/evidence/EvidenceAppender.java +++ b/src/main/java/com/quantai/trader/evidence/EvidenceAppender.java @@ -1,50 +1,31 @@ package com.quantai.trader.evidence; -import com.quantai.trader.domain.StageDecision; -import com.quantai.trader.domain.TraderDecisionCycle; import com.quantai.trader.domain.TraderEvidence; -import com.quantai.trader.persistence.TraderEvidenceRepository; -import com.quantai.trader.util.Ids; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + @Component public class EvidenceAppender { - private static final Logger log = LoggerFactory.getLogger(EvidenceAppender.class); - private final TraderEvidenceRepository repository; + private final CopyOnWriteArrayList evidence = new CopyOnWriteArrayList<>(); - public EvidenceAppender(TraderEvidenceRepository repository) { - this.repository = repository; + public TraderEvidence append(String runId, String cycleId, String stage, boolean pass, String reason, String blocker, Map details) { + TraderEvidence item = new TraderEvidence("evidence_" + cycleId + "_" + evidence.size(), runId, cycleId, + stage, pass, reason, blocker, Instant.now(), details); + evidence.add(item); + log.info("event=trader.evidence.appended runId={} cycleId={} stage={} pass={} reason={} blocker={}", + runId, cycleId, stage, pass, reason, blocker); + return item; } - public TraderEvidence append(TraderDecisionCycle cycle, String stage, StageDecision decision) { - TraderEvidence evidence = new TraderEvidence( - cycle.runId(), - cycle.cycleId(), - Ids.evidenceId(cycle, stage), - stage, - decision.pass(), - decision.reason(), - decision.blocker(), - cycle.cycleTime(), - decision.details() - ); - repository.insert(evidence); - log.info( - "event=trader.evidence runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} stage={} pass={} reason={} blocker={}", - cycle.runId(), - cycle.cycleId(), - cycle.symbol(), - cycle.playbookId(), - cycle.playbookVersion(), - cycle.state(), - stage, - decision.pass(), - decision.reason(), - decision.blocker() - ); - return evidence; + public List all() { + return new ArrayList<>(evidence); } } diff --git a/src/main/java/com/quantai/trader/execution/ExecutionQualityGate.java b/src/main/java/com/quantai/trader/execution/ExecutionQualityGate.java deleted file mode 100644 index df316bb..0000000 --- a/src/main/java/com/quantai/trader/execution/ExecutionQualityGate.java +++ /dev/null @@ -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 - )); - } -} diff --git a/src/main/java/com/quantai/trader/execution/TraderEntryPlanner.java b/src/main/java/com/quantai/trader/execution/TraderEntryPlanner.java deleted file mode 100644 index f3df074..0000000 --- a/src/main/java/com/quantai/trader/execution/TraderEntryPlanner.java +++ /dev/null @@ -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 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" - ); - } - } -} diff --git a/src/main/java/com/quantai/trader/infrastructure/entity/TraderEvidenceEntity.java b/src/main/java/com/quantai/trader/infrastructure/entity/TraderEvidenceEntity.java deleted file mode 100644 index c6502ec..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/entity/TraderEvidenceEntity.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/quantai/trader/infrastructure/entity/TraderPlaybookDefinitionEntity.java b/src/main/java/com/quantai/trader/infrastructure/entity/TraderPlaybookDefinitionEntity.java deleted file mode 100644 index 8ab81df..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/entity/TraderPlaybookDefinitionEntity.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayReportEntity.java b/src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayReportEntity.java deleted file mode 100644 index 08c78c5..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayReportEntity.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayRunEntity.java b/src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayRunEntity.java deleted file mode 100644 index c785450..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/entity/TraderReplayRunEntity.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/quantai/trader/infrastructure/entity/TraderRiskDecisionEntity.java b/src/main/java/com/quantai/trader/infrastructure/entity/TraderRiskDecisionEntity.java deleted file mode 100644 index ab05247..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/entity/TraderRiskDecisionEntity.java +++ /dev/null @@ -1,36 +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_risk_decision") -public class TraderRiskDecisionEntity { - @TableId(type = IdType.AUTO) - private Long id; - private String runId; - private String cycleId; - private String actionId; - private String accountStateId; - private String actionType; - private BigDecimal leverageScreen; - private BigDecimal plannedTotalPositionRatio; - private BigDecimal maxLossBps; - private BigDecimal liquidationBufferBps; - @TableField("expected_value_bps_1x") - private BigDecimal expectedValueBps1x; - @TableField("expected_value_bps_10x") - private BigDecimal expectedValueBps10x; - private BigDecimal uncertainty; - private BigDecimal oodScore; - private Boolean allowAction; - private String blocker; - private String decisionJson; - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/quantai/trader/infrastructure/entity/TraderTrainingSampleEntity.java b/src/main/java/com/quantai/trader/infrastructure/entity/TraderTrainingSampleEntity.java deleted file mode 100644 index 3f22c96..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/entity/TraderTrainingSampleEntity.java +++ /dev/null @@ -1,33 +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_training_sample") -public class TraderTrainingSampleEntity { - @TableId(type = IdType.AUTO) - private Long id; - private String runId; - private String cycleId; - private String sampleId; - private String actionId; - private String positionId; - private String featureVersion; - private String labelVersion; - private LocalDateTime sampleTime; - private String featuresJson; - private String labelsJson; - @TableField("net_return_bps_1x") - private BigDecimal netReturnBps1x; - @TableField("net_return_bps_10x") - private BigDecimal netReturnBps10x; - private Boolean proxyOnly; - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderEvidenceMapper.java b/src/main/java/com/quantai/trader/infrastructure/mapper/TraderEvidenceMapper.java deleted file mode 100644 index 6f7958b..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderEvidenceMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.infrastructure.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.quantai.trader.infrastructure.entity.TraderEvidenceEntity; - -public interface TraderEvidenceMapper extends BaseMapper { -} diff --git a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderPlaybookDefinitionMapper.java b/src/main/java/com/quantai/trader/infrastructure/mapper/TraderPlaybookDefinitionMapper.java deleted file mode 100644 index f5d2b14..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderPlaybookDefinitionMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.infrastructure.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.quantai.trader.infrastructure.entity.TraderPlaybookDefinitionEntity; - -public interface TraderPlaybookDefinitionMapper extends BaseMapper { -} diff --git a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayReportMapper.java b/src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayReportMapper.java deleted file mode 100644 index 17c259d..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayReportMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.infrastructure.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.quantai.trader.infrastructure.entity.TraderReplayReportEntity; - -public interface TraderReplayReportMapper extends BaseMapper { -} diff --git a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayRunMapper.java b/src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayRunMapper.java deleted file mode 100644 index dc97339..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderReplayRunMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.infrastructure.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.quantai.trader.infrastructure.entity.TraderReplayRunEntity; - -public interface TraderReplayRunMapper extends BaseMapper { -} diff --git a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderRiskDecisionMapper.java b/src/main/java/com/quantai/trader/infrastructure/mapper/TraderRiskDecisionMapper.java deleted file mode 100644 index a725ace..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderRiskDecisionMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.infrastructure.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.quantai.trader.infrastructure.entity.TraderRiskDecisionEntity; - -public interface TraderRiskDecisionMapper extends BaseMapper { -} diff --git a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderTrainingSampleMapper.java b/src/main/java/com/quantai/trader/infrastructure/mapper/TraderTrainingSampleMapper.java deleted file mode 100644 index 5f57a7b..0000000 --- a/src/main/java/com/quantai/trader/infrastructure/mapper/TraderTrainingSampleMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quantai.trader.infrastructure.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.quantai.trader.infrastructure.entity.TraderTrainingSampleEntity; - -public interface TraderTrainingSampleMapper extends BaseMapper { -} diff --git a/src/main/java/com/quantai/trader/market/SnapshotBuilder.java b/src/main/java/com/quantai/trader/market/SnapshotBuilder.java deleted file mode 100644 index 22343cb..0000000 --- a/src/main/java/com/quantai/trader/market/SnapshotBuilder.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.quantai.trader.market; - -import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.domain.TraderMarketSnapshot; -import com.quantai.trader.replay.ReplayClockTick; -import com.quantai.trader.state.TraderRuntimeState; -import com.quantai.trader.util.Ids; -import org.springframework.stereotype.Component; - -import java.util.Objects; - -@Component -public class SnapshotBuilder { - - private final TraderProperties properties; - - public SnapshotBuilder(TraderProperties properties) { - this.properties = properties; - } - - public TraderMarketSnapshot build(ReplayClockTick tick, TraderRuntimeState runtimeState) { - String cycleId = Ids.cycleId(runtimeState.runId(), tick.symbol(), tick.eventTime()); - return new TraderMarketSnapshot( - runtimeState.runId(), - cycleId, - Ids.snapshotId(cycleId), - tick.symbol(), - tick.eventTime(), - properties.getFeatureVersion(), - Objects.requireNonNull(tick.contextFeatures(), "contextFeatures is required"), - Objects.requireNonNull(tick.setupFeatures(), "setupFeatures is required"), - Objects.requireNonNull(tick.triggerFeatures(), "triggerFeatures is required"), - Objects.requireNonNull(tick.executionFeatures(), "executionFeatures is required"), - Objects.requireNonNull(tick.dataQuality(), "dataQuality is required"), - Objects.requireNonNull(tick.labelInputs(), "labelInputs is required") - ); - } - - public TraderMarketSnapshot buildNextManagementSnapshot( - TraderMarketSnapshot previous, - ReplayClockTick tick, - TraderRuntimeState runtimeState - ) { - return build(tick, runtimeState); - } -} diff --git a/src/main/java/com/quantai/trader/model/DeterministicTraderModelService.java b/src/main/java/com/quantai/trader/model/DeterministicTraderModelService.java new file mode 100644 index 0000000..a6f9bef --- /dev/null +++ b/src/main/java/com/quantai/trader/model/DeterministicTraderModelService.java @@ -0,0 +1,50 @@ +package com.quantai.trader.model; + +import com.quantai.trader.artifact.TraderArtifactBundle; +import com.quantai.trader.domain.*; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Map; + +@Service +public class DeterministicTraderModelService implements TraderModelService { + @Override + public TraderModelOutput evaluate(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle) { + BigDecimal directionalBias = snapshot.markPrice().compareTo(snapshot.indexPrice()) >= 0 + ? new BigDecimal("0.62") + : new BigDecimal("0.32"); + BigDecimal shortBias = BigDecimal.ONE.subtract(directionalBias).subtract(new BigDecimal("0.10")); + if (shortBias.compareTo(BigDecimal.ZERO) < 0) { + shortBias = BigDecimal.ZERO; + } + BigDecimal neutral = BigDecimal.ONE.subtract(directionalBias).subtract(shortBias); + EntryOutput entry = new EntryOutput( + new BigDecimal("0.63"), new BigDecimal("0.42"), new BigDecimal("0.64"), + new BigDecimal("12.0"), "p0-plan-atr-2r", "p0-price-plan-hash", + new BigDecimal("35"), new BigDecimal("70"), 45, new BigDecimal("4.0"), + "entry-p0", bundle.calibrationBundleVersion(), Map.of("source", "deterministic_fixture")); + return new TraderModelOutput( + "model_output_" + snapshot.cycleId(), + snapshot.runId(), + snapshot.cycleId(), + bundle.modelBundleVersion(), + bundle.calibrationBundleVersion(), + new DirectionOutput(directionalBias, shortBias, neutral, directionalBias, directionalBias.subtract(shortBias).abs(), + new BigDecimal("8.0"), 45, "direction-p0", bundle.calibrationBundleVersion(), Map.of()), + entry, + new ContinueOutput(new BigDecimal("0.61"), new BigDecimal("0.39"), new BigDecimal("0.58"), + new BigDecimal("5.0"), new BigDecimal("3.0"), "continue-p0", bundle.calibrationBundleVersion(), Map.of()), + new ExitOutput(new BigDecimal("0.24"), new BigDecimal("0.48"), new BigDecimal("0.20"), + new BigDecimal("0.25"), new BigDecimal("0.22"), new BigDecimal("0.20"), + new BigDecimal("10"), "exit-p0", bundle.calibrationBundleVersion(), Map.of()), + new RiskOutput(new BigDecimal("0.20"), new BigDecimal("0.18"), + new BigDecimal("20"), new BigDecimal("18"), new BigDecimal("0.15"), + new BigDecimal("20"), new BigDecimal("0.20"), new BigDecimal("0.10"), + new BigDecimal("0.12"), snapshot.depthNotional5Bps().compareTo(BigDecimal.ZERO) > 0 ? new BigDecimal("1.0") : BigDecimal.ZERO, + "risk-p0", bundle.calibrationBundleVersion(), Map.of()), + new BigDecimal("0.10"), + new BigDecimal("0.05"), + Map.of("source", "deterministic_p0_fixture")); + } +} diff --git a/src/main/java/com/quantai/trader/model/TraderModel.java b/src/main/java/com/quantai/trader/model/TraderModel.java deleted file mode 100644 index adb9942..0000000 --- a/src/main/java/com/quantai/trader/model/TraderModel.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.quantai.trader.model; - -import com.quantai.trader.domain.TraderModelManifest; -import com.quantai.trader.domain.TraderModelOutput; - -public interface TraderModel { - - TraderModelOutput predict(TInput input, TraderModelManifest manifest); -} diff --git a/src/main/java/com/quantai/trader/model/TraderModelService.java b/src/main/java/com/quantai/trader/model/TraderModelService.java new file mode 100644 index 0000000..3e722b8 --- /dev/null +++ b/src/main/java/com/quantai/trader/model/TraderModelService.java @@ -0,0 +1,9 @@ +package com.quantai.trader.model; + +import com.quantai.trader.artifact.TraderArtifactBundle; +import com.quantai.trader.domain.TraderMarketSnapshot; +import com.quantai.trader.domain.TraderModelOutput; + +public interface TraderModelService { + TraderModelOutput evaluate(TraderMarketSnapshot snapshot, TraderArtifactBundle bundle); +} diff --git a/src/main/java/com/quantai/trader/outbox/InMemoryOutboxRepository.java b/src/main/java/com/quantai/trader/outbox/InMemoryOutboxRepository.java new file mode 100644 index 0000000..32ea3b7 --- /dev/null +++ b/src/main/java/com/quantai/trader/outbox/InMemoryOutboxRepository.java @@ -0,0 +1,30 @@ +package com.quantai.trader.outbox; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +@Repository +public class InMemoryOutboxRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryOutboxRepository.class); + private final CopyOnWriteArrayList events = new CopyOnWriteArrayList<>(); + + public void insert(TraderOutboxEvent event) { + boolean duplicate = events.stream().anyMatch(existing -> existing.destination().equals(event.destination()) + && existing.idempotencyKey().equals(event.idempotencyKey())); + if (duplicate) { + throw new IllegalArgumentException("duplicate outbox idempotency key: " + event.idempotencyKey()); + } + events.add(event); + log.info("event=trader.outbox.inserted runId={} cycleId={} destination={} aggregateType={} aggregateId={} status={}", + event.runId(), event.cycleId(), event.destination(), event.aggregateType(), event.aggregateId(), event.status()); + } + + public List all() { + return new ArrayList<>(events); + } +} diff --git a/src/main/java/com/quantai/trader/outbox/TraderOutboxEvent.java b/src/main/java/com/quantai/trader/outbox/TraderOutboxEvent.java new file mode 100644 index 0000000..38a50bd --- /dev/null +++ b/src/main/java/com/quantai/trader/outbox/TraderOutboxEvent.java @@ -0,0 +1,34 @@ +package com.quantai.trader.outbox; + +import static com.quantai.trader.util.TraderNumbers.requiredText; + +import java.time.Instant; +import java.util.Map; +import java.util.Objects; + +public record TraderOutboxEvent( + String outboxId, + String runId, + String cycleId, + String aggregateType, + String aggregateId, + String eventType, + String destination, + Map payloadJson, + String idempotencyKey, + String status, + Instant createdAt +) { + public TraderOutboxEvent { + outboxId = requiredText(outboxId, "outboxId"); + runId = requiredText(runId, "runId"); + aggregateType = requiredText(aggregateType, "aggregateType"); + aggregateId = requiredText(aggregateId, "aggregateId"); + eventType = requiredText(eventType, "eventType"); + destination = requiredText(destination, "destination"); + idempotencyKey = requiredText(idempotencyKey, "idempotencyKey"); + status = requiredText(status, "status"); + createdAt = Objects.requireNonNull(createdAt, "createdAt is required"); + payloadJson = Map.copyOf(payloadJson == null ? Map.of() : payloadJson); + } +} diff --git a/src/main/java/com/quantai/trader/persistence/MybatisReplayReportRepository.java b/src/main/java/com/quantai/trader/persistence/MybatisReplayReportRepository.java deleted file mode 100644 index 0f48489..0000000 --- a/src/main/java/com/quantai/trader/persistence/MybatisReplayReportRepository.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.quantai.trader.persistence; - -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.quantai.trader.domain.TraderReplayReport; -import com.quantai.trader.infrastructure.entity.TraderReplayReportEntity; -import com.quantai.trader.infrastructure.mapper.TraderReplayReportMapper; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public class MybatisReplayReportRepository implements ReplayReportRepository { - - private final TraderReplayReportMapper mapper; - - public MybatisReplayReportRepository(TraderReplayReportMapper mapper) { - this.mapper = mapper; - } - - @Override - public void insert(TraderReplayReport report) { - mapper.insert(toEntity(report)); - } - - @Override - public Optional findByRunId(String runId) { - TraderReplayReportEntity entity = mapper.selectOne( - Wrappers.lambdaQuery() - .eq(TraderReplayReportEntity::getRunId, runId) - .orderByDesc(TraderReplayReportEntity::getCreatedAt) - .last("limit 1") - ); - return Optional.ofNullable(entity).map(this::toDomain); - } - - private TraderReplayReportEntity toEntity(TraderReplayReport report) { - TraderReplayReportEntity entity = new TraderReplayReportEntity(); - entity.setRunId(report.runId()); - entity.setReportId(report.reportId()); - entity.setSymbol(report.symbol()); - entity.setPlaybookId(report.playbookId()); - entity.setPlaybookVersion(report.playbookVersion()); - entity.setCandidateEvents(report.candidateEvents()); - entity.setMonthsCovered(report.monthsCovered()); - entity.setBaseNetReturnBps1x(report.baseNetReturnBps1x()); - entity.setLeveragedNetReturnBps10x(report.leveragedNetReturnBps10x()); - entity.setHoldoutReturnBps10x(report.holdoutReturnBps10x()); - entity.setStrictVsLooseJson(TraderPersistenceCodec.json(report.strictVsLoose())); - entity.setFailureRisksJson(TraderPersistenceCodec.json(report.failureRisks())); - entity.setConclusion(report.conclusion()); - entity.setReportPath(report.reportPath()); - entity.setCreatedAt(TraderPersistenceCodec.requiredLocal("report.createdAt", report.createdAt())); - return entity; - } - - private TraderReplayReport toDomain(TraderReplayReportEntity entity) { - return new TraderReplayReport( - entity.getRunId(), - entity.getReportId(), - entity.getSymbol(), - entity.getPlaybookId(), - entity.getPlaybookVersion(), - entity.getCandidateEvents(), - entity.getMonthsCovered(), - entity.getBaseNetReturnBps1x(), - entity.getLeveragedNetReturnBps10x(), - entity.getHoldoutReturnBps10x(), - TraderPersistenceCodec.map(entity.getStrictVsLooseJson()), - TraderPersistenceCodec.stringList(entity.getFailureRisksJson()), - entity.getConclusion(), - entity.getReportPath(), - TraderPersistenceCodec.instant(entity.getCreatedAt()) - ); - } -} diff --git a/src/main/java/com/quantai/trader/persistence/MybatisReplayRunRepository.java b/src/main/java/com/quantai/trader/persistence/MybatisReplayRunRepository.java deleted file mode 100644 index 7927033..0000000 --- a/src/main/java/com/quantai/trader/persistence/MybatisReplayRunRepository.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.quantai.trader.persistence; - -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.quantai.trader.enums.ReplayRunStatus; -import com.quantai.trader.infrastructure.entity.TraderReplayRunEntity; -import com.quantai.trader.infrastructure.mapper.TraderReplayRunMapper; -import com.quantai.trader.replay.ReplayRun; -import com.quantai.trader.replay.ReplayRunConfig; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public class MybatisReplayRunRepository implements ReplayRunRepository { - - private final TraderReplayRunMapper mapper; - - public MybatisReplayRunRepository(TraderReplayRunMapper mapper) { - this.mapper = mapper; - } - - @Override - public void insert(ReplayRun run) { - mapper.insert(toEntity(run)); - } - - @Override - public void update(ReplayRun run) { - int updated = mapper.update( - null, - Wrappers.lambdaUpdate() - .set(TraderReplayRunEntity::getStatus, run.status().name()) - .set(TraderReplayRunEntity::getConfigJson, TraderPersistenceCodec.json(run.config())) - .set(TraderReplayRunEntity::getStartedAt, TraderPersistenceCodec.local(run.startedAt())) - .set(TraderReplayRunEntity::getFinishedAt, TraderPersistenceCodec.local(run.finishedAt())) - .set(TraderReplayRunEntity::getFailureReason, run.failureReason()) - .eq(TraderReplayRunEntity::getRunId, run.runId()) - ); - if (updated == 0) { - throw new IllegalStateException("replay run not found for update: " + run.runId()); - } - } - - @Override - public Optional findByRunId(String runId) { - TraderReplayRunEntity entity = mapper.selectOne( - Wrappers.lambdaQuery() - .eq(TraderReplayRunEntity::getRunId, runId) - ); - return Optional.ofNullable(entity).map(this::toDomain); - } - - private TraderReplayRunEntity toEntity(ReplayRun run) { - ReplayRunConfig config = run.config(); - TraderReplayRunEntity entity = new TraderReplayRunEntity(); - entity.setRunId(run.runId()); - entity.setRunMode("REPLAY"); - entity.setSymbol(config.symbol()); - entity.setPlaybookId(config.playbookId()); - entity.setPlaybookVersion(config.playbookVersion()); - entity.setPlaybookDefinitionHash(run.playbookDefinitionHash()); - entity.setDataFrom(TraderPersistenceCodec.local(config.from())); - entity.setDataTo(TraderPersistenceCodec.local(config.to())); - entity.setFeatureVersion(config.featureVersion()); - entity.setLabelVersion(config.labelVersion()); - entity.setDataSourceManifestJson(TraderPersistenceCodec.json(config.dataSources())); - entity.setStatus(run.status().name()); - entity.setConfigJson(TraderPersistenceCodec.json(config)); - entity.setStartedAt(TraderPersistenceCodec.local(run.startedAt())); - entity.setFinishedAt(TraderPersistenceCodec.local(run.finishedAt())); - entity.setFailureReason(run.failureReason()); - entity.setCreatedAt(TraderPersistenceCodec.requiredLocal("run.createdAt", run.createdAt())); - return entity; - } - - private ReplayRun toDomain(TraderReplayRunEntity entity) { - return new ReplayRun( - entity.getRunId(), - ReplayRunStatus.valueOf(entity.getStatus()), - TraderPersistenceCodec.replayRunConfig(entity.getConfigJson()), - entity.getPlaybookDefinitionHash(), - TraderPersistenceCodec.instant(entity.getCreatedAt()), - TraderPersistenceCodec.instant(entity.getStartedAt()), - TraderPersistenceCodec.instant(entity.getFinishedAt()), - entity.getFailureReason() - ); - } -} diff --git a/src/main/java/com/quantai/trader/persistence/MybatisTraderEvidenceRepository.java b/src/main/java/com/quantai/trader/persistence/MybatisTraderEvidenceRepository.java deleted file mode 100644 index fd99334..0000000 --- a/src/main/java/com/quantai/trader/persistence/MybatisTraderEvidenceRepository.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.quantai.trader.persistence; - -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.quantai.trader.domain.TraderEvidence; -import com.quantai.trader.infrastructure.entity.TraderEvidenceEntity; -import com.quantai.trader.infrastructure.mapper.TraderEvidenceMapper; -import org.springframework.stereotype.Repository; - -import java.time.Instant; -import java.util.List; - -@Repository -public class MybatisTraderEvidenceRepository implements TraderEvidenceRepository { - - private final TraderEvidenceMapper mapper; - - public MybatisTraderEvidenceRepository(TraderEvidenceMapper mapper) { - this.mapper = mapper; - } - - @Override - public void insert(TraderEvidence evidence) { - TraderEvidenceEntity entity = new TraderEvidenceEntity(); - entity.setRunId(evidence.runId()); - entity.setCycleId(evidence.cycleId()); - entity.setEvidenceId(evidence.evidenceId()); - entity.setStage(evidence.stage()); - entity.setPass(evidence.pass()); - entity.setReason(evidence.reason()); - entity.setBlocker(evidence.blocker()); - entity.setEvidenceTime(TraderPersistenceCodec.local(evidence.evidenceTime())); - entity.setDetailsJson(TraderPersistenceCodec.json(evidence.details())); - entity.setCreatedAt(TraderPersistenceCodec.local(Instant.now())); - mapper.insert(entity); - } - - @Override - public List findByCycleId(String runId, String cycleId) { - return mapper.selectList( - Wrappers.lambdaQuery() - .eq(TraderEvidenceEntity::getRunId, runId) - .eq(TraderEvidenceEntity::getCycleId, cycleId) - .orderByAsc(TraderEvidenceEntity::getEvidenceTime) - .orderByAsc(TraderEvidenceEntity::getId) - ).stream().map(this::toDomain).toList(); - } - - private TraderEvidence toDomain(TraderEvidenceEntity entity) { - return new TraderEvidence( - entity.getRunId(), - entity.getCycleId(), - entity.getEvidenceId(), - entity.getStage(), - Boolean.TRUE.equals(entity.getPass()), - entity.getReason(), - entity.getBlocker(), - TraderPersistenceCodec.instant(entity.getEvidenceTime()), - TraderPersistenceCodec.map(entity.getDetailsJson()) - ); - } -} diff --git a/src/main/java/com/quantai/trader/persistence/MybatisTraderPlaybookDefinitionRepository.java b/src/main/java/com/quantai/trader/persistence/MybatisTraderPlaybookDefinitionRepository.java deleted file mode 100644 index 4632704..0000000 --- a/src/main/java/com/quantai/trader/persistence/MybatisTraderPlaybookDefinitionRepository.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.quantai.trader.persistence; - -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.enums.TraderErrorCode; -import com.quantai.trader.infrastructure.entity.TraderPlaybookDefinitionEntity; -import com.quantai.trader.infrastructure.mapper.TraderPlaybookDefinitionMapper; -import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot; -import org.springframework.stereotype.Repository; - -import java.time.Instant; -import java.util.Objects; -import java.util.Optional; - -@Repository -public class MybatisTraderPlaybookDefinitionRepository implements TraderPlaybookDefinitionRepository { - - private final TraderPlaybookDefinitionMapper mapper; - - public MybatisTraderPlaybookDefinitionRepository(TraderPlaybookDefinitionMapper mapper) { - this.mapper = mapper; - } - - @Override - public void insertPlaybookDefinitionIfAbsent(TraderPlaybookDefinitionSnapshot definition) { - Optional existing = findPlaybookDefinition( - definition.playbookId(), - definition.playbookVersion() - ); - if (existing.isPresent()) { - if (!existing.get().definitionHashSha256().equals(definition.definitionHashSha256())) { - throw new TraderException( - TraderErrorCode.TRADER_PLAYBOOK_VERSION_CONFLICT, - "playbook version already exists with another definition hash" - ); - } - return; - } - mapper.insert(toEntity(definition)); - } - - @Override - public Optional findPlaybookDefinition(String playbookId, String playbookVersion) { - TraderPlaybookDefinitionEntity entity = mapper.selectOne( - Wrappers.lambdaQuery() - .eq(TraderPlaybookDefinitionEntity::getPlaybookId, playbookId) - .eq(TraderPlaybookDefinitionEntity::getPlaybookVersion, playbookVersion) - ); - return Optional.ofNullable(entity).map(this::toDomain); - } - - private TraderPlaybookDefinitionEntity toEntity(TraderPlaybookDefinitionSnapshot definition) { - TraderPlaybookDefinitionEntity entity = new TraderPlaybookDefinitionEntity(); - entity.setPlaybookId(definition.playbookId()); - entity.setPlaybookVersion(definition.playbookVersion()); - entity.setFamily(definition.family()); - entity.setVariant(definition.variant()); - entity.setSideMode(Objects.requireNonNull(definition.definition(), "playbook definition is required").sideMode()); - entity.setSourcePath(definition.sourcePath()); - entity.setDefinitionHashSha256(definition.definitionHashSha256()); - entity.setDefinitionJson(definition.definitionJson()); - entity.setLoadedAt(TraderPersistenceCodec.local(definition.loadedAt())); - entity.setStatus(definition.status()); - entity.setCreatedAt(TraderPersistenceCodec.local(Instant.now())); - return entity; - } - - private TraderPlaybookDefinitionSnapshot toDomain(TraderPlaybookDefinitionEntity entity) { - String definitionJson = entity.getDefinitionJson(); - return new TraderPlaybookDefinitionSnapshot( - entity.getPlaybookId(), - entity.getPlaybookVersion(), - entity.getFamily(), - entity.getVariant(), - entity.getSourcePath(), - entity.getDefinitionHashSha256(), - definitionJson, - TraderPersistenceCodec.instant(entity.getLoadedAt()), - entity.getStatus(), - TraderPersistenceCodec.playbookDefinition(definitionJson) - ); - } -} diff --git a/src/main/java/com/quantai/trader/persistence/MybatisTraderRiskDecisionRepository.java b/src/main/java/com/quantai/trader/persistence/MybatisTraderRiskDecisionRepository.java deleted file mode 100644 index b8be1f2..0000000 --- a/src/main/java/com/quantai/trader/persistence/MybatisTraderRiskDecisionRepository.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.quantai.trader.persistence; - -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.quantai.trader.domain.TraderRiskDecision; -import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.infrastructure.entity.TraderRiskDecisionEntity; -import com.quantai.trader.infrastructure.mapper.TraderRiskDecisionMapper; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public class MybatisTraderRiskDecisionRepository implements TraderRiskDecisionRepository { - - private final TraderRiskDecisionMapper mapper; - - public MybatisTraderRiskDecisionRepository(TraderRiskDecisionMapper mapper) { - this.mapper = mapper; - } - - @Override - public void insert(TraderRiskDecision decision) { - TraderRiskDecisionEntity entity = new TraderRiskDecisionEntity(); - entity.setRunId(decision.runId()); - entity.setCycleId(decision.cycleId()); - entity.setActionId(decision.actionId()); - entity.setAccountStateId(decision.accountStateId()); - entity.setActionType(decision.actionType().name()); - entity.setLeverageScreen(decision.leverageScreen()); - entity.setPlannedTotalPositionRatio(decision.plannedTotalPositionRatio()); - entity.setMaxLossBps(decision.maxLossBps()); - entity.setLiquidationBufferBps(decision.liquidationBufferBps()); - entity.setExpectedValueBps1x(decision.expectedValueBps1x()); - entity.setExpectedValueBps10x(decision.expectedValueBps10x()); - entity.setUncertainty(decision.uncertainty()); - entity.setOodScore(decision.oodScore()); - entity.setAllowAction(decision.allowAction()); - entity.setBlocker(decision.blocker()); - entity.setDecisionJson(TraderPersistenceCodec.json(decision.decision())); - entity.setCreatedAt(TraderPersistenceCodec.requiredLocal("decision.createdAt", decision.createdAt())); - mapper.insert(entity); - } - - @Override - public List findByCycleId(String runId, String cycleId) { - return mapper.selectList( - Wrappers.lambdaQuery() - .eq(TraderRiskDecisionEntity::getRunId, runId) - .eq(TraderRiskDecisionEntity::getCycleId, cycleId) - .orderByAsc(TraderRiskDecisionEntity::getCreatedAt) - .orderByAsc(TraderRiskDecisionEntity::getId) - ).stream().map(this::toDomain).toList(); - } - - private TraderRiskDecision toDomain(TraderRiskDecisionEntity entity) { - return new TraderRiskDecision( - entity.getRunId(), - entity.getCycleId(), - entity.getActionId(), - entity.getAccountStateId(), - TraderActionType.valueOf(entity.getActionType()), - entity.getLeverageScreen(), - entity.getPlannedTotalPositionRatio(), - entity.getMaxLossBps(), - entity.getLiquidationBufferBps(), - entity.getExpectedValueBps1x(), - entity.getExpectedValueBps10x(), - entity.getUncertainty(), - entity.getOodScore(), - Boolean.TRUE.equals(entity.getAllowAction()), - entity.getBlocker(), - TraderPersistenceCodec.map(entity.getDecisionJson()), - TraderPersistenceCodec.instant(entity.getCreatedAt()) - ); - } -} diff --git a/src/main/java/com/quantai/trader/persistence/MybatisTraderSampleRepository.java b/src/main/java/com/quantai/trader/persistence/MybatisTraderSampleRepository.java deleted file mode 100644 index 458e913..0000000 --- a/src/main/java/com/quantai/trader/persistence/MybatisTraderSampleRepository.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.quantai.trader.persistence; - -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.quantai.trader.domain.TraderTrainingSample; -import com.quantai.trader.infrastructure.entity.TraderTrainingSampleEntity; -import com.quantai.trader.infrastructure.mapper.TraderTrainingSampleMapper; -import org.springframework.stereotype.Repository; - -import java.time.Instant; -import java.util.List; - -@Repository -public class MybatisTraderSampleRepository implements TraderSampleRepository { - - private final TraderTrainingSampleMapper mapper; - - public MybatisTraderSampleRepository(TraderTrainingSampleMapper mapper) { - this.mapper = mapper; - } - - @Override - public void insert(TraderTrainingSample sample) { - TraderTrainingSampleEntity entity = new TraderTrainingSampleEntity(); - entity.setRunId(sample.runId()); - entity.setCycleId(sample.cycleId()); - entity.setSampleId(sample.sampleId()); - entity.setActionId(sample.actionId()); - entity.setPositionId(sample.positionId()); - entity.setFeatureVersion(sample.featureVersion()); - entity.setLabelVersion(sample.labelVersion()); - entity.setSampleTime(TraderPersistenceCodec.local(sample.sampleTime())); - entity.setFeaturesJson(TraderPersistenceCodec.json(sample.features())); - entity.setLabelsJson(TraderPersistenceCodec.json(sample.labels())); - entity.setNetReturnBps1x(sample.netReturnBps1x()); - entity.setNetReturnBps10x(sample.netReturnBps10x()); - entity.setProxyOnly(sample.proxyOnly()); - entity.setCreatedAt(TraderPersistenceCodec.local(Instant.now())); - mapper.insert(entity); - } - - @Override - public List findByRunId(String runId) { - return mapper.selectList( - Wrappers.lambdaQuery() - .eq(TraderTrainingSampleEntity::getRunId, runId) - .orderByAsc(TraderTrainingSampleEntity::getSampleTime) - .orderByAsc(TraderTrainingSampleEntity::getId) - ).stream().map(this::toDomain).toList(); - } - - private TraderTrainingSample toDomain(TraderTrainingSampleEntity entity) { - return new TraderTrainingSample( - entity.getRunId(), - entity.getCycleId(), - entity.getSampleId(), - entity.getActionId(), - entity.getPositionId(), - entity.getFeatureVersion(), - entity.getLabelVersion(), - TraderPersistenceCodec.instant(entity.getSampleTime()), - TraderPersistenceCodec.map(entity.getFeaturesJson()), - TraderPersistenceCodec.map(entity.getLabelsJson()), - entity.getNetReturnBps1x(), - entity.getNetReturnBps10x(), - Boolean.TRUE.equals(entity.getProxyOnly()) - ); - } -} diff --git a/src/main/java/com/quantai/trader/persistence/ReplayReportRepository.java b/src/main/java/com/quantai/trader/persistence/ReplayReportRepository.java deleted file mode 100644 index c8788e1..0000000 --- a/src/main/java/com/quantai/trader/persistence/ReplayReportRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.domain.TraderReplayReport; - -import java.util.Optional; - -public interface ReplayReportRepository { - - void insert(TraderReplayReport report); - - Optional findByRunId(String runId); -} diff --git a/src/main/java/com/quantai/trader/persistence/ReplayRunRepository.java b/src/main/java/com/quantai/trader/persistence/ReplayRunRepository.java deleted file mode 100644 index 261a22a..0000000 --- a/src/main/java/com/quantai/trader/persistence/ReplayRunRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.replay.ReplayRun; - -import java.util.Optional; - -public interface ReplayRunRepository { - - void insert(ReplayRun run); - - void update(ReplayRun run); - - Optional findByRunId(String runId); -} diff --git a/src/main/java/com/quantai/trader/persistence/TraderEvidenceRepository.java b/src/main/java/com/quantai/trader/persistence/TraderEvidenceRepository.java deleted file mode 100644 index f092047..0000000 --- a/src/main/java/com/quantai/trader/persistence/TraderEvidenceRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.domain.TraderEvidence; - -import java.util.List; - -public interface TraderEvidenceRepository { - - void insert(TraderEvidence evidence); - - List findByCycleId(String runId, String cycleId); -} diff --git a/src/main/java/com/quantai/trader/persistence/TraderPersistenceCodec.java b/src/main/java/com/quantai/trader/persistence/TraderPersistenceCodec.java deleted file mode 100644 index d6154ff..0000000 --- a/src/main/java/com/quantai/trader/persistence/TraderPersistenceCodec.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.quantai.trader.persistence; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.quantai.trader.playbook.TraderPlaybookDefinition; -import com.quantai.trader.replay.ReplayRunConfig; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -final class TraderPersistenceCodec { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().findAndRegisterModules(); - private static final TypeReference> MAP_TYPE = new TypeReference<>() { - }; - private static final TypeReference> STRING_LIST_TYPE = new TypeReference<>() { - }; - - private TraderPersistenceCodec() { - } - - static String json(Object value) { - try { - return OBJECT_MAPPER.writeValueAsString(Objects.requireNonNull(value, "json value is required")); - } catch (Exception ex) { - throw new IllegalStateException("failed to serialize trader persistence json", ex); - } - } - - static Map map(String json) { - try { - return OBJECT_MAPPER.readValue(requiredJson(json), MAP_TYPE); - } catch (Exception ex) { - throw new IllegalStateException("failed to deserialize trader persistence json map", ex); - } - } - - static List stringList(String json) { - try { - return OBJECT_MAPPER.readValue(requiredJson(json), STRING_LIST_TYPE); - } catch (Exception ex) { - throw new IllegalStateException("failed to deserialize trader persistence json list", ex); - } - } - - static ReplayRunConfig replayRunConfig(String json) { - try { - return OBJECT_MAPPER.readValue(requiredJson(json), ReplayRunConfig.class); - } catch (Exception ex) { - throw new IllegalStateException("failed to deserialize replay run config", ex); - } - } - - static TraderPlaybookDefinition playbookDefinition(String json) { - try { - return OBJECT_MAPPER.readValue(requiredJson(json), TraderPlaybookDefinition.class); - } catch (Exception ex) { - throw new IllegalStateException("failed to deserialize playbook definition", ex); - } - } - - static LocalDateTime local(Instant value) { - return value == null ? null : LocalDateTime.ofInstant(value, ZoneOffset.UTC); - } - - static Instant instant(LocalDateTime value) { - return value == null ? null : value.toInstant(ZoneOffset.UTC); - } - - static LocalDateTime requiredLocal(String fieldName, Instant value) { - return LocalDateTime.ofInstant(Objects.requireNonNull(value, fieldName + " is required"), ZoneOffset.UTC); - } - - private static String requiredJson(String json) { - Objects.requireNonNull(json, "json text is required"); - String trimmed = json.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("json text is blank"); - } - return trimmed; - } -} diff --git a/src/main/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepository.java b/src/main/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepository.java deleted file mode 100644 index 385594d..0000000 --- a/src/main/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot; - -import java.util.Optional; - -public interface TraderPlaybookDefinitionRepository { - - void insertPlaybookDefinitionIfAbsent(TraderPlaybookDefinitionSnapshot definition); - - Optional findPlaybookDefinition(String playbookId, String playbookVersion); -} diff --git a/src/main/java/com/quantai/trader/persistence/TraderRiskDecisionRepository.java b/src/main/java/com/quantai/trader/persistence/TraderRiskDecisionRepository.java deleted file mode 100644 index d43edb7..0000000 --- a/src/main/java/com/quantai/trader/persistence/TraderRiskDecisionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.domain.TraderRiskDecision; - -import java.util.List; - -public interface TraderRiskDecisionRepository { - - void insert(TraderRiskDecision decision); - - List findByCycleId(String runId, String cycleId); -} diff --git a/src/main/java/com/quantai/trader/persistence/TraderSampleRepository.java b/src/main/java/com/quantai/trader/persistence/TraderSampleRepository.java deleted file mode 100644 index 77c8684..0000000 --- a/src/main/java/com/quantai/trader/persistence/TraderSampleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.domain.TraderTrainingSample; - -import java.util.List; - -public interface TraderSampleRepository { - - void insert(TraderTrainingSample sample); - - List findByRunId(String runId); -} diff --git a/src/main/java/com/quantai/trader/playbook/RuleDefinition.java b/src/main/java/com/quantai/trader/playbook/RuleDefinition.java deleted file mode 100644 index 5415af4..0000000 --- a/src/main/java/com/quantai/trader/playbook/RuleDefinition.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.quantai.trader.playbook; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record RuleDefinition( - String name, - String description, - @JsonProperty("max_planned_entry_legs") - Integer maxPlannedEntryLegs, - @JsonProperty("ratio_mode") - String ratioMode, - @JsonProperty("allow_full_initial_entry") - Boolean allowFullInitialEntry, - @JsonProperty("ratio_template_fixed") - Boolean ratioTemplateFixed, - @JsonProperty("description_ext") - String descriptionExt -) { -} diff --git a/src/main/java/com/quantai/trader/playbook/TraderPlaybookCatalog.java b/src/main/java/com/quantai/trader/playbook/TraderPlaybookCatalog.java deleted file mode 100644 index 1932d57..0000000 --- a/src/main/java/com/quantai/trader/playbook/TraderPlaybookCatalog.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.quantai.trader.playbook; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.persistence.TraderPlaybookDefinitionRepository; -import jakarta.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.time.Instant; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@Component -public class TraderPlaybookCatalog { - - private static final Logger log = LoggerFactory.getLogger(TraderPlaybookCatalog.class); - private final TraderProperties properties; - private final TraderPlaybookValidator validator; - private final TraderPlaybookDefinitionRepository repository; - private final ObjectMapper yamlMapper; - private final ObjectMapper jsonMapper; - private volatile Map definitions = Map.of(); - - public TraderPlaybookCatalog( - TraderProperties properties, - TraderPlaybookValidator validator, - TraderPlaybookDefinitionRepository repository - ) { - this.properties = properties; - this.validator = validator; - this.repository = repository; - this.yamlMapper = new ObjectMapper(new YAMLFactory()); - this.jsonMapper = JsonMapper.builder() - .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) - .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) - .build(); - } - - @PostConstruct - public void load() { - try { - Resource[] resources = new PathMatchingResourcePatternResolver() - .getResources(properties.getPlaybook().getLocationPattern()); - Map loaded = new LinkedHashMap<>(); - for (Resource resource : resources) { - TraderPlaybookDefinitionSnapshot snapshot = loadResource(resource); - repository.insertPlaybookDefinitionIfAbsent(snapshot); - loaded.put(snapshot.playbookId(), snapshot); - log.info( - "event=trader.playbook.loaded playbookId={} playbookVersion={} variant={} definitionHashSha256={} sourcePath={}", - snapshot.playbookId(), - snapshot.playbookVersion(), - snapshot.variant(), - snapshot.definitionHashSha256(), - snapshot.sourcePath() - ); - } - this.definitions = Map.copyOf(loaded); - } catch (IOException ex) { - throw new IllegalStateException("failed to load trader playbooks", ex); - } - } - - public List list() { - return definitions.values().stream() - .sorted(Comparator.comparing(TraderPlaybookDefinitionSnapshot::playbookId)) - .toList(); - } - - public TraderPlaybookDefinitionSnapshot require(String playbookId) { - TraderPlaybookDefinitionSnapshot snapshot = definitions.get(playbookId); - if (snapshot == null) { - throw new TraderPlaybookValidationException("playbook not loaded: " + playbookId); - } - return snapshot; - } - - public TraderPlaybookDefinitionSnapshot require(String playbookId, String version) { - TraderPlaybookDefinitionSnapshot snapshot = require(playbookId); - if (!snapshot.playbookVersion().equals(version)) { - throw new TraderPlaybookValidationException("playbook version not loaded: " + playbookId + "/" + version); - } - return snapshot; - } - - private TraderPlaybookDefinitionSnapshot loadResource(Resource resource) throws IOException { - try (InputStream inputStream = resource.getInputStream()) { - TraderPlaybookDefinition definition = yamlMapper.readValue(inputStream, TraderPlaybookDefinition.class); - validator.validate(definition); - String definitionJson = normalizedJson(definition); - return new TraderPlaybookDefinitionSnapshot( - definition.playbookId(), - definition.playbookVersion(), - definition.family(), - definition.variant(), - resource.getDescription(), - sha256(definitionJson), - definitionJson, - Instant.now(), - "ACTIVE", - definition - ); - } - } - - private String normalizedJson(TraderPlaybookDefinition definition) throws IOException { - JsonNode node = jsonMapper.valueToTree(definition); - return jsonMapper.writeValueAsString(node); - } - - private static String sha256(String value) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8)); - StringBuilder builder = new StringBuilder(bytes.length * 2); - for (byte b : bytes) { - builder.append(String.format("%02x", b)); - } - return builder.toString(); - } catch (NoSuchAlgorithmException ex) { - throw new IllegalStateException("SHA-256 digest is unavailable", ex); - } - } -} diff --git a/src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinition.java b/src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinition.java deleted file mode 100644 index 741a176..0000000 --- a/src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinition.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.quantai.trader.playbook; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.Map; - -public record TraderPlaybookDefinition( - @JsonProperty("playbook_id") - String playbookId, - @JsonProperty("playbook_version") - String playbookVersion, - String family, - String variant, - @JsonProperty("side_mode") - String sideMode, - @JsonProperty("context_timeframes") - List contextTimeframes, - @JsonProperty("setup_timeframes") - List setupTimeframes, - @JsonProperty("trigger_timeframes") - List triggerTimeframes, - @JsonProperty("execution_window") - String executionWindow, - @JsonProperty("management_windows") - List managementWindows, - @JsonProperty("entry_rule") - RuleDefinition entryRule, - @JsonProperty("planned_entry_leg_rule") - RuleDefinition plannedEntryLegRule, - @JsonProperty("invalid_rule") - RuleDefinition invalidRule, - @JsonProperty("stop_rule") - RuleDefinition stopRule, - @JsonProperty("target_rule") - RuleDefinition targetRule, - @JsonProperty("partial_take_profit_rule") - RuleDefinition partialTakeProfitRule, - @JsonProperty("max_hold_rule") - RuleDefinition maxHoldRule, - @JsonProperty("failure_exit_rule") - RuleDefinition failureExitRule, - @JsonProperty("risk_constraints") - Map riskConstraints, - @JsonProperty("required_features") - List requiredFeatures, - @JsonProperty("output_actions") - List outputActions -) { - - public List outputActions() { - return outputActions == null ? List.of() : List.copyOf(outputActions); - } -} diff --git a/src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinitionSnapshot.java b/src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinitionSnapshot.java deleted file mode 100644 index 1a75efc..0000000 --- a/src/main/java/com/quantai/trader/playbook/TraderPlaybookDefinitionSnapshot.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.quantai.trader.playbook; - -import java.time.Instant; - -public record TraderPlaybookDefinitionSnapshot( - String playbookId, - String playbookVersion, - String family, - String variant, - String sourcePath, - String definitionHashSha256, - String definitionJson, - Instant loadedAt, - String status, - TraderPlaybookDefinition definition -) { -} diff --git a/src/main/java/com/quantai/trader/playbook/TraderPlaybookValidationException.java b/src/main/java/com/quantai/trader/playbook/TraderPlaybookValidationException.java deleted file mode 100644 index 77e4dc4..0000000 --- a/src/main/java/com/quantai/trader/playbook/TraderPlaybookValidationException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.quantai.trader.playbook; - -public class TraderPlaybookValidationException extends RuntimeException { - - public TraderPlaybookValidationException(String message) { - super(message); - } -} diff --git a/src/main/java/com/quantai/trader/playbook/TraderPlaybookValidator.java b/src/main/java/com/quantai/trader/playbook/TraderPlaybookValidator.java deleted file mode 100644 index e9ddebf..0000000 --- a/src/main/java/com/quantai/trader/playbook/TraderPlaybookValidator.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.quantai.trader.playbook; - -import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.enums.TraderPlaybookId; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Set; - -@Component -public class TraderPlaybookValidator { - - private static final Set REQUIRED_FEATURES = Set.of("candles", "trades", "level_1"); - - public void validate(TraderPlaybookDefinition definition) { - requireText(definition.playbookId(), "playbook_id is required"); - requireText(definition.playbookVersion(), "playbook_version is required"); - requireEnum(definition.playbookId()); - requireRule(definition.entryRule(), "entry_rule is required"); - requireRule(definition.plannedEntryLegRule(), "planned_entry_leg_rule is required"); - requireRule(definition.invalidRule(), "invalid_rule is required"); - requireRule(definition.stopRule(), "stop_rule is required"); - requireRule(definition.targetRule(), "target_rule is required"); - requireRule(definition.maxHoldRule(), "max_hold_rule is required"); - requireOutputActions(definition.outputActions()); - requirePlannedLegBoundary(definition); - requireRequiredFeatures(definition.requiredFeatures()); - } - - private void requireEnum(String playbookId) { - try { - TraderPlaybookId.valueOf(playbookId); - } catch (IllegalArgumentException ex) { - throw new TraderPlaybookValidationException("unknown playbook_id: " + playbookId); - } - } - - private void requireOutputActions(List outputActions) { - if (outputActions == null || outputActions.isEmpty()) { - throw new TraderPlaybookValidationException("output_actions is required"); - } - if (outputActions.contains("SCALE_IN")) { - throw new TraderPlaybookValidationException("SCALE_IN is not a P0 action"); - } - for (String action : outputActions) { - try { - TraderActionType.valueOf(action); - } catch (IllegalArgumentException ex) { - throw new TraderPlaybookValidationException("unsupported output action: " + action); - } - } - } - - private void requirePlannedLegBoundary(TraderPlaybookDefinition definition) { - if (definition.outputActions().contains(TraderActionType.OPEN_PLANNED_LEG.name()) - && definition.plannedEntryLegRule() == null) { - throw new TraderPlaybookValidationException("OPEN_PLANNED_LEG requires planned_entry_leg_rule"); - } - RuleDefinition rule = definition.plannedEntryLegRule(); - if (rule != null) { - int maxLegs = rule.maxPlannedEntryLegs() == null ? 0 : rule.maxPlannedEntryLegs(); - if (maxLegs < 1 || maxLegs > 3) { - throw new TraderPlaybookValidationException("max_planned_entry_legs must be between 1 and 3"); - } - if (Boolean.TRUE.equals(rule.ratioTemplateFixed())) { - throw new TraderPlaybookValidationException("fixed leg ratio templates are not allowed in P0"); - } - } - } - - private void requireRequiredFeatures(List requiredFeatures) { - if (requiredFeatures == null || !requiredFeatures.containsAll(REQUIRED_FEATURES)) { - throw new TraderPlaybookValidationException("required_features must include candles, trades, and level_1"); - } - } - - private void requireRule(RuleDefinition rule, String message) { - if (rule == null || rule.name() == null || rule.name().isBlank()) { - throw new TraderPlaybookValidationException(message); - } - } - - private void requireText(String value, String message) { - if (value == null || value.isBlank()) { - throw new TraderPlaybookValidationException(message); - } - } -} diff --git a/src/main/java/com/quantai/trader/position/TraderPositionManager.java b/src/main/java/com/quantai/trader/position/TraderPositionManager.java index 8ff3f5b..008e47e 100644 --- a/src/main/java/com/quantai/trader/position/TraderPositionManager.java +++ b/src/main/java/com/quantai/trader/position/TraderPositionManager.java @@ -1,247 +1,210 @@ package com.quantai.trader.position; -import com.quantai.trader.domain.TraderAction; -import com.quantai.trader.domain.TraderDecisionCycle; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.domain.TraderMarketSnapshot; -import com.quantai.trader.domain.TraderPositionPath; +import com.quantai.trader.domain.*; +import com.quantai.trader.enums.PositionSide; import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.enums.TraderErrorCode; -import com.quantai.trader.util.Ids; -import org.springframework.stereotype.Component; +import com.quantai.trader.util.TraderNumbers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; import java.math.BigDecimal; -import java.time.Instant; +import java.math.MathContext; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -@Component +@Service public class TraderPositionManager { + private static final Logger log = LoggerFactory.getLogger(TraderPositionManager.class); + private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000"); - private final Map pathsByPosition = new ConcurrentHashMap<>(); - - public TraderPositionPath simulateOrUpdate( - TraderDecisionCycle cycle, - TraderAction action, - TraderMarketSnapshot snapshot - ) { - return switch (action.actionType()) { - case OPEN_INITIAL -> openInitialProxy(cycle, action, snapshot); - case OPEN_PLANNED_LEG -> appendPlannedLegProxy(cycle, action, snapshot); - case HOLD, MOVE_STOP, CANCEL, REQUOTE -> updatePathOnly(cycle, action, snapshot); - case REDUCE -> reducePosition(cycle, action, snapshot); - case CLOSE -> closePosition(cycle, action, snapshot); - case WAIT -> noPositionChange(cycle, action, snapshot); - }; + public TraderPositionManagerDecision decide(PositionManagerInput input) { + if (!input.snapshot().dataReady()) { + return waitDecision(input, "DATA_NOT_READY"); + } + TraderPositionManagerDecision decision = input.positionState().isFlat() + ? decideWhenFlat(input) + : decideExistingPosition(input); + log.info("event=trader.pm.decided runId={} cycleId={} action={} side={} reason={}", + decision.runId(), decision.cycleId(), decision.candidateAction(), decision.side(), decision.reason()); + return decision; } - private TraderPositionPath openInitialProxy(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) { - BigDecimal ratio = contextDecimal(action, "plannedLegRatio"); - String positionId = Ids.positionId(cycle.runId(), cycle.symbol(), action.side().name(), 1); - TraderPositionPath path = new TraderPositionPath( - cycle.runId(), - cycle.cycleId(), - action.actionId(), - positionId, - action.side(), - action.actionTime(), - snapshot.snapshotTime(), - action.price(), - currentPrice(action, snapshot), - BigDecimal.ZERO, - BigDecimal.ZERO, - null, - null, - false, - false, - true, - false, - 1, - ratio, - Map.of("proxyFill", true, "lastAction", action.actionType().name()) - ); - pathsByPosition.put(positionId, path); - return path; + public BigDecimal calculateInitialRatio(PositionManagerInput input, PositionSide side) { + TraderPmConfig.SizingConfig sizing = input.pmConfig().sizing(); + EntryOutput entry = input.modelOutput().entry(); + RiskOutput risk = input.modelOutput().risk(); + BigDecimal expectedEdge = entry.expectedEdgeBps().max(BigDecimal.ZERO); + if (expectedEdge.compareTo(sizing.minEdgeBps()) < 0) { + return BigDecimal.ZERO; + } + BigDecimal directionStrength = side.isLong() + ? input.modelOutput().direction().longProb() + : input.modelOutput().direction().shortProb(); + BigDecimal entryProb = side.isLong() ? entry.longEntryProb() : entry.shortEntryProb(); + BigDecimal stopLossBudget = entry.stopDistanceBps().add(entry.costBps()).max(BigDecimal.ONE); + BigDecimal uncertaintyPenalty = BigDecimal.ONE.subtract(input.modelOutput().uncertainty() + .multiply(sizing.uncertaintyPenaltyMultiplier())).max(BigDecimal.ZERO); + BigDecimal edgeRiskBudget = TraderNumbers.safeDivide(expectedEdge, stopLossBudget) + .multiply(directionStrength) + .multiply(entryProb) + .multiply(BigDecimal.ONE.subtract(risk.marketRiskProb())) + .multiply(uncertaintyPenalty); + BigDecimal raw = sizing.baseRatio().multiply(edgeRiskBudget); + BigDecimal liquidityCap = risk.liquidityCapacityRatio().multiply(sizing.maxLiquidityUsageRatio()); + BigDecimal lossBudgetCap = TraderNumbers.safeDivide(sizing.maxLossPerTradeBps(), stopLossBudget); + BigDecimal hardCap = min(sizing.maxSingleLegRatio(), min(input.accountState().remainingSymbolCapacityRatio(), min(liquidityCap, lossBudgetCap))); + if (hardCap.compareTo(sizing.minInitialRatio()) < 0) { + return BigDecimal.ZERO; + } + return TraderNumbers.clamp(raw, sizing.minInitialRatio(), hardCap); } - private TraderPositionPath appendPlannedLegProxy(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) { - String positionId = String.valueOf(action.actionContext().get("positionId")); - TraderPositionPath existing = pathsByPosition.get(positionId); - if (existing == null) { - throw new TraderException(TraderErrorCode.TRADER_ILLEGAL_ACTION_TRANSITION, "planned leg position path not found: " + positionId); - } - if (existing.reduceSeen()) { - throw new TraderException(TraderErrorCode.TRADER_PLANNED_LEG_AFTER_REDUCE, "planned leg cannot follow reduce"); - } - BigDecimal ratio = contextDecimal(action, "plannedLegRatio"); - TraderPositionPath updated = new TraderPositionPath( - existing.runId(), - cycle.cycleId(), - action.actionId(), - existing.positionId(), - existing.side(), - existing.entryTime(), - snapshot.snapshotTime(), - existing.entryPrice(), - currentPrice(action, snapshot), - existing.mfeBps(), - existing.maeBps(), - existing.timeToTargetMs(), - existing.timeToInvalidMs(), - existing.targetBeforeStop(), - existing.stagnationTimeoutHit(), - true, - false, - existing.filledLegCount() + 1, - existing.totalPositionRatio().add(ratio), - Map.of("proxyFill", true, "lastAction", action.actionType().name()) - ); - pathsByPosition.put(positionId, updated); - return updated; + public BigDecimal calculateAddRatio(PositionManagerInput input) { + BigDecimal raw = input.pmConfig().sizing().baseRatio() + .multiply(input.modelOutput().continuation().continueVsExitEdgeBps().max(BigDecimal.ZERO)) + .divide(new BigDecimal("100"), java.math.MathContext.DECIMAL64); + return TraderNumbers.clamp(raw, input.pmConfig().sizing().minAddRatio(), + min(input.pmConfig().sizing().maxAddRatio(), input.positionState().remainingAddCapacity())); } - private TraderPositionPath updatePathOnly(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) { - String positionId = String.valueOf(action.actionContext().get("positionId")); - TraderPositionPath existing = pathsByPosition.get(positionId); - if (existing == null) { - throw new TraderException(TraderErrorCode.TRADER_ILLEGAL_ACTION_TRANSITION, "position path not found: " + positionId); + private TraderPositionManagerDecision decideWhenFlat(PositionManagerInput input) { + DirectionOutput direction = input.modelOutput().direction(); + EntryOutput entry = input.modelOutput().entry(); + RiskOutput risk = input.modelOutput().risk(); + TraderPmConfig.OpenRuleConfig open = input.pmConfig().open(); + boolean longPass = direction.longProb().compareTo(open.longOpenProb()) > 0 + && direction.directionMargin().compareTo(open.minDirectionMargin()) > 0 + && entry.longEntryProb().compareTo(open.minLongEntryProb()) > 0 + && entry.expectedEdgeBps().compareTo(open.minExpectedEdgeBps()) > 0 + && risk.marketRiskProb().compareTo(open.maxMarketRiskProb()) < 0 + && risk.liquidityCapacityRatio().compareTo(open.minLiquidityCapacityRatio()) >= 0 + && input.modelOutput().oodScore().compareTo(open.maxOodScore()) <= 0 + && input.executionState().openOrders().isEmpty(); + boolean shortPass = direction.shortProb().compareTo(open.shortOpenProb()) > 0 + && direction.directionMargin().compareTo(open.minDirectionMargin()) > 0 + && entry.shortEntryProb().compareTo(open.minShortEntryProb()) > 0 + && entry.expectedEdgeBps().compareTo(open.minExpectedEdgeBps()) > 0 + && risk.marketRiskProb().compareTo(open.maxMarketRiskProb()) < 0 + && risk.liquidityCapacityRatio().compareTo(open.minLiquidityCapacityRatio()) >= 0 + && input.modelOutput().oodScore().compareTo(open.maxOodScore()) <= 0 + && input.executionState().openOrders().isEmpty(); + BigDecimal longRatio = longPass ? calculateInitialRatio(input, PositionSide.LONG) : BigDecimal.ZERO; + BigDecimal shortRatio = shortPass ? calculateInitialRatio(input, PositionSide.SHORT) : BigDecimal.ZERO; + if (longPass && longRatio.compareTo(BigDecimal.ZERO) > 0 && (!shortPass || longRatio.compareTo(shortRatio) >= 0)) { + return decision(input, TraderActionType.OPEN_LONG, PositionSide.LONG, longRatio, null, null, "OPEN_LONG_PM_PASS"); } - TraderPositionPath updated = new TraderPositionPath( - existing.runId(), - cycle.cycleId(), - action.actionId(), - existing.positionId(), - existing.side(), - existing.entryTime(), - snapshot.snapshotTime(), - existing.entryPrice(), - currentPrice(action, snapshot), - existing.mfeBps(), - existing.maeBps(), - existing.timeToTargetMs(), - existing.timeToInvalidMs(), - existing.targetBeforeStop(), - existing.stagnationTimeoutHit(), - true, - existing.reduceSeen(), - existing.filledLegCount(), - existing.totalPositionRatio(), - Map.of("proxyFill", true, "lastAction", action.actionType().name()) - ); - pathsByPosition.put(positionId, updated); - return updated; + if (shortPass && shortRatio.compareTo(BigDecimal.ZERO) > 0) { + return decision(input, TraderActionType.OPEN_SHORT, PositionSide.SHORT, shortRatio, null, null, "OPEN_SHORT_PM_PASS"); + } + return waitDecision(input, "NO_ENTRY_CONDITION"); } - private TraderPositionPath reducePosition(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) { - TraderPositionPath updated = updatePathOnly(cycle, action, snapshot); - if (!updated.opened()) { - return updated; + private TraderPositionManagerDecision decideExistingPosition(PositionManagerInput input) { + if (shouldClose(input)) { + TraderActionType action = input.positionState().side().isLong() ? TraderActionType.CLOSE_LONG : TraderActionType.CLOSE_SHORT; + return decision(input, action, input.positionState().side(), null, null, null, "EXIT_OR_RISK_HIGH"); } - TraderPositionPath reduced = new TraderPositionPath( - updated.runId(), - updated.cycleId(), - updated.actionId(), - updated.positionId(), - updated.side(), - updated.entryTime(), - updated.lastEventTime(), - updated.entryPrice(), - updated.currentPrice(), - updated.mfeBps(), - updated.maeBps(), - updated.timeToTargetMs(), - updated.timeToInvalidMs(), - updated.targetBeforeStop(), - updated.stagnationTimeoutHit(), - true, - true, - updated.filledLegCount(), - updated.totalPositionRatio().divide(BigDecimal.valueOf(2)), - Map.of("proxyFill", true, "lastAction", TraderActionType.REDUCE.name()) - ); - pathsByPosition.put(reduced.positionId(), reduced); - return reduced; + if (shouldReduce(input)) { + TraderActionType action = input.positionState().side().isLong() ? TraderActionType.REDUCE_LONG : TraderActionType.REDUCE_SHORT; + return decision(input, action, input.positionState().side(), null, null, new BigDecimal("0.50"), "PROFIT_GIVEBACK_REDUCE"); + } + if (shouldAdd(input)) { + BigDecimal ratio = calculateAddRatio(input); + TraderActionType action = input.positionState().side().isLong() ? TraderActionType.ADD_LONG : TraderActionType.ADD_SHORT; + return decision(input, action, input.positionState().side(), null, ratio, null, "ADD_PM_PASS"); + } + return decision(input, TraderActionType.HOLD, input.positionState().side(), null, null, null, "CONTINUE_HOLD"); } - private TraderPositionPath closePosition(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) { - TraderPositionPath updated = updatePathOnly(cycle, action, snapshot); - if (!updated.opened()) { - return updated; - } - TraderPositionPath closed = new TraderPositionPath( - updated.runId(), - updated.cycleId(), - updated.actionId(), - updated.positionId(), - updated.side(), - updated.entryTime(), - updated.lastEventTime(), - updated.entryPrice(), - updated.currentPrice(), - updated.mfeBps(), - updated.maeBps(), - updated.timeToTargetMs(), - updated.timeToInvalidMs(), - true, - false, - true, - updated.reduceSeen(), - updated.filledLegCount(), - BigDecimal.ZERO, - Map.of("proxyFill", true, "lastAction", TraderActionType.CLOSE.name()) - ); - pathsByPosition.put(closed.positionId(), closed); - return closed; + private boolean shouldAdd(PositionManagerInput input) { + TraderPmConfig.AddRuleConfig add = input.pmConfig().add(); + boolean sidePass = input.positionState().side().isLong() + ? input.modelOutput().direction().longProb().compareTo(add.minLongProb()) > 0 + && input.modelOutput().entry().longEntryProb().compareTo(add.minEntryProb()) > 0 + && input.modelOutput().exit().longExitProb().compareTo(add.maxExitProb()) < 0 + : input.modelOutput().direction().shortProb().compareTo(add.minShortProb()) > 0 + && input.modelOutput().entry().shortEntryProb().compareTo(add.minEntryProb()) > 0 + && input.modelOutput().exit().shortExitProb().compareTo(add.maxExitProb()) < 0; + BigDecimal continueProb = input.positionState().side().isLong() + ? input.modelOutput().continuation().longContinueProb() + : input.modelOutput().continuation().shortContinueProb(); + return sidePass + && continueProb.compareTo(add.minContinueProb()) > 0 + && input.modelOutput().continuation().continueVsExitEdgeBps().compareTo(add.minContinueVsExitEdgeBps()) > 0 + && input.modelOutput().entry().expectedEdgeBps().compareTo(add.minExpectedEdgeBps()) > 0 + && input.modelOutput().risk().marketRiskProb().compareTo(add.maxMarketRiskProb()) < 0 + && input.modelOutput().risk().positionRiskProb().compareTo(add.maxPositionRiskProb()) < 0 + && input.modelOutput().risk().liquidityCapacityRatio().compareTo(add.minLiquidityCapacityRatio()) >= 0 + && input.positionState().unrealizedPnlBps().compareTo(BigDecimal.ZERO) > 0 + && input.positionState().addCount() < add.maxAddCount() + && input.positionState().remainingAddCapacity().compareTo(BigDecimal.ZERO) > 0 + && input.executionState().openOrders().isEmpty(); } - private TraderPositionPath noPositionChange(TraderDecisionCycle cycle, TraderAction action, TraderMarketSnapshot snapshot) { - Instant now = snapshot.snapshotTime(); - return new TraderPositionPath( - cycle.runId(), - cycle.cycleId(), - action.actionId(), - null, - action.side(), - now, - now, - action.price(), - action.price(), - BigDecimal.ZERO, - BigDecimal.ZERO, - null, - null, - false, - false, - true, - false, - 0, - BigDecimal.ZERO, - Map.of("lastAction", action.actionType().name()) - ); + private boolean shouldClose(PositionManagerInput input) { + TraderPmConfig.ExitRuleConfig exit = input.pmConfig().exit(); + boolean exitProb = input.positionState().side().isLong() + ? input.modelOutput().exit().longExitProb().compareTo(exit.closeExitProb()) > 0 + : input.modelOutput().exit().shortExitProb().compareTo(exit.closeExitProb()) > 0; + BigDecimal continueProb = input.positionState().side().isLong() + ? input.modelOutput().continuation().longContinueProb() + : input.modelOutput().continuation().shortContinueProb(); + return exitProb + || continueProb.compareTo(exit.closeContinueMax()) < 0 + || input.modelOutput().risk().positionRiskProb().compareTo(exit.closePositionRiskProb()) > 0 + || input.modelOutput().risk().marketRiskProb().compareTo(exit.closeMarketRiskProb()) > 0 + || input.modelOutput().risk().expectedShortfallBps().compareTo(exit.maxExpectedShortfallBps()) > 0; } - private BigDecimal currentPrice(TraderAction action, TraderMarketSnapshot snapshot) { - Object lastPrice = snapshot.executionFeatures().get("lastPrice"); - if (lastPrice instanceof Number number) { - return BigDecimal.valueOf(number.doubleValue()); - } - if (lastPrice instanceof String text && !text.isBlank()) { - return new BigDecimal(text); - } - return action.price(); + private boolean shouldReduce(PositionManagerInput input) { + TraderPmConfig.ExitRuleConfig exit = input.pmConfig().exit(); + BigDecimal continueProb = input.positionState().side().isLong() + ? input.modelOutput().continuation().longContinueProb() + : input.modelOutput().continuation().shortContinueProb(); + return input.modelOutput().exit().profitGivebackProb().compareTo(exit.reduceGivebackProb()) > 0 + && continueProb.compareTo(exit.reduceContinueMin()) >= 0 + && continueProb.compareTo(exit.reduceContinueMax()) <= 0 + && input.positionState().unrealizedPnlBps().compareTo(exit.minProfitForReduceBps()) > 0; } - private BigDecimal contextDecimal(TraderAction action, String key) { - Object value = action.actionContext().get(key); - if (value instanceof BigDecimal decimal) { - return decimal; - } - if (value instanceof Number number) { - return BigDecimal.valueOf(number.doubleValue()); - } - if (value instanceof String text && !text.isBlank()) { - return new BigDecimal(text); - } - throw new TraderException(TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE, "missing action context decimal: " + key); + private TraderPositionManagerDecision waitDecision(PositionManagerInput input, String reason) { + return decision(input, TraderActionType.WAIT, PositionSide.NONE, null, null, null, reason); + } + + private TraderPositionManagerDecision decision(PositionManagerInput input, TraderActionType action, PositionSide side, + BigDecimal targetRatio, BigDecimal addRatio, BigDecimal reduceRatio, String reason) { + EntryOutput entry = input.modelOutput().entry(); + BigDecimal stopPrice = action.increasesExposure() ? priceFromBps(input.snapshot().markPrice(), entry.stopDistanceBps(), side, false) : null; + BigDecimal targetPrice = action.increasesExposure() ? priceFromBps(input.snapshot().markPrice(), entry.targetDistanceBps(), side, true) : null; + return new TraderPositionManagerDecision( + "pm_" + input.cycle().cycleId(), + input.cycle().runId(), + input.cycle().cycleId(), + input.modelOutput().modelOutputId(), + input.positionState().positionStateId(), + input.accountState().accountStateId(), + input.executionState().executionStateId(), + action, + side, + action.increasesExposure() ? entry.pricePlanId() : null, + action.increasesExposure() ? entry.pricePlanConfigHash() : null, + targetRatio, + addRatio, + reduceRatio, + stopPrice, + targetPrice, + reason, + Map.of("pmConfigVersion", input.pmConfig().pmConfigVersion())); + } + + private BigDecimal priceFromBps(BigDecimal markPrice, BigDecimal distanceBps, PositionSide side, boolean profitTarget) { + // Entry distances are model bps distances; convert them into side-aware price levels. + BigDecimal direction = side.isLong() == profitTarget ? BigDecimal.ONE : BigDecimal.ONE.negate(); + BigDecimal multiplier = BigDecimal.ONE.add(distanceBps.multiply(direction).divide(TEN_THOUSAND, MathContext.DECIMAL64)); + return markPrice.multiply(multiplier, MathContext.DECIMAL64); + } + + private static BigDecimal min(BigDecimal left, BigDecimal right) { + return left.compareTo(right) <= 0 ? left : right; } } diff --git a/src/main/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReader.java b/src/main/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReader.java deleted file mode 100644 index 0f1ff0d..0000000 --- a/src/main/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReader.java +++ /dev/null @@ -1,498 +0,0 @@ -package com.quantai.trader.replay; - -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.enums.TraderErrorCode; -import com.quantai.trader.enums.TraderSide; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVRecord; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; - -@Component -public class CryptoLakeReplayCsvMarketEventReader implements ReplayMarketEventReader { - - private static final Logger log = LoggerFactory.getLogger(CryptoLakeReplayCsvMarketEventReader.class); - private static final MathContext MC = new MathContext(16, RoundingMode.HALF_UP); - private static final String REPLAY_SOURCE_KEY = "cryptoLakeReplay1m"; - private static final String CANDIDATE_SOURCE_KEY = "candidateEvents"; - private static final BigDecimal LONG_INVALID_BPS = new BigDecimal("12.0"); - private static final BigDecimal LONG_STOP_BPS = new BigDecimal("8.0"); - private static final BigDecimal LONG_TARGET_BPS = new BigDecimal("30.0"); - private static final BigDecimal SHORT_INVALID_BPS = new BigDecimal("12.0"); - private static final BigDecimal SHORT_STOP_BPS = new BigDecimal("8.0"); - private static final BigDecimal SHORT_TARGET_BPS = new BigDecimal("30.0"); - - @Override - public boolean supports(ReplayRunConfig config) { - DataSourceSpec source = config.dataSources() == null ? null : config.dataSources().get(REPLAY_SOURCE_KEY); - return source != null && source.path() != null && source.path().endsWith(".csv"); - } - - @Override - public void validateReadable(ReplayRunConfig config) { - validateSource(selectReplaySource(config), REPLAY_SOURCE_KEY); - DataSourceSpec candidateSource = config.dataSources().get(CANDIDATE_SOURCE_KEY); - if (candidateSource != null) { - validateSource(candidateSource, CANDIDATE_SOURCE_KEY); - } - } - - @Override - public List readTicks(ReplayRunConfig config) { - validateReadable(config); - NavigableMap bars = readReplayBars(config); - List ticks = config.dataSources().containsKey(CANDIDATE_SOURCE_KEY) - ? readCandidateTicks(config, bars) - : readMarketAuditTicks(config, bars); - if (ticks.isEmpty()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "crypto lake replay csv produced no ticks"); - } - log.info( - "event=trader.replay.crypto_lake_csv.loaded runId={} symbol={} tickCount={} candidateMode={}", - config.runId(), - config.symbol(), - ticks.size(), - config.dataSources().containsKey(CANDIDATE_SOURCE_KEY) - ); - return ticks; - } - - private NavigableMap readReplayBars(ReplayRunConfig config) { - Path path = Path.of(selectReplaySource(config).path()); - NavigableMap bars = new TreeMap<>(); - try (CSVParser parser = CSVParser.parse(path, java.nio.charset.StandardCharsets.UTF_8, - CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build())) { - for (CSVRecord record : parser) { - if (!config.symbol().equals(required(record, "symbol"))) { - continue; - } - if (!"1m".equals(required(record, "timeframe"))) { - continue; - } - MarketBar bar = marketBar(record); - bars.put(bar.openTime(), bar); - } - } catch (IOException ex) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "failed to read crypto lake replay csv: " + ex.getMessage()); - } - if (bars.isEmpty()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "crypto lake replay csv has no rows for symbol: " + config.symbol()); - } - return bars; - } - - private List readMarketAuditTicks(ReplayRunConfig config, NavigableMap bars) { - List ticks = new ArrayList<>(); - List ordered = List.copyOf(bars.values()); - for (int i = 0; i < ordered.size(); i++) { - MarketBar bar = ordered.get(i); - if (outsideRunWindow(config, bar.openTime())) { - continue; - } - ticks.add(toTick(config, bar, null, labelInputs(ordered, i, null))); - } - return ticks.stream() - .sorted(Comparator.comparing(ReplayClockTick::eventTime)) - .toList(); - } - - private List readCandidateTicks(ReplayRunConfig config, NavigableMap bars) { - Path path = Path.of(config.dataSources().get(CANDIDATE_SOURCE_KEY).path()); - List ordered = List.copyOf(bars.values()); - Map indexByTime = new LinkedHashMap<>(); - for (int i = 0; i < ordered.size(); i++) { - indexByTime.put(ordered.get(i).openTime(), i); - } - List ticks = new ArrayList<>(); - try (CSVParser parser = CSVParser.parse(path, java.nio.charset.StandardCharsets.UTF_8, - CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build())) { - for (CSVRecord record : parser) { - if (!config.symbol().equals(required(record, "symbol"))) { - continue; - } - Instant candidateTime = Instant.ofEpochMilli(requiredLong(record, "bar_time")); - if (outsideRunWindow(config, candidateTime)) { - continue; - } - Map.Entry entry = bars.ceilingEntry(candidateTime); - if (entry == null || outsideRunWindow(config, entry.getKey())) { - continue; - } - int barIndex = indexByTime.get(entry.getKey()); - CandidateEvent event = candidateEvent(record, candidateTime); - ticks.add(toTick(config, entry.getValue(), event, labelInputs(ordered, barIndex, event.side()))); - } - } catch (IOException ex) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "failed to read candidate events csv: " + ex.getMessage()); - } - return ticks.stream() - .sorted(Comparator.comparing(ReplayClockTick::eventTime)) - .toList(); - } - - private ReplayClockTick toTick( - ReplayRunConfig config, - MarketBar bar, - CandidateEvent candidate, - Map labelInputs - ) { - List missing = missingFeatures(bar); - Map context = new LinkedHashMap<>(); - context.put("contextPass", missing.isEmpty()); - context.put("replaySourceType", "CRYPTO_LAKE_1M_CSV"); - putDecimal(context, "sourceCoverage", bar.sourceCoverage()); - putDecimal(context, "fundingBps", bar.fundingBps()); - putDecimal(context, "openInterest", bar.openInterest()); - putDecimal(context, "volume", bar.volume()); - - Map setup = new LinkedHashMap<>(); - setup.put("setupPass", candidate != null); - setup.put("setupName", candidate == null ? "market_audit_only" : "candidate_event_replay"); - if (candidate != null) { - if (bar.close() == null) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "candidate event matched a replay bar without close price"); - } - setup.put("candidateEventId", candidate.eventId()); - setup.put("signalType", candidate.signalType()); - setup.put("side", candidate.side().name()); - setup.put("sourceService", candidate.sourceService()); - putDecimal(setup, "entryPrice", bar.close()); - putDecimal(setup, "invalidPrice", priceByBps(bar.close(), invalidBps(candidate.side()), adverseSign(candidate.side()))); - putDecimal(setup, "stopPrice", priceByBps(bar.close(), stopBps(candidate.side()), adverseSign(candidate.side()))); - putDecimal(setup, "targetPrice", priceByBps(bar.close(), targetBps(candidate.side()), favorableSign(candidate.side()))); - putDecimal(setup, "executionQualityScore", executionQualityScore(bar)); - } - - Map trigger = new LinkedHashMap<>(); - if (candidate != null && candidate.triggerScore() != null) { - putDecimal(trigger, "triggerScore", candidate.triggerScore()); - } - trigger.put("replayTriggerSource", candidate == null ? "NONE" : "CANDIDATE_EVENT"); - - Map execution = new LinkedHashMap<>(); - putDecimal(execution, "lastPrice", bar.close()); - putDecimal(execution, "bestBidPrice", bar.bestBidPrice()); - putDecimal(execution, "bestAskPrice", bar.bestAskPrice()); - putDecimal(execution, "observedSpreadBps", bar.observedSpreadBps()); - putDecimal(execution, "expectedSlippageBps", bar.expectedSlippageBps()); - putDecimal(execution, "p95LatencyMs", bar.p95LatencyMs()); - - Map dataQuality = new LinkedHashMap<>(); - dataQuality.put("missing_features", missing); - putDecimal(dataQuality, "sourceCoverage", bar.sourceCoverage()); - dataQuality.put("replaySourcePath", selectReplaySource(config).path()); - - return new ReplayClockTick( - config.runId(), - config.symbol(), - bar.openTime(), - context, - setup, - trigger, - execution, - dataQuality, - labelInputs - ); - } - - private Map labelInputs(List bars, int index, TraderSide side) { - Map labels = new LinkedHashMap<>(); - labels.put("labelSource", "CRYPTO_LAKE_1M_REPLAY"); - if (side == null) { - labels.put("labelStatus", "MARKET_AUDIT_NO_SIDE"); - return labels; - } - MarketBar entry = bars.get(index); - labels.put("side", side.name()); - putDecimal(labels, "entryPrice", entry.close()); - putIfPresent(labels, "markoutBps1m", markout(bars, index, side, 1)); - putIfPresent(labels, "markoutBps5m", markout(bars, index, side, 5)); - putIfPresent(labels, "markoutBps15m", markout(bars, index, side, 15)); - putIfPresent(labels, "mfeBps15m", mfe(bars, index, side, 15)); - putIfPresent(labels, "maeBps15m", mae(bars, index, side, 15)); - putIfPresent(labels, "targetBeforeStop15m", targetBeforeStop(bars, index, side, targetBps(side), stopBps(side), 15)); - putDecimal(labels, "expectedSlippageBps", entry.expectedSlippageBps()); - labels.put("labelStatus", hasMandatoryLabels(labels) ? "REPLAY_MARKOUT_LABELED" : "FUTURE_WINDOW_INCOMPLETE"); - return labels; - } - - private boolean hasMandatoryLabels(Map labels) { - return labels.containsKey("markoutBps1m") - && labels.containsKey("markoutBps5m") - && labels.containsKey("markoutBps15m"); - } - - private String markout(List bars, int index, TraderSide side, int minutes) { - if (index + minutes >= bars.size()) { - return null; - } - BigDecimal entry = bars.get(index).close(); - BigDecimal close = bars.get(index + minutes).close(); - return decimalText(sideReturnBps(side, entry, close)); - } - - private String mfe(List bars, int index, TraderSide side, int minutes) { - if (index + minutes >= bars.size()) { - return null; - } - BigDecimal entry = bars.get(index).close(); - BigDecimal best = BigDecimal.ZERO; - for (int i = index + 1; i <= index + minutes; i++) { - BigDecimal favorable = side == TraderSide.LONG ? bars.get(i).high() : bars.get(i).low(); - best = best.max(sideReturnBps(side, entry, favorable)); - } - return decimalText(best.max(BigDecimal.ZERO)); - } - - private String mae(List bars, int index, TraderSide side, int minutes) { - if (index + minutes >= bars.size()) { - return null; - } - BigDecimal entry = bars.get(index).close(); - BigDecimal worst = BigDecimal.ZERO; - for (int i = index + 1; i <= index + minutes; i++) { - BigDecimal adverse = side == TraderSide.LONG ? bars.get(i).low() : bars.get(i).high(); - BigDecimal signed = sideReturnBps(side, entry, adverse); - if (signed.compareTo(BigDecimal.ZERO) < 0) { - worst = worst.max(signed.abs()); - } - } - return decimalText(worst); - } - - private Boolean targetBeforeStop(List bars, int index, TraderSide side, BigDecimal targetBps, BigDecimal stopBps, int minutes) { - if (index + minutes >= bars.size()) { - return null; - } - BigDecimal entry = bars.get(index).close(); - BigDecimal target = priceByBps(entry, targetBps, favorableSign(side)); - BigDecimal stop = priceByBps(entry, stopBps, adverseSign(side)); - for (int i = index + 1; i <= index + minutes; i++) { - MarketBar bar = bars.get(i); - boolean targetHit = side == TraderSide.LONG - ? bar.high().compareTo(target) >= 0 - : bar.low().compareTo(target) <= 0; - boolean stopHit = side == TraderSide.LONG - ? bar.low().compareTo(stop) <= 0 - : bar.high().compareTo(stop) >= 0; - if (targetHit) { - return true; - } - if (stopHit) { - return false; - } - } - return false; - } - - private MarketBar marketBar(CSVRecord record) { - return new MarketBar( - Instant.parse(required(record, "open_time")), - decimal(record, "open"), - decimal(record, "high"), - decimal(record, "low"), - decimal(record, "close"), - decimal(record, "volume"), - decimal(record, "taker_buy_volume"), - decimal(record, "funding_bps"), - decimal(record, "open_interest"), - decimal(record, "best_bid_price"), - decimal(record, "best_ask_price"), - decimal(record, "observed_spread_bps"), - decimal(record, "expected_slippage_bps"), - decimal(record, "p95_latency_ms"), - decimal(record, "source_coverage") - ); - } - - private CandidateEvent candidateEvent(CSVRecord record, Instant candidateTime) { - String side = required(record, "direction").toUpperCase(); - return new CandidateEvent( - required(record, "event_id"), - candidateTime, - required(record, "signal_type"), - TraderSide.valueOf(side), - required(record, "source_service"), - firstDecimal(record, "old_fusion_score", "legacy_fusion_score") - ); - } - - private List missingFeatures(MarketBar bar) { - List missing = new ArrayList<>(); - requirePresent(missing, "open", bar.open()); - requirePresent(missing, "high", bar.high()); - requirePresent(missing, "low", bar.low()); - requirePresent(missing, "close", bar.close()); - requirePresent(missing, "taker_buy_volume", bar.takerBuyVolume()); - requirePresent(missing, "expected_slippage_bps", bar.expectedSlippageBps()); - requirePresent(missing, "source_coverage", bar.sourceCoverage()); - return missing; - } - - private void requirePresent(List missing, String field, BigDecimal value) { - if (value == null) { - missing.add(field); - } - } - - private DataSourceSpec selectReplaySource(ReplayRunConfig config) { - DataSourceSpec source = config.dataSources().get(REPLAY_SOURCE_KEY); - if (source == null) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "dataSources.cryptoLakeReplay1m is required"); - } - return source; - } - - private void validateSource(DataSourceSpec source, String sourceType) { - if (source.path() == null || source.path().isBlank()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source path is required: " + sourceType); - } - Path path = Path.of(source.path()); - if (!Files.isRegularFile(path) || !Files.isReadable(path)) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source is not readable: " + source.path()); - } - } - - private boolean outsideRunWindow(ReplayRunConfig config, Instant time) { - return time.isBefore(config.from()) || !time.isBefore(config.to()); - } - - private String required(CSVRecord record, String column) { - String value = record.get(column); - if (value == null || value.isBlank()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "csv column is required: " + column); - } - return value; - } - - private long requiredLong(CSVRecord record, String column) { - return Long.parseLong(required(record, column)); - } - - private BigDecimal firstDecimal(CSVRecord record, String... columns) { - for (String column : columns) { - if (!record.isMapped(column)) { - continue; - } - BigDecimal value = decimal(record, column); - if (value != null) { - return value; - } - } - return null; - } - - private BigDecimal decimal(CSVRecord record, String column) { - if (!record.isMapped(column)) { - return null; - } - String value = record.get(column); - if (value == null || value.isBlank()) { - return null; - } - return new BigDecimal(value); - } - - private String decimalText(BigDecimal value) { - return value == null ? null : value.stripTrailingZeros().toPlainString(); - } - - private void putDecimal(Map target, String key, BigDecimal value) { - String text = decimalText(value); - if (text != null) { - target.put(key, text); - } - } - - private void putIfPresent(Map target, String key, Object value) { - if (value != null) { - target.put(key, value); - } - } - - private BigDecimal executionQualityScore(MarketBar bar) { - if (bar.expectedSlippageBps() == null) { - return null; - } - BigDecimal score = BigDecimal.ONE.subtract(bar.expectedSlippageBps().divide(new BigDecimal("20.0"), MC), MC); - return score.max(new BigDecimal("0.20")).min(BigDecimal.ONE).setScale(8, RoundingMode.HALF_UP); - } - - private BigDecimal sideReturnBps(TraderSide side, BigDecimal entry, BigDecimal exit) { - BigDecimal gross = exit.subtract(entry, MC) - .divide(entry, MC) - .multiply(new BigDecimal("10000"), MC); - return side == TraderSide.LONG ? gross : gross.negate(); - } - - private BigDecimal priceByBps(BigDecimal entry, BigDecimal bps, int sign) { - BigDecimal multiplier = BigDecimal.ONE.add(BigDecimal.valueOf(sign).multiply(bps, MC).divide(new BigDecimal("10000"), MC), MC); - return entry.multiply(multiplier, MC).setScale(8, RoundingMode.HALF_UP); - } - - private int favorableSign(TraderSide side) { - return side == TraderSide.LONG ? 1 : -1; - } - - private int adverseSign(TraderSide side) { - return side == TraderSide.LONG ? -1 : 1; - } - - private BigDecimal invalidBps(TraderSide side) { - return side == TraderSide.LONG ? LONG_INVALID_BPS : SHORT_INVALID_BPS; - } - - private BigDecimal stopBps(TraderSide side) { - return side == TraderSide.LONG ? LONG_STOP_BPS : SHORT_STOP_BPS; - } - - private BigDecimal targetBps(TraderSide side) { - return side == TraderSide.LONG ? LONG_TARGET_BPS : SHORT_TARGET_BPS; - } - - private record MarketBar( - Instant openTime, - BigDecimal open, - BigDecimal high, - BigDecimal low, - BigDecimal close, - BigDecimal volume, - BigDecimal takerBuyVolume, - BigDecimal fundingBps, - BigDecimal openInterest, - BigDecimal bestBidPrice, - BigDecimal bestAskPrice, - BigDecimal observedSpreadBps, - BigDecimal expectedSlippageBps, - BigDecimal p95LatencyMs, - BigDecimal sourceCoverage - ) { - } - - private record CandidateEvent( - String eventId, - Instant barTime, - String signalType, - TraderSide side, - String sourceService, - BigDecimal triggerScore - ) { - } -} diff --git a/src/main/java/com/quantai/trader/replay/DataSourceSpec.java b/src/main/java/com/quantai/trader/replay/DataSourceSpec.java deleted file mode 100644 index 8d064cf..0000000 --- a/src/main/java/com/quantai/trader/replay/DataSourceSpec.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.quantai.trader.replay; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.time.Instant; -import java.util.Map; - -public record DataSourceSpec( - @JsonProperty("sourceId") - String sourceId, - String path, - @JsonProperty("hashSha256") - String hashSha256, - @JsonProperty("schemaHashSha256") - String schemaHashSha256, - Long rowCount, - Instant minEventTime, - Instant maxEventTime, - String timezone, - Map missingSummary -) { -} diff --git a/src/main/java/com/quantai/trader/replay/JsonlReplayMarketEventReader.java b/src/main/java/com/quantai/trader/replay/JsonlReplayMarketEventReader.java deleted file mode 100644 index 3f79494..0000000 --- a/src/main/java/com/quantai/trader/replay/JsonlReplayMarketEventReader.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.quantai.trader.replay; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.enums.TraderErrorCode; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -@Component -public class JsonlReplayMarketEventReader implements ReplayMarketEventReader { - - private final ObjectMapper objectMapper; - - public JsonlReplayMarketEventReader() { - this.objectMapper = new ObjectMapper().findAndRegisterModules(); - } - - @Override - public boolean supports(ReplayRunConfig config) { - DataSourceSpec source = config.dataSources() == null ? null : config.dataSources().get("ticks"); - return source != null && source.path() != null && source.path().endsWith(".jsonl"); - } - - @Override - public void validateReadable(ReplayRunConfig config) { - DataSourceSpec source = selectReplaySource(config); - if (source.path() == null || source.path().isBlank()) { - throw new TraderException( - TraderErrorCode.TRADER_DATA_SOURCE_MISSING, - "replay source path is required" - ); - } - Path path = Path.of(source.path()); - if (!Files.isRegularFile(path) || !Files.isReadable(path)) { - throw new TraderException( - TraderErrorCode.TRADER_DATA_SOURCE_MISSING, - "replay source is not readable: " + source.path() - ); - } - } - - @Override - public List readTicks(ReplayRunConfig config) { - DataSourceSpec source = selectReplaySource(config); - Path path = Path.of(source.path()); - validateReadable(config); - try (var lines = Files.lines(path)) { - List ticks = lines - .map(String::trim) - .filter(line -> !line.isEmpty()) - .map(line -> parseLine(config, line)) - .sorted(Comparator.comparing(ReplayClockTick::eventTime)) - .toList(); - if (ticks.isEmpty()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "replay source produced no ticks"); - } - return ticks; - } catch (IOException ex) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "failed to read replay source: " + ex.getMessage()); - } - } - - private ReplayClockTick parseLine(ReplayRunConfig config, String line) { - try { - ReplayTickFixture fixture = objectMapper.readValue(line, ReplayTickFixture.class); - if (fixture.eventTime() == null) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "replay tick eventTime is required"); - } - Instant eventTime = Instant.parse(fixture.eventTime()); - return new ReplayClockTick( - config.runId(), - config.symbol(), - eventTime, - fixture.contextFeatures(), - fixture.setupFeatures(), - fixture.triggerFeatures(), - fixture.executionFeatures(), - fixture.dataQuality(), - fixture.labelInputs() == null ? Map.of() : fixture.labelInputs() - ); - } catch (IOException ex) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "invalid replay tick json: " + ex.getMessage()); - } - } - - private DataSourceSpec selectReplaySource(ReplayRunConfig config) { - DataSourceSpec explicit = config.dataSources().get("ticks"); - if (explicit != null) { - return explicit; - } - throw new TraderException( - TraderErrorCode.TRADER_DATA_SOURCE_MISSING, - "P0 replay requires dataSources.ticks" - ); - } - - public record ReplayTickFixture( - String eventTime, - Map contextFeatures, - Map setupFeatures, - Map triggerFeatures, - Map executionFeatures, - Map dataQuality, - Map labelInputs - ) { - } -} diff --git a/src/main/java/com/quantai/trader/replay/ReplayClockTick.java b/src/main/java/com/quantai/trader/replay/ReplayClockTick.java deleted file mode 100644 index 229d5f9..0000000 --- a/src/main/java/com/quantai/trader/replay/ReplayClockTick.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.quantai.trader.replay; - -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; - -public record ReplayClockTick( - String runId, - String symbol, - Instant eventTime, - Map contextFeatures, - Map setupFeatures, - Map triggerFeatures, - Map executionFeatures, - Map dataQuality, - Map labelInputs -) { - - public ReplayClockTick { - contextFeatures = immutable(contextFeatures); - setupFeatures = immutable(setupFeatures); - triggerFeatures = immutable(triggerFeatures); - executionFeatures = immutable(executionFeatures); - dataQuality = immutable(dataQuality); - labelInputs = immutable(labelInputs); - } - - private static Map immutable(Map value) { - if (value == null || value.isEmpty()) { - return Map.of(); - } - return Map.copyOf(new LinkedHashMap<>(value)); - } -} diff --git a/src/main/java/com/quantai/trader/replay/ReplayMarketEvent.java b/src/main/java/com/quantai/trader/replay/ReplayMarketEvent.java new file mode 100644 index 0000000..3958a03 --- /dev/null +++ b/src/main/java/com/quantai/trader/replay/ReplayMarketEvent.java @@ -0,0 +1,15 @@ +package com.quantai.trader.replay; + +import java.math.BigDecimal; +import java.time.Instant; + +public record ReplayMarketEvent( + String runId, + String symbol, + Instant eventTime, + BigDecimal markPrice, + BigDecimal indexPrice, + BigDecimal spreadBps, + BigDecimal depthNotional5Bps +) { +} diff --git a/src/main/java/com/quantai/trader/replay/ReplayMarketEventReader.java b/src/main/java/com/quantai/trader/replay/ReplayMarketEventReader.java deleted file mode 100644 index 39aaea8..0000000 --- a/src/main/java/com/quantai/trader/replay/ReplayMarketEventReader.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.replay; - -import java.util.List; - -public interface ReplayMarketEventReader { - - boolean supports(ReplayRunConfig config); - - void validateReadable(ReplayRunConfig config); - - List readTicks(ReplayRunConfig config); -} diff --git a/src/main/java/com/quantai/trader/replay/ReplayRun.java b/src/main/java/com/quantai/trader/replay/ReplayRun.java deleted file mode 100644 index cb6130c..0000000 --- a/src/main/java/com/quantai/trader/replay/ReplayRun.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.quantai.trader.replay; - -import com.quantai.trader.enums.ReplayRunStatus; - -import java.time.Instant; - -public record ReplayRun( - String runId, - ReplayRunStatus status, - ReplayRunConfig config, - String playbookDefinitionHash, - Instant createdAt, - Instant startedAt, - Instant finishedAt, - String failureReason -) { - - public ReplayRun withStatus(ReplayRunStatus nextStatus) { - Instant now = Instant.now(); - return new ReplayRun( - runId, - nextStatus, - config, - playbookDefinitionHash, - createdAt, - nextStatus == ReplayRunStatus.RUNNING ? now : startedAt, - nextStatus == ReplayRunStatus.COMPLETED || nextStatus == ReplayRunStatus.CANCELLED || nextStatus == ReplayRunStatus.FAILED ? now : finishedAt, - failureReason - ); - } - - public ReplayRun failed(String reason) { - return new ReplayRun( - runId, - ReplayRunStatus.FAILED, - config, - playbookDefinitionHash, - createdAt, - startedAt, - Instant.now(), - reason - ); - } -} diff --git a/src/main/java/com/quantai/trader/replay/ReplayRunConfig.java b/src/main/java/com/quantai/trader/replay/ReplayRunConfig.java deleted file mode 100644 index 39268ed..0000000 --- a/src/main/java/com/quantai/trader/replay/ReplayRunConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.quantai.trader.replay; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.time.Instant; -import java.util.Map; - -public record ReplayRunConfig( - String runId, - String symbol, - String playbookId, - String playbookVersion, - Instant from, - Instant to, - String featureVersion, - String labelVersion, - @JsonProperty("dataSources") - Map dataSources -) { - - public ReplayRunConfig withRunId(String nextRunId) { - return new ReplayRunConfig( - nextRunId, - symbol, - playbookId, - playbookVersion, - from, - to, - featureVersion, - labelVersion, - Map.copyOf(dataSources) - ); - } -} diff --git a/src/main/java/com/quantai/trader/replay/ReplayRunResponse.java b/src/main/java/com/quantai/trader/replay/ReplayRunResponse.java deleted file mode 100644 index b267253..0000000 --- a/src/main/java/com/quantai/trader/replay/ReplayRunResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.quantai.trader.replay; - -import com.quantai.trader.enums.ReplayRunStatus; - -public record ReplayRunResponse( - String runId, - ReplayRunStatus status -) { -} diff --git a/src/main/java/com/quantai/trader/replay/ReplayRunService.java b/src/main/java/com/quantai/trader/replay/ReplayRunService.java deleted file mode 100644 index 2d7db82..0000000 --- a/src/main/java/com/quantai/trader/replay/ReplayRunService.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.quantai.trader.replay; - -import com.quantai.trader.brain.TraderCycleResult; -import com.quantai.trader.brain.TraderDecisionCycleRunner; -import com.quantai.trader.domain.TraderDataSourceManifest; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.enums.ReplayRunStatus; -import com.quantai.trader.enums.TraderErrorCode; -import com.quantai.trader.enums.TraderRunMode; -import com.quantai.trader.persistence.ReplayRunRepository; -import com.quantai.trader.playbook.TraderPlaybookCatalog; -import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot; -import com.quantai.trader.report.ReplayReportWriter; -import com.quantai.trader.state.TraderRuntimeState; -import com.quantai.trader.util.Ids; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@Service -public class ReplayRunService { - - private static final Logger log = LoggerFactory.getLogger(ReplayRunService.class); - private final TraderPlaybookCatalog catalog; - private final ReplayRunRepository repository; - private final ReplayReportWriter reportWriter; - private final List eventReaders; - private final TraderDecisionCycleRunner cycleRunner; - private final ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> { - Thread thread = new Thread(runnable, "trader-replay-worker"); - thread.setDaemon(true); - return thread; - }); - - public ReplayRunService( - TraderPlaybookCatalog catalog, - ReplayRunRepository repository, - ReplayReportWriter reportWriter, - List eventReaders, - TraderDecisionCycleRunner cycleRunner - ) { - this.catalog = catalog; - this.repository = repository; - this.reportWriter = reportWriter; - this.eventReaders = List.copyOf(eventReaders); - this.cycleRunner = cycleRunner; - } - - public ReplayRunResponse createRun(ReplayRunConfig request) { - validateRequest(request); - TraderPlaybookDefinitionSnapshot playbook = catalog.require(request.playbookId(), request.playbookVersion()); - request.dataSources().forEach((sourceType, spec) -> validateDataSource(request, sourceType, spec)); - readerFor(request).validateReadable(request); - - String runId = Ids.runId(Instant.now()); - ReplayRunConfig config = request.withRunId(runId); - ReplayRun run = new ReplayRun( - runId, - ReplayRunStatus.CREATED, - config, - playbook.definitionHashSha256(), - Instant.now(), - null, - null, - null - ); - repository.insert(run); - log.info( - "event=trader.replay.created runId={} symbol={} playbookId={} playbookVersion={} status={}", - runId, - config.symbol(), - config.playbookId(), - config.playbookVersion(), - ReplayRunStatus.CREATED - ); - executorService.submit(() -> execute(run, playbook)); - return new ReplayRunResponse(runId, ReplayRunStatus.CREATED); - } - - public Optional find(String runId) { - return repository.findByRunId(runId); - } - - public ReplayRun cancel(String runId) { - ReplayRun run = repository.findByRunId(runId) - .orElseThrow(() -> new IllegalArgumentException("replay run not found: " + runId)); - if (run.status() == ReplayRunStatus.COMPLETED - || run.status() == ReplayRunStatus.CANCELLED - || run.status() == ReplayRunStatus.FAILED) { - return run; - } - ReplayRun cancelled = run.withStatus(ReplayRunStatus.CANCEL_REQUESTED); - repository.update(cancelled); - log.info("event=trader.replay.cancel_requested runId={} status={}", runId, cancelled.status()); - return cancelled; - } - - private void execute(ReplayRun run, TraderPlaybookDefinitionSnapshot playbook) { - try { - repository.update(run.withStatus(ReplayRunStatus.RUNNING)); - log.info( - "event=trader.replay.start runId={} symbol={} playbookId={} playbookVersion={} status={}", - run.runId(), - run.config().symbol(), - playbook.playbookId(), - playbook.playbookVersion(), - ReplayRunStatus.RUNNING - ); - List ticks = readerFor(run.config()).readTicks(run.config()); - List results = new ArrayList<>(ticks.size()); - TraderRuntimeState runtimeState = new TraderRuntimeState( - run.runId(), - TraderRunMode.REPLAY, - playbook.playbookId(), - playbook.playbookVersion() - ); - for (ReplayClockTick tick : ticks) { - ReplayRun current = currentRun(run.runId()); - if (current.status() == ReplayRunStatus.CANCEL_REQUESTED) { - repository.update(current.withStatus(ReplayRunStatus.CANCELLED)); - log.info("event=trader.replay.cancelled runId={} status={}", run.runId(), ReplayRunStatus.CANCELLED); - return; - } - results.add(cycleRunner.runReplayTick(tick, runtimeState)); - } - reportWriter.writeReport(run.config(), playbook, results); - ReplayRun current = currentRun(run.runId()); - repository.update(current.withStatus(ReplayRunStatus.COMPLETED)); - log.info( - "event=trader.replay.completed runId={} symbol={} playbookId={} playbookVersion={} status={} tickCount={} resultCount={}", - run.runId(), - run.config().symbol(), - playbook.playbookId(), - playbook.playbookVersion(), - ReplayRunStatus.COMPLETED, - ticks.size(), - results.size() - ); - } catch (RuntimeException ex) { - log.warn( - "event=trader.replay.failed_detected runId={} symbol={} playbookId={} playbookVersion={} status={} reason={}", - run.runId(), - run.config().symbol(), - playbook.playbookId(), - playbook.playbookVersion(), - ReplayRunStatus.FAILED, - ex.getMessage(), - ex - ); - try { - repository.update(run.failed(ex.toString())); - } catch (RuntimeException updateFailure) { - log.error( - "event=trader.replay.failed_status_update_failed runId={} originalReason={} updateReason={}", - run.runId(), - ex.getMessage(), - updateFailure.getMessage(), - updateFailure - ); - } - } - } - - private void validateRequest(ReplayRunConfig request) { - if (request.symbol() == null || request.symbol().isBlank() - || request.playbookId() == null || request.playbookId().isBlank() - || request.playbookVersion() == null || request.playbookVersion().isBlank() - || request.from() == null - || request.to() == null - || request.dataSources() == null - || request.dataSources().isEmpty()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "replay request is missing required lineage fields"); - } - } - - private ReplayRun currentRun(String runId) { - return repository.findByRunId(runId) - .orElseThrow(() -> new IllegalStateException("replay run disappeared: " + runId)); - } - - private ReplayMarketEventReader readerFor(ReplayRunConfig config) { - return eventReaders.stream() - .filter(reader -> reader.supports(config)) - .findFirst() - .orElseThrow(() -> new TraderException( - TraderErrorCode.TRADER_DATA_SOURCE_MISSING, - "no replay reader supports the requested dataSources" - )); - } - - private void validateDataSource(ReplayRunConfig request, String sourceType, DataSourceSpec spec) { - if (spec.timezone() == null || spec.timezone().isBlank()) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source timezone is required: " + sourceType); - } - if (spec.missingSummary() == null) { - throw new TraderException(TraderErrorCode.TRADER_DATA_SOURCE_MISSING, "data source missingSummary is required: " + sourceType); - } - new TraderDataSourceManifest( - spec.sourceId(), - request.symbol(), - sourceType, - "BINANCE", - sourceType.equals("candles") ? "1m" : "event", - spec.path(), - spec.hashSha256(), - spec.schemaHashSha256(), - request.from(), - request.to(), - spec.minEventTime(), - spec.maxEventTime(), - spec.timezone(), - spec.rowCount(), - spec.missingSummary(), - "P0_ACCEPTED" - ); - } -} diff --git a/src/main/java/com/quantai/trader/replay/TraderCycleResult.java b/src/main/java/com/quantai/trader/replay/TraderCycleResult.java new file mode 100644 index 0000000..db1b513 --- /dev/null +++ b/src/main/java/com/quantai/trader/replay/TraderCycleResult.java @@ -0,0 +1,14 @@ +package com.quantai.trader.replay; + +import com.quantai.trader.domain.TraderAction; +import com.quantai.trader.domain.TraderPositionManagerDecision; +import com.quantai.trader.domain.TraderRiskDecision; + +public record TraderCycleResult( + String runId, + String cycleId, + TraderPositionManagerDecision pmDecision, + TraderRiskDecision riskDecision, + TraderAction action +) { +} diff --git a/src/main/java/com/quantai/trader/replay/TraderP0CycleRunner.java b/src/main/java/com/quantai/trader/replay/TraderP0CycleRunner.java new file mode 100644 index 0000000..f12f13b --- /dev/null +++ b/src/main/java/com/quantai/trader/replay/TraderP0CycleRunner.java @@ -0,0 +1,108 @@ +package com.quantai.trader.replay; + +import com.quantai.trader.artifact.TraderArtifactBundle; +import com.quantai.trader.artifact.TraderArtifactLoader; +import com.quantai.trader.config.TraderProperties; +import com.quantai.trader.domain.*; +import com.quantai.trader.enums.PositionSide; +import com.quantai.trader.evidence.EvidenceAppender; +import com.quantai.trader.model.TraderModelService; +import com.quantai.trader.outbox.InMemoryOutboxRepository; +import com.quantai.trader.outbox.TraderOutboxEvent; +import com.quantai.trader.position.TraderPositionManager; +import com.quantai.trader.risk.RiskGateInput; +import com.quantai.trader.risk.RiskLimits; +import com.quantai.trader.risk.TraderRiskGate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Map; + +@Service +public class TraderP0CycleRunner { + private static final Logger log = LoggerFactory.getLogger(TraderP0CycleRunner.class); + + private final TraderProperties properties; + private final TraderArtifactLoader artifactLoader; + private final TraderModelService modelService; + private final TraderPositionManager positionManager; + private final TraderRiskGate riskGate; + private final TraderActionFactory actionFactory; + private final EvidenceAppender evidenceAppender; + private final InMemoryOutboxRepository outboxRepository; + + public TraderP0CycleRunner(TraderProperties properties, + TraderArtifactLoader artifactLoader, + TraderModelService modelService, + TraderPositionManager positionManager, + TraderRiskGate riskGate, + TraderActionFactory actionFactory, + EvidenceAppender evidenceAppender, + InMemoryOutboxRepository outboxRepository) { + this.properties = properties; + this.artifactLoader = artifactLoader; + this.modelService = modelService; + this.positionManager = positionManager; + this.riskGate = riskGate; + this.actionFactory = actionFactory; + this.evidenceAppender = evidenceAppender; + this.outboxRepository = outboxRepository; + } + + public TraderCycleResult runFlatCycle(ReplayMarketEvent event) { + String cycleId = "cycle_" + event.runId() + "_" + event.eventTime().toEpochMilli(); + TraderArtifactBundle bundle = artifactLoader.loadActiveBundle(); + TraderDecisionCycle cycle = new TraderDecisionCycle(event.runId(), cycleId, event.symbol(), event.eventTime(), + properties.runMode(), bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion()); + TraderMarketSnapshot snapshot = snapshot(event, cycleId); + evidenceAppender.append(cycle.runId(), cycle.cycleId(), "MARKET_SNAPSHOT", snapshot.dataReady(), "SNAPSHOT_BUILT", null, Map.of()); + TraderModelOutput modelOutput = modelService.evaluate(snapshot, bundle); + evidenceAppender.append(cycle.runId(), cycle.cycleId(), "MODEL_OUTPUT", true, "MODEL_EVALUATED", null, Map.of("modelOutputId", modelOutput.modelOutputId())); + PositionManagerInput pmInput = new PositionManagerInput(cycle, snapshot, modelOutput, + flatPosition(cycle, snapshot), account(cycle), execution(cycle), bundle.pmConfig()); + TraderPositionManagerDecision pmDecision = positionManager.decide(pmInput); + evidenceAppender.append(cycle.runId(), cycle.cycleId(), "PM_DECISION", true, pmDecision.reason(), null, Map.of("action", pmDecision.candidateAction().name())); + TraderRiskDecision riskDecision = riskGate.evaluate(new RiskGateInput(pmDecision, pmInput.positionState(), pmInput.accountState(), + pmInput.executionState(), snapshot, riskLimits())); + evidenceAppender.append(cycle.runId(), cycle.cycleId(), "RISK_DECISION", riskDecision.allowAction(), riskDecision.allowAction() ? "RISK_PASS" : riskDecision.blocker(), riskDecision.blocker(), Map.of()); + TraderAction action = actionFactory.create(pmDecision, riskDecision, event.symbol()); + outboxRepository.insert(new TraderOutboxEvent("outbox_" + action.actionId(), action.runId(), action.cycleId(), + "TRADER_ACTION", action.actionId(), "ACTION_CREATED", properties.runMode().name() + "_RECORDER", + Map.of("actionType", action.actionType().name()), action.idempotencyKey(), "PENDING", Instant.now())); + log.info("event=trader.cycle.completed runId={} cycleId={} action={} outbox=PENDING", action.runId(), action.cycleId(), action.actionType()); + return new TraderCycleResult(cycle.runId(), cycle.cycleId(), pmDecision, riskDecision, action); + } + + private TraderMarketSnapshot snapshot(ReplayMarketEvent event, String cycleId) { + return new TraderMarketSnapshot("snapshot_" + cycleId, event.runId(), cycleId, event.symbol(), event.eventTime(), + "feature-v4-p0", event.markPrice(), event.indexPrice(), event.spreadBps(), BigDecimal.ZERO, + event.depthNotional5Bps(), event.depthNotional5Bps(), event.depthNotional5Bps(), + event.depthNotional5Bps().compareTo(BigDecimal.ZERO) > 0, Map.of(), Map.of()); + } + + private TraderPositionState flatPosition(TraderDecisionCycle cycle, TraderMarketSnapshot snapshot) { + return new TraderPositionState("position_state_" + cycle.cycleId(), cycle.runId(), cycle.cycleId(), cycle.symbol(), + PositionSide.NONE, BigDecimal.ZERO, null, snapshot.markPrice(), BigDecimal.ZERO, new BigDecimal("1000"), + 0, BigDecimal.ONE, null); + } + + private TraderAccountState account(TraderDecisionCycle cycle) { + return new TraderAccountState("account_state_" + cycle.cycleId(), cycle.runId(), cycle.cycleId(), + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ONE, 0); + } + + private TraderExecutionState execution(TraderDecisionCycle cycle) { + return new TraderExecutionState("execution_state_" + cycle.cycleId(), cycle.runId(), cycle.cycleId(), cycle.symbol(), + java.util.List.of(), new BigDecimal("1.5"), 10, 0, new BigDecimal("1"), new BigDecimal("4"), + new BigDecimal("5"), new BigDecimal("0.1"), new BigDecimal("0.001"), new BigDecimal("0.001"), BigDecimal.ONE); + } + + private RiskLimits riskLimits() { + return new RiskLimits(properties.risk().maxDailyLossBps(), properties.risk().maxTotalExposureRatio(), + properties.risk().minLiquidationBufferBps(), properties.execution().maxApiErrorCount(), + properties.execution().maxExchangeLatencyMs(), false, false); + } +} diff --git a/src/main/java/com/quantai/trader/report/ReplayReportWriter.java b/src/main/java/com/quantai/trader/report/ReplayReportWriter.java deleted file mode 100644 index 92aacb3..0000000 --- a/src/main/java/com/quantai/trader/report/ReplayReportWriter.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.quantai.trader.report; - -import com.quantai.trader.domain.TraderReplayReport; -import com.quantai.trader.brain.TraderCycleResult; -import com.quantai.trader.domain.TraderTrainingSample; -import com.quantai.trader.persistence.ReplayReportRepository; -import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot; -import com.quantai.trader.replay.ReplayRunConfig; -import com.quantai.trader.util.Ids; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -@Component -public class ReplayReportWriter { - - private final ReplayReportRepository repository; - - public ReplayReportWriter(ReplayReportRepository repository) { - this.repository = repository; - } - - public TraderReplayReport writeReport( - ReplayRunConfig config, - TraderPlaybookDefinitionSnapshot playbook, - List results - ) { - int actionCount = (int) results.stream().filter(result -> result.action() != null).count(); - int sampleCount = (int) results.stream().filter(result -> result.sample() != null).count(); - SampleAudit audit = audit(results); - int monthsCovered = Math.max(1, (int) ChronoUnit.MONTHS.between( - config.from().atZone(java.time.ZoneOffset.UTC).withDayOfMonth(1), - config.to().atZone(java.time.ZoneOffset.UTC).withDayOfMonth(1) - )); - List failureRisks = failureRisks(actionCount, audit); - Map auditReport = new LinkedHashMap<>(); - auditReport.put("replayEngine", replayEngine(config)); - auditReport.put("tickCount", results.size()); - auditReport.put("sampleCount", sampleCount); - auditReport.put("actionCount", actionCount); - auditReport.put("labeledSampleCount", audit.labeledSampleCount()); - auditReport.put("proxyOnlySampleCount", audit.proxyOnlySampleCount()); - auditReport.put("positiveNetReturnCount", audit.positiveNetReturnCount()); - auditReport.put("negativeNetReturnCount", audit.negativeNetReturnCount()); - auditReport.put("missingNetReturnCount", audit.missingNetReturnCount()); - putIfPresent(auditReport, "meanNetReturnBps1x", audit.meanNetReturnBps1x()); - putIfPresent(auditReport, "meanNetReturnBps10x", audit.meanNetReturnBps10x()); - auditReport.put("labelStatusDistribution", audit.labelStatusDistribution()); - TraderReplayReport report = new TraderReplayReport( - config.runId(), - Ids.reportId(config.runId()), - config.symbol(), - playbook.playbookId(), - playbook.playbookVersion(), - actionCount, - monthsCovered, - audit.meanNetReturnBps1x(), - audit.meanNetReturnBps10x(), - null, - auditReport, - failureRisks, - audit.labeledSampleCount() > 0 ? "TRAINING_SAMPLE_AUDIT_ONLY" : "P0_OBSERVE_ONLY", - null, - Instant.now() - ); - repository.insert(report); - return report; - } - - private String replayEngine(ReplayRunConfig config) { - if (config.dataSources().containsKey("cryptoLakeReplay1m")) { - return "crypto_lake_1m_csv"; - } - return "jsonl_fixture"; - } - - private List failureRisks(int actionCount, SampleAudit audit) { - java.util.ArrayList risks = new java.util.ArrayList<>(); - if (actionCount == 0) { - risks.add("no_action_generated"); - } - if (audit.labeledSampleCount() == 0) { - risks.add("no_replay_markout_labels"); - } - if (audit.proxyOnlySampleCount() > 0) { - risks.add("proxy_only_samples_present"); - } - if (audit.missingNetReturnCount() > 0) { - risks.add("missing_net_return_labels"); - } - return risks; - } - - private SampleAudit audit(List results) { - List samples = results.stream() - .map(TraderCycleResult::sample) - .filter(Objects::nonNull) - .toList(); - int proxyOnly = (int) samples.stream().filter(TraderTrainingSample::proxyOnly).count(); - int labeled = samples.size() - proxyOnly; - int missingNet = (int) samples.stream().filter(sample -> sample.netReturnBps1x() == null).count(); - int positive = (int) samples.stream() - .filter(sample -> sample.netReturnBps1x() != null && sample.netReturnBps1x().compareTo(BigDecimal.ZERO) > 0) - .count(); - int negative = (int) samples.stream() - .filter(sample -> sample.netReturnBps1x() != null && sample.netReturnBps1x().compareTo(BigDecimal.ZERO) < 0) - .count(); - BigDecimal mean1x = mean(samples.stream() - .map(TraderTrainingSample::netReturnBps1x) - .filter(Objects::nonNull) - .toList()); - BigDecimal mean10x = mean(samples.stream() - .map(TraderTrainingSample::netReturnBps10x) - .filter(Objects::nonNull) - .toList()); - Map labelStatuses = samples.stream() - .map(sample -> String.valueOf(sample.labels().getOrDefault("label_status", "UNKNOWN"))) - .collect(java.util.stream.Collectors.groupingBy(status -> status, LinkedHashMap::new, java.util.stream.Collectors.counting())); - return new SampleAudit(labeled, proxyOnly, positive, negative, missingNet, mean1x, mean10x, labelStatuses); - } - - private BigDecimal mean(List values) { - if (values.isEmpty()) { - return null; - } - BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); - return sum.divide(BigDecimal.valueOf(values.size()), 8, java.math.RoundingMode.HALF_UP); - } - - private void putIfPresent(Map target, String key, Object value) { - if (value != null) { - target.put(key, value); - } - } - - private record SampleAudit( - int labeledSampleCount, - int proxyOnlySampleCount, - int positiveNetReturnCount, - int negativeNetReturnCount, - int missingNetReturnCount, - BigDecimal meanNetReturnBps1x, - BigDecimal meanNetReturnBps10x, - Map labelStatusDistribution - ) { - } -} diff --git a/src/main/java/com/quantai/trader/risk/RiskGateInput.java b/src/main/java/com/quantai/trader/risk/RiskGateInput.java new file mode 100644 index 0000000..6ae0f63 --- /dev/null +++ b/src/main/java/com/quantai/trader/risk/RiskGateInput.java @@ -0,0 +1,13 @@ +package com.quantai.trader.risk; + +import com.quantai.trader.domain.*; + +public record RiskGateInput( + TraderPositionManagerDecision pmDecision, + TraderPositionState positionState, + TraderAccountState accountState, + TraderExecutionState executionState, + TraderMarketSnapshot snapshot, + RiskLimits riskLimits +) { +} diff --git a/src/main/java/com/quantai/trader/risk/RiskLimits.java b/src/main/java/com/quantai/trader/risk/RiskLimits.java new file mode 100644 index 0000000..fbc5914 --- /dev/null +++ b/src/main/java/com/quantai/trader/risk/RiskLimits.java @@ -0,0 +1,14 @@ +package com.quantai.trader.risk; + +import java.math.BigDecimal; + +public record RiskLimits( + BigDecimal maxDailyLossBps, + BigDecimal maxTotalExposureRatio, + BigDecimal minLiquidationBufferBps, + int maxApiErrorCount, + long maxExchangeLatencyMs, + boolean killSwitchActive, + boolean executionBlocked +) { +} diff --git a/src/main/java/com/quantai/trader/risk/TraderPositionSizer.java b/src/main/java/com/quantai/trader/risk/TraderPositionSizer.java deleted file mode 100644 index 08ede02..0000000 --- a/src/main/java/com/quantai/trader/risk/TraderPositionSizer.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.quantai.trader.risk; - -import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.domain.PlaybookCandidate; -import com.quantai.trader.domain.PositionSizingPlan; -import com.quantai.trader.domain.TraderDecisionCycle; -import com.quantai.trader.domain.TraderPositionPath; -import com.quantai.trader.domain.TraderPricePlan; -import com.quantai.trader.domain.TriggerDecision; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; - -@Component -public class TraderPositionSizer { - - private static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP); - private final TraderProperties properties; - - public TraderPositionSizer(TraderProperties properties) { - this.properties = properties; - } - - public PositionSizingPlan sizeInitialPlan( - TraderDecisionCycle cycle, - PlaybookCandidate candidate, - TriggerDecision trigger, - TraderPricePlan pricePlan - ) { - BigDecimal signalStrength = clamp(trigger.signalStrengthScore()); - BigDecimal executionQuality = readScore(candidate, "executionQualityScore"); - BigDecimal riskScore = structuralRiskScore(pricePlan); - BigDecimal composite = signalStrength.add(executionQuality, MC).add(riskScore, MC) - .divide(BigDecimal.valueOf(3), MC); - - BigDecimal initialRatio = initialRatio(composite); - int plannedLegCount = plannedLegCount(initialRatio, candidate); - - return new PositionSizingPlan( - plannedLegCount, - initialRatio, - BigDecimal.ZERO, - properties.getSizing().getMethod(), - signalStrength, - executionQuality, - riskScore - ); - } - - public PositionSizingPlan sizeNextPlannedLeg( - TraderDecisionCycle cycle, - PlaybookCandidate candidate, - TraderPositionPath path, - TraderPricePlan pricePlan, - int nextIndex - ) { - int plannedLegCount = Math.min(candidate.maxPlannedEntryLegs(), properties.getSizing().getMaxPlannedEntryLegs()); - BigDecimal current = path.totalPositionRatio() == null ? BigDecimal.ZERO : path.totalPositionRatio(); - BigDecimal remaining = BigDecimal.ONE.subtract(current, MC).max(BigDecimal.ZERO); - int remainingLegs = Math.max(1, plannedLegCount - nextIndex); - BigDecimal nextRatio = capSingle(remaining.divide(BigDecimal.valueOf(remainingLegs), MC)); - BigDecimal riskScore = structuralRiskScore(pricePlan); - return new PositionSizingPlan( - plannedLegCount, - BigDecimal.ZERO, - nextRatio, - properties.getSizing().getMethod(), - BigDecimal.ZERO, - BigDecimal.ZERO, - riskScore - ); - } - - private BigDecimal initialRatio(BigDecimal composite) { - BigDecimal ratio; - if (properties.getSizing().isAllowFullInitialEntry() && composite.compareTo(new BigDecimal("0.85")) >= 0) { - ratio = BigDecimal.ONE; - } else if (composite.compareTo(new BigDecimal("0.55")) >= 0) { - ratio = new BigDecimal("0.60"); - } else { - ratio = new BigDecimal("0.30"); - } - return capSingle(ratio); - } - - private int plannedLegCount(BigDecimal initialRatio, PlaybookCandidate candidate) { - if (initialRatio.compareTo(BigDecimal.ONE) == 0) { - return 1; - } - return Math.min(candidate.maxPlannedEntryLegs(), properties.getSizing().getMaxPlannedEntryLegs()); - } - - private BigDecimal capSingle(BigDecimal ratio) { - BigDecimal maxSingle = properties.getSizing().getMaxSingleLegRatio(); - BigDecimal maxTotal = properties.getSizing().getMaxTotalPositionRatio(); - return ratio.min(maxSingle).min(maxTotal).setScale(8, RoundingMode.HALF_UP); - } - - private BigDecimal structuralRiskScore(TraderPricePlan pricePlan) { - if (pricePlan == null || pricePlan.entryPrice() == null || pricePlan.stopPrice() == null) { - return BigDecimal.ZERO; - } - BigDecimal distance = pricePlan.entryPrice().subtract(pricePlan.stopPrice(), MC).abs(); - if (pricePlan.entryPrice().compareTo(BigDecimal.ZERO) <= 0) { - return BigDecimal.ZERO; - } - BigDecimal distanceBps = distance.divide(pricePlan.entryPrice(), MC).multiply(BigDecimal.valueOf(10_000), MC); - if (distanceBps.compareTo(BigDecimal.valueOf(50)) <= 0) { - return new BigDecimal("0.90"); - } - if (distanceBps.compareTo(BigDecimal.valueOf(120)) <= 0) { - return new BigDecimal("0.65"); - } - return new BigDecimal("0.35"); - } - - private BigDecimal readScore(PlaybookCandidate candidate, String key) { - Object value = candidate.setupEvidence().get(key); - if (value instanceof Number number) { - return clamp(BigDecimal.valueOf(number.doubleValue())); - } - if (value instanceof String text && !text.isBlank()) { - return clamp(new BigDecimal(text)); - } - return BigDecimal.ZERO.setScale(8, RoundingMode.HALF_UP); - } - - private BigDecimal clamp(BigDecimal value) { - if (value == null) { - return BigDecimal.ZERO; - } - return value.max(BigDecimal.ZERO).min(BigDecimal.ONE).setScale(8, RoundingMode.HALF_UP); - } -} diff --git a/src/main/java/com/quantai/trader/risk/TraderRiskGate.java b/src/main/java/com/quantai/trader/risk/TraderRiskGate.java index 8bae855..ec8d666 100644 --- a/src/main/java/com/quantai/trader/risk/TraderRiskGate.java +++ b/src/main/java/com/quantai/trader/risk/TraderRiskGate.java @@ -1,163 +1,61 @@ package com.quantai.trader.risk; -import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.domain.ExecutionDecision; -import com.quantai.trader.domain.ManagementDecision; -import com.quantai.trader.domain.RiskDecision; -import com.quantai.trader.domain.TraderAction; -import com.quantai.trader.domain.TraderDecisionCycle; -import com.quantai.trader.domain.TraderEntryPlan; -import com.quantai.trader.domain.TraderPositionPath; import com.quantai.trader.domain.TraderRiskDecision; -import com.quantai.trader.enums.TraderErrorCode; -import com.quantai.trader.persistence.TraderRiskDecisionRepository; +import com.quantai.trader.enums.TraderActionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.time.Instant; import java.util.Map; -@Component +@Service public class TraderRiskGate { - private static final Logger log = LoggerFactory.getLogger(TraderRiskGate.class); - private static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP); - private final TraderProperties properties; - private final TraderRiskDecisionRepository repository; - public TraderRiskGate(TraderProperties properties, TraderRiskDecisionRepository repository) { - this.properties = properties; - this.repository = repository; + public TraderRiskDecision evaluate(RiskGateInput input) { + TraderRiskDecision decision; + if (input.riskLimits().killSwitchActive() && input.pmDecision().candidateAction().increasesExposure()) { + decision = block(input, "KILL_SWITCH_ACTIVE"); + } else if (input.riskLimits().executionBlocked()) { + decision = block(input, "EXECUTION_BLOCKED"); + } else if (input.accountState().dailyDrawdownBps().compareTo(input.riskLimits().maxDailyLossBps()) >= 0) { + decision = block(input, "MAX_DAILY_LOSS"); + } else if (input.accountState().portfolioExposureRatio().compareTo(input.riskLimits().maxTotalExposureRatio()) >= 0 + && input.pmDecision().candidateAction().increasesExposure()) { + decision = block(input, "MAX_TOTAL_EXPOSURE"); + } else if (input.positionState().liquidationBufferBps().compareTo(input.riskLimits().minLiquidationBufferBps()) < 0) { + decision = forceClose(input, "LIQUIDATION_BUFFER_LOW"); + } else if (!input.snapshot().dataReady()) { + decision = block(input, "DATA_NOT_READY"); + } else if (input.executionState().apiErrorCount() >= input.riskLimits().maxApiErrorCount()) { + decision = block(input, "EXCHANGE_UNSTABLE"); + } else if (input.executionState().exchangeLatencyMs() > input.riskLimits().maxExchangeLatencyMs()) { + decision = block(input, "EXCHANGE_LATENCY_HIGH"); + } else { + decision = allow(input); + } + log.info("event=trader.risk.decided runId={} cycleId={} originalAction={} finalAction={} allow={} blocker={}", + decision.runId(), decision.cycleId(), decision.originalAction(), decision.finalAction(), decision.allowAction(), decision.blocker()); + return decision; } - public RiskDecision evaluate(TraderDecisionCycle cycle, TraderEntryPlan entryPlan, ExecutionDecision execution) { - RiskDecision result = decide(entryPlan, execution); - TraderRiskDecision persisted = new TraderRiskDecision( - cycle.runId(), - cycle.cycleId(), - entryPlan.actionId(), - null, - entryPlan.entryAction(), - properties.getRisk().getLeverageScreen(), - entryPlan.plannedLegRatio(), - maxLossBps(entryPlan), - null, - null, - null, - null, - null, - result.allowAction(), - result.blocker(), - result.details(), - Instant.now() - ); - repository.insert(persisted); - log.info( - "event=trader.risk.decision runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} actionType={} allowAction={} blocker={}", - cycle.runId(), - cycle.cycleId(), - cycle.symbol(), - cycle.playbookId(), - cycle.playbookVersion(), - cycle.state(), - entryPlan.actionId(), - entryPlan.entryAction(), - result.allowAction(), - result.blocker() - ); - return result; + private TraderRiskDecision allow(RiskGateInput input) { + return new TraderRiskDecision("risk_" + input.pmDecision().cycleId(), input.pmDecision().runId(), input.pmDecision().cycleId(), + input.pmDecision().pmDecisionId(), true, input.pmDecision().candidateAction(), input.pmDecision().candidateAction(), null, + Map.of("risk", "pass")); } - public RiskDecision evaluateManagement( - TraderDecisionCycle cycle, - TraderAction action, - TraderPositionPath path, - ManagementDecision management - ) { - if (path == null || path.totalPositionRatio() == null) { - throw new IllegalArgumentException("management risk evaluation requires an opened position path"); - } - BigDecimal ratio = path.totalPositionRatio(); - RiskDecision result = new RiskDecision(true, null, BigDecimal.ONE, Map.of( - "positionId", path.positionId(), - "managementReason", management.reason(), - "plannedTotalPositionRatio", ratio - )); - TraderRiskDecision persisted = new TraderRiskDecision( - cycle.runId(), - cycle.cycleId(), - action.actionId(), - null, - action.actionType(), - properties.getRisk().getLeverageScreen(), - ratio, - BigDecimal.ZERO, - null, - null, - null, - null, - null, - true, - null, - result.details(), - Instant.now() - ); - repository.insert(persisted); - log.info( - "event=trader.risk.decision runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} actionType={} allowAction={} blocker={}", - cycle.runId(), - cycle.cycleId(), - cycle.symbol(), - cycle.playbookId(), - cycle.playbookVersion(), - cycle.state(), - action.actionId(), - action.actionType(), - true, - null - ); - return result; + private TraderRiskDecision block(RiskGateInput input, String blocker) { + TraderActionType finalAction = input.pmDecision().candidateAction().increasesExposure() ? TraderActionType.WAIT : input.pmDecision().candidateAction(); + return new TraderRiskDecision("risk_" + input.pmDecision().cycleId(), input.pmDecision().runId(), input.pmDecision().cycleId(), + input.pmDecision().pmDecisionId(), false, input.pmDecision().candidateAction(), finalAction, blocker, + Map.of("blocker", blocker)); } - private RiskDecision decide(TraderEntryPlan entryPlan, ExecutionDecision execution) { - if (!entryPlan.completeForEntry()) { - return block(TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE.name(), Map.of("reason", "entry plan incomplete")); - } - if (execution.blocked()) { - return block(execution.blocker(), Map.of("reason", execution.reason())); - } - if (entryPlan.plannedLegRatio().compareTo(BigDecimal.ZERO) <= 0 - || entryPlan.plannedLegRatio().compareTo(properties.getSizing().getMaxSingleLegRatio()) > 0 - || entryPlan.plannedLegRatio().compareTo(properties.getSizing().getMaxTotalPositionRatio()) > 0) { - return block(TraderErrorCode.TRADER_RISK_BLOCKED.name(), Map.of( - "reason", "planned position ratio exceeds P0 risk constraints", - "plannedLegRatio", entryPlan.plannedLegRatio() - )); - } - return new RiskDecision(true, null, entryPlan.riskGateScore(), Map.of( - "plannedLegRatio", entryPlan.plannedLegRatio(), - "leverageScreen", properties.getRisk().getLeverageScreen(), - "requireOneXNotNegative", properties.getRisk().isRequireOneXNotNegative() - )); - } - - private RiskDecision block(String blocker, Map details) { - return new RiskDecision(false, blocker, BigDecimal.ZERO, details); - } - - private BigDecimal maxLossBps(TraderEntryPlan entryPlan) { - if (entryPlan.entryPrice() == null || entryPlan.stopPrice() == null || entryPlan.entryPrice().compareTo(BigDecimal.ZERO) <= 0) { - return BigDecimal.ZERO; - } - return entryPlan.entryPrice() - .subtract(entryPlan.stopPrice(), MC) - .abs() - .divide(entryPlan.entryPrice(), MC) - .multiply(BigDecimal.valueOf(10_000), MC) - .setScale(8, RoundingMode.HALF_UP); + private TraderRiskDecision forceClose(RiskGateInput input, String blocker) { + TraderActionType finalAction = input.positionState().side().isShort() ? TraderActionType.CLOSE_SHORT : TraderActionType.CLOSE_LONG; + return new TraderRiskDecision("risk_" + input.pmDecision().cycleId(), input.pmDecision().runId(), input.pmDecision().cycleId(), + input.pmDecision().pmDecisionId(), true, input.pmDecision().candidateAction(), finalAction, blocker, + Map.of("blocker", blocker, "forced", true)); } } diff --git a/src/main/java/com/quantai/trader/runtime/P0RuntimeGuard.java b/src/main/java/com/quantai/trader/runtime/P0RuntimeGuard.java new file mode 100644 index 0000000..5c2fd54 --- /dev/null +++ b/src/main/java/com/quantai/trader/runtime/P0RuntimeGuard.java @@ -0,0 +1,40 @@ +package com.quantai.trader.runtime; + +import com.quantai.trader.config.TraderProperties; +import com.quantai.trader.domain.TraderException; +import com.quantai.trader.enums.TraderErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class P0RuntimeGuard { + private static final Logger log = LoggerFactory.getLogger(P0RuntimeGuard.class); + + private final TraderProperties properties; + + public P0RuntimeGuard(TraderProperties properties) { + this.properties = properties; + } + + public void validateStartup() { + if (!properties.runMode().p0Allowed()) { + throw new TraderException(TraderErrorCode.TRADER_P0_MODE_BLOCKED, + "P0 only allows REPLAY_SIM or SHADOW run-mode"); + } + if (!properties.execution().mode().p0Allowed()) { + throw new TraderException(TraderErrorCode.TRADER_P0_MODE_BLOCKED, + "P0 only allows REPLAY_SIM or SHADOW execution.mode"); + } + if (properties.runtime().tradingEnabled()) { + throw new TraderException(TraderErrorCode.TRADER_P0_MODE_BLOCKED, + "P0 must keep trading-enabled=false"); + } + log.info("event=trader.runtime.p0_validated runMode={} executionMode={} tradingEnabled={}", + properties.runMode(), properties.execution().mode(), properties.runtime().tradingEnabled()); + } + + public TraderProperties properties() { + return properties; + } +} diff --git a/src/main/java/com/quantai/trader/runtime/StartupValidationRunner.java b/src/main/java/com/quantai/trader/runtime/StartupValidationRunner.java new file mode 100644 index 0000000..5a89774 --- /dev/null +++ b/src/main/java/com/quantai/trader/runtime/StartupValidationRunner.java @@ -0,0 +1,19 @@ +package com.quantai.trader.runtime; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +public class StartupValidationRunner implements ApplicationRunner { + private final P0RuntimeGuard runtimeGuard; + + public StartupValidationRunner(P0RuntimeGuard runtimeGuard) { + this.runtimeGuard = runtimeGuard; + } + + @Override + public void run(ApplicationArguments args) { + runtimeGuard.validateStartup(); + } +} diff --git a/src/main/java/com/quantai/trader/sample/TrainingLabelSet.java b/src/main/java/com/quantai/trader/sample/TrainingLabelSet.java deleted file mode 100644 index 9ed4aa5..0000000 --- a/src/main/java/com/quantai/trader/sample/TrainingLabelSet.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader.sample; - -import java.math.BigDecimal; -import java.util.Map; - -public record TrainingLabelSet( - Map labels, - BigDecimal netReturnBps1x, - BigDecimal netReturnBps10x, - boolean proxyOnly -) { -} diff --git a/src/main/java/com/quantai/trader/sample/TrainingSampleExporter.java b/src/main/java/com/quantai/trader/sample/TrainingSampleExporter.java deleted file mode 100644 index ef0b39b..0000000 --- a/src/main/java/com/quantai/trader/sample/TrainingSampleExporter.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.quantai.trader.sample; - -import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.domain.PlaybookCandidate; -import com.quantai.trader.domain.TraderAction; -import com.quantai.trader.domain.TraderDecisionCycle; -import com.quantai.trader.domain.TraderMarketSnapshot; -import com.quantai.trader.domain.TraderPositionPath; -import com.quantai.trader.domain.TraderTrainingSample; -import com.quantai.trader.persistence.TraderSampleRepository; -import com.quantai.trader.util.Ids; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.util.LinkedHashMap; -import java.util.Map; - -@Component -public class TrainingSampleExporter { - - private static final Logger log = LoggerFactory.getLogger(TrainingSampleExporter.class); - private final TraderProperties properties; - private final TraderSampleRepository repository; - private final TriggerMarkoutLabeler labeler; - - public TrainingSampleExporter(TraderProperties properties, TraderSampleRepository repository, TriggerMarkoutLabeler labeler) { - this.properties = properties; - this.repository = repository; - this.labeler = labeler; - } - - public TraderTrainingSample export( - TraderDecisionCycle cycle, - TraderMarketSnapshot snapshot, - PlaybookCandidate candidate, - TraderAction action, - TraderPositionPath path - ) { - TrainingLabelSet labelSet = labeler.label(snapshot, candidate, action, path); - Map features = features(cycle, snapshot, candidate, action); - TraderTrainingSample sample = new TraderTrainingSample( - cycle.runId(), - cycle.cycleId(), - Ids.sampleId(cycle, properties.getLabelVersion()), - action == null ? null : action.actionId(), - path == null ? null : path.positionId(), - properties.getFeatureVersion(), - properties.getLabelVersion(), - cycle.cycleTime(), - features, - labelSet.labels(), - labelSet.netReturnBps1x(), - labelSet.netReturnBps10x(), - labelSet.proxyOnly() - ); - log.info( - "event=trader.sample.export_start runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} positionId={} sampleId={} proxyOnly={} labelStatus={}", - cycle.runId(), - cycle.cycleId(), - cycle.symbol(), - cycle.playbookId(), - cycle.playbookVersion(), - cycle.state(), - sample.actionId(), - sample.positionId(), - sample.sampleId(), - sample.proxyOnly(), - sample.labels().get("label_status") - ); - repository.insert(sample); - log.info( - "event=trader.sample.exported runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} positionId={} sampleId={} proxyOnly={} netReturnBps1x={}", - cycle.runId(), - cycle.cycleId(), - cycle.symbol(), - cycle.playbookId(), - cycle.playbookVersion(), - cycle.state(), - sample.actionId(), - sample.positionId(), - sample.sampleId(), - sample.proxyOnly(), - sample.netReturnBps1x() - ); - return sample; - } - - private Map features( - TraderDecisionCycle cycle, - TraderMarketSnapshot snapshot, - PlaybookCandidate candidate, - TraderAction action - ) { - Map features = new LinkedHashMap<>(); - features.put("playbookId", candidate == null ? cycle.playbookId() : candidate.playbookId()); - features.put("playbookVersion", candidate == null ? cycle.playbookVersion() : candidate.playbookVersion()); - features.put("state", cycle.state().name()); - features.put("actionType", action == null ? "NONE" : action.actionType().name()); - if (candidate != null) { - features.put("candidateSide", candidate.side().name()); - features.put("candidateVariant", candidate.variant()); - } - if (snapshot != null) { - features.put("context", snapshot.contextFeatures()); - features.put("setup", snapshot.setupFeatures()); - features.put("trigger", snapshot.triggerFeatures()); - features.put("execution", snapshot.executionFeatures()); - features.put("dataQuality", snapshot.dataQuality()); - } - return features; - } -} diff --git a/src/main/java/com/quantai/trader/sample/TriggerMarkoutLabeler.java b/src/main/java/com/quantai/trader/sample/TriggerMarkoutLabeler.java deleted file mode 100644 index e4d1d02..0000000 --- a/src/main/java/com/quantai/trader/sample/TriggerMarkoutLabeler.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.quantai.trader.sample; - -import com.quantai.trader.domain.PlaybookCandidate; -import com.quantai.trader.domain.TraderAction; -import com.quantai.trader.domain.TraderMarketSnapshot; -import com.quantai.trader.domain.TraderPositionPath; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.util.LinkedHashMap; -import java.util.Map; - -@Component -public class TriggerMarkoutLabeler { - - private static final MathContext MC = new MathContext(16, RoundingMode.HALF_UP); - private static final BigDecimal TAKER_FEE_ROUND_TRIP_BPS = new BigDecimal("8.0"); - - public TrainingLabelSet label( - TraderMarketSnapshot snapshot, - PlaybookCandidate candidate, - TraderAction action, - TraderPositionPath path - ) { - Map labels = new LinkedHashMap<>(); - labels.put("label_family", "TRIGGER_MARKOUT"); - labels.put("trigger_acceptance", action != null); - labels.put("target_before_stop", path != null && path.targetBeforeStop()); - labels.put("stagnation_timeout_hit", path != null && path.stagnationTimeoutHit()); - labels.put("action_type", action == null ? "NONE" : action.actionType().name()); - if (candidate != null) { - labels.put("candidate_side", candidate.side().name()); - } - - if (snapshot == null || snapshot.labelInputs().isEmpty()) { - labels.put("label_status", "PROXY_ONLY_NO_REPLAY_LABEL"); - labels.put("best_counterfactual_action", action == null ? "WAIT" : action.actionType().name()); - return new TrainingLabelSet(labels, null, null, true); - } - - Map labelInputs = snapshot.labelInputs(); - labelInputs.forEach((key, value) -> labels.put("replay_" + key, value)); - String labelStatus = String.valueOf(labelInputs.getOrDefault("labelStatus", "UNKNOWN")); - labels.put("label_status", labelStatus); - labels.put("best_counterfactual_action", counterfactualAction(labelInputs)); - - BigDecimal netReturn1x = netReturn1x(labelInputs); - BigDecimal netReturn10x = netReturn1x == null - ? null - : netReturn1x.multiply(BigDecimal.TEN, MC).setScale(8, RoundingMode.HALF_UP); - boolean proxyOnly = !"REPLAY_MARKOUT_LABELED".equals(labelStatus); - return new TrainingLabelSet(labels, netReturn1x, netReturn10x, proxyOnly); - } - - private String counterfactualAction(Map labelInputs) { - BigDecimal netReturn = netReturn1x(labelInputs); - if (netReturn == null) { - return "WAIT"; - } - return netReturn.compareTo(BigDecimal.ZERO) > 0 ? "OPEN_INITIAL" : "WAIT"; - } - - private BigDecimal netReturn1x(Map labelInputs) { - BigDecimal markout15m = decimal(labelInputs.get("markoutBps15m")); - BigDecimal expectedSlippage = decimal(labelInputs.get("expectedSlippageBps")); - if (markout15m == null || expectedSlippage == null) { - return null; - } - // TriggerMarkout is a market-path label. Round-trip taker fee and - // level_1 expected slippage keep the label cost-aware without pretending - // we have real App fill feedback. - BigDecimal executionCost = TAKER_FEE_ROUND_TRIP_BPS.add(expectedSlippage.multiply(BigDecimal.valueOf(2), MC), MC); - return markout15m.subtract(executionCost, MC).setScale(8, RoundingMode.HALF_UP); - } - - private BigDecimal decimal(Object value) { - if (value instanceof BigDecimal decimal) { - return decimal; - } - if (value instanceof Number number) { - return BigDecimal.valueOf(number.doubleValue()); - } - if (value instanceof String text && !text.isBlank()) { - return new BigDecimal(text); - } - return null; - } -} diff --git a/src/main/java/com/quantai/trader/state/TraderDecisionCycleFactory.java b/src/main/java/com/quantai/trader/state/TraderDecisionCycleFactory.java deleted file mode 100644 index f985899..0000000 --- a/src/main/java/com/quantai/trader/state/TraderDecisionCycleFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.quantai.trader.state; - -import com.quantai.trader.domain.TraderDecisionCycle; -import com.quantai.trader.domain.TraderMarketSnapshot; -import com.quantai.trader.enums.TraderState; - -public final class TraderDecisionCycleFactory { - - private TraderDecisionCycleFactory() { - } - - public static TraderDecisionCycle create(TraderMarketSnapshot snapshot, TraderRuntimeState runtimeState) { - return new TraderDecisionCycle( - runtimeState.runId(), - snapshot.cycleId(), - snapshot.snapshotId(), - snapshot.symbol(), - runtimeState.playbookId(), - runtimeState.playbookVersion(), - TraderState.CONTEXT_CHECK, - snapshot.snapshotTime(), - runtimeState.runMode(), - "CREATED", - null - ); - } - - public static TraderDecisionCycle next(TraderDecisionCycle cycle, TraderState state) { - return cycle.withState(state, "RUNNING", null); - } -} diff --git a/src/main/java/com/quantai/trader/state/TraderRuntimeState.java b/src/main/java/com/quantai/trader/state/TraderRuntimeState.java deleted file mode 100644 index 90f626b..0000000 --- a/src/main/java/com/quantai/trader/state/TraderRuntimeState.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.quantai.trader.state; - -import com.quantai.trader.enums.TraderRunMode; - -public record TraderRuntimeState( - String runId, - TraderRunMode runMode, - String playbookId, - String playbookVersion -) { -} diff --git a/src/main/java/com/quantai/trader/state/TraderStateMachine.java b/src/main/java/com/quantai/trader/state/TraderStateMachine.java deleted file mode 100644 index d9f7c34..0000000 --- a/src/main/java/com/quantai/trader/state/TraderStateMachine.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.quantai.trader.state; - -import com.quantai.trader.domain.PlaybookCandidate; -import com.quantai.trader.domain.TraderAction; -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.enums.TraderActionType; -import com.quantai.trader.enums.TraderErrorCode; -import com.quantai.trader.enums.TraderState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.Set; - -@Component -public class TraderStateMachine { - - private static final Logger log = LoggerFactory.getLogger(TraderStateMachine.class); - private static final Set MANAGEMENT_ACTIONS = Set.of( - TraderActionType.HOLD, - TraderActionType.REDUCE, - TraderActionType.MOVE_STOP, - TraderActionType.CLOSE, - TraderActionType.CANCEL, - TraderActionType.REQUOTE - ); - - public TraderAction toInitialEntryAction( - TraderDecisionCycle cycle, - PlaybookCandidate candidate, - TraderEntryPlan entryPlan - ) { - requireState(cycle.state(), TraderState.ENTRY_PLANNED); - requireAction(entryPlan.entryAction(), TraderActionType.OPEN_INITIAL); - requireEntryPlanComplete(entryPlan); - TraderAction action = new TraderAction( - cycle.runId(), - cycle.cycleId(), - entryPlan.actionId(), - TraderActionType.OPEN_INITIAL, - candidate.playbookId(), - candidate.playbookVersion(), - cycle.symbol(), - candidate.side(), - entryPlan.entryPrice(), - null, - cycle.cycleTime(), - entryPlan.reason(), - Map.of( - "entryLegId", entryPlan.entryLegId(), - "plannedLegRatio", entryPlan.plannedLegRatio(), - "plannedLegCount", entryPlan.plannedLegCount(), - "proxyOnly", true - ), - "SHADOW_CREATED" - ); - logActionCreated(cycle, action); - return action; - } - - public TraderAction toPlannedLegAction( - TraderDecisionCycle cycle, - TraderEntryPlan plannedLegPlan, - TraderPositionPath currentPath - ) { - requireState(cycle.state(), TraderState.PLANNED_LEG_WAIT); - requireAction(plannedLegPlan.entryAction(), TraderActionType.OPEN_PLANNED_LEG); - requireEntryPlanComplete(plannedLegPlan); - if (currentPath == null || !currentPath.opened()) { - throw illegal("planned leg requires an opened proxy position"); - } - if (currentPath.reduceSeen()) { - throw new TraderException( - TraderErrorCode.TRADER_PLANNED_LEG_AFTER_REDUCE, - "planned leg cannot be sent after reduce" - ); - } - TraderAction action = new TraderAction( - cycle.runId(), - cycle.cycleId(), - plannedLegPlan.actionId(), - TraderActionType.OPEN_PLANNED_LEG, - cycle.playbookId(), - cycle.playbookVersion(), - cycle.symbol(), - currentPath.side(), - plannedLegPlan.entryPrice(), - null, - cycle.cycleTime(), - plannedLegPlan.reason(), - Map.of( - "entryLegId", plannedLegPlan.entryLegId(), - "positionId", currentPath.positionId(), - "plannedLegRatio", plannedLegPlan.plannedLegRatio(), - "proxyOnly", true - ), - "SHADOW_CREATED" - ); - logActionCreated(cycle, action); - return action; - } - - public TraderAction toManagementAction( - TraderDecisionCycle cycle, - TraderPositionPath path, - TraderActionType actionType - ) { - requireState(cycle.state(), TraderState.MANAGING); - if (!MANAGEMENT_ACTIONS.contains(actionType)) { - throw illegal("illegal management action: " + actionType); - } - if (path == null || !path.opened()) { - throw illegal("management action requires an opened proxy position"); - } - TraderAction action = new TraderAction( - cycle.runId(), - cycle.cycleId(), - cycle.cycleId() + "_" + actionType, - actionType, - cycle.playbookId(), - cycle.playbookVersion(), - cycle.symbol(), - path.side(), - path.currentPrice(), - null, - cycle.cycleTime(), - "MANAGEMENT_" + actionType, - Map.of( - "positionId", path.positionId(), - "proxyOnly", true - ), - "SHADOW_CREATED" - ); - logActionCreated(cycle, action); - return action; - } - - private void requireEntryPlanComplete(TraderEntryPlan entryPlan) { - if (!entryPlan.completeForEntry()) { - throw new TraderException( - TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE, - "entry plan must contain entry/invalid/stop/target/maxHold" - ); - } - } - - private void requireState(TraderState actual, TraderState expected) { - if (actual != expected) { - throw illegal("state " + actual + " cannot output an action that requires " + expected); - } - } - - private void requireAction(TraderActionType actual, TraderActionType expected) { - if (actual != expected) { - throw illegal("entry plan action " + actual + " must be " + expected); - } - } - - private TraderException illegal(String message) { - return new TraderException(TraderErrorCode.TRADER_ILLEGAL_ACTION_TRANSITION, message); - } - - private void logActionCreated(TraderDecisionCycle cycle, TraderAction action) { - log.info( - "event=trader.action.created runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} actionType={} positionId={} reason={}", - cycle.runId(), - cycle.cycleId(), - cycle.symbol(), - cycle.playbookId(), - cycle.playbookVersion(), - cycle.state(), - action.actionId(), - action.actionType(), - action.actionContext().get("positionId"), - action.reason() - ); - } -} diff --git a/src/main/java/com/quantai/trader/util/Ids.java b/src/main/java/com/quantai/trader/util/Ids.java deleted file mode 100644 index cbb588d..0000000 --- a/src/main/java/com/quantai/trader/util/Ids.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.quantai.trader.util; - -import com.quantai.trader.domain.TraderDecisionCycle; - -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.Locale; -import java.util.UUID; - -public final class Ids { - - private static final DateTimeFormatter RUN_FORMATTER = - DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(ZoneOffset.UTC); - - private Ids() { - } - - public static String runId(Instant now) { - return "trader_run_" + RUN_FORMATTER.format(now) + "_" + suffix(); - } - - public static String cycleId(String runId, String symbol, Instant eventTime) { - return "trader_cycle_" + runId + "_" + symbol + "_" + eventTime.toEpochMilli(); - } - - public static String snapshotId(String cycleId) { - return "trader_snapshot_" + cycleId; - } - - public static String candidateId(TraderDecisionCycle cycle, String playbookId) { - return "trader_candidate_" + cycle.cycleId() + "_" + playbookId; - } - - public static String actionId(TraderDecisionCycle cycle, int sequence) { - return "trader_action_" + cycle.cycleId() + "_" + sequence; - } - - public static String entryLegId(TraderDecisionCycle cycle, int index) { - return "trader_leg_" + actionId(cycle, index + 1) + "_" + index; - } - - public static String positionId(String runId, String symbol, String side, int sequence) { - return "trader_position_" + runId + "_" + symbol + "_" + side + "_" + sequence; - } - - public static String evidenceId(TraderDecisionCycle cycle, String stage) { - return "trader_evidence_" + cycle.cycleId() + "_" + slug(stage) + "_" + suffix(); - } - - public static String sampleId(TraderDecisionCycle cycle, String labelVersion) { - return "trader_sample_" + cycle.cycleId() + "_" + slug(labelVersion); - } - - public static String reportId(String runId) { - return "trader_report_" + runId; - } - - private static String slug(String value) { - return value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "_"); - } - - private static String suffix() { - return UUID.randomUUID().toString().replace("-", "").substring(0, 8); - } -} diff --git a/src/main/java/com/quantai/trader/util/TraderNumbers.java b/src/main/java/com/quantai/trader/util/TraderNumbers.java new file mode 100644 index 0000000..726c14a --- /dev/null +++ b/src/main/java/com/quantai/trader/util/TraderNumbers.java @@ -0,0 +1,73 @@ +package com.quantai.trader.util; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Collection; +import java.util.Objects; + +public final class TraderNumbers { + public static final BigDecimal ZERO = BigDecimal.ZERO; + public static final BigDecimal ONE = BigDecimal.ONE; + public static final BigDecimal HUNDRED = new BigDecimal("100"); + + private TraderNumbers() { + } + + public static BigDecimal required(BigDecimal value, String field) { + return Objects.requireNonNull(value, field + " is required"); + } + + public static String requiredText(String value, String field) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(field + " is required"); + } + return value; + } + + public static BigDecimal probability(BigDecimal value, String field) { + BigDecimal checked = required(value, field); + if (checked.compareTo(ZERO) < 0 || checked.compareTo(ONE) > 0) { + throw new IllegalArgumentException(field + " must be in [0,1]"); + } + return checked; + } + + public static BigDecimal nonNegative(BigDecimal value, String field) { + BigDecimal checked = required(value, field); + if (checked.compareTo(ZERO) < 0) { + throw new IllegalArgumentException(field + " must be >= 0"); + } + return checked; + } + + public static BigDecimal positive(BigDecimal value, String field) { + BigDecimal checked = required(value, field); + if (checked.compareTo(ZERO) <= 0) { + throw new IllegalArgumentException(field + " must be > 0"); + } + return checked; + } + + public static BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator) { + BigDecimal den = positive(denominator, "denominator"); + return required(numerator, "numerator").divide(den, MathContext.DECIMAL64); + } + + public static BigDecimal clamp(BigDecimal value, BigDecimal min, BigDecimal max) { + BigDecimal checked = required(value, "value"); + if (min.compareTo(max) > 0) { + return ZERO; + } + if (checked.compareTo(min) < 0) { + return min; + } + if (checked.compareTo(max) > 0) { + return max; + } + return checked; + } + + public static Collection requiredCollection(Collection value, String field) { + return Objects.requireNonNull(value, field + " is required"); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e039716..213d15d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,21 +6,11 @@ spring: username: ${TRADER_DB_USERNAME:quant_trader} password: ${TRADER_DB_PASSWORD:quant_trader} driver-class-name: com.mysql.cj.jdbc.Driver - hikari: - maximum-pool-size: ${TRADER_DB_HIKARI_MAX_POOL_SIZE:10} - minimum-idle: ${TRADER_DB_HIKARI_MIN_IDLE:2} - connection-timeout: ${TRADER_DB_HIKARI_CONNECTION_TIMEOUT_MS:30000} - validation-timeout: ${TRADER_DB_HIKARI_VALIDATION_TIMEOUT_MS:5000} - leak-detection-threshold: ${TRADER_DB_HIKARI_LEAK_DETECTION_MS:60000} - jdbc: - template: - query-timeout: ${TRADER_JDBC_QUERY_TIMEOUT_SECONDS:20} flyway: enabled: ${TRADER_FLYWAY_ENABLED:true} locations: classpath:db/migration validate-on-migrate: true clean-disabled: true - baseline-on-migrate: false management: endpoints: @@ -28,39 +18,36 @@ management: exposure: include: health,info -mybatis-plus: - mapper-locations: classpath*:/mapper/**/*.xml - global-config: - banner: false - trader: service-name: quant-trader-service - run-mode: REPLAY - symbol: BTCUSDT - feature-version: trader_feature_v0 - label-version: trader_label_v0 - playbook: - location-pattern: classpath:/playbooks/*.yml - replay: - output-dir: /Users/zach/Desktop/app/trader/replay-output - fail-on-data-missing: true - integration: - app-action-channel: JAR_FUTURE - http-feedback-enabled: false + run-mode: SHADOW + symbol: BTC-USDT-PERP + artifact: + model-bundle-version: trader-v4-btc-p0 + calibration-bundle-version: cal-v4-btc-p0 + pm-config-version: pm-v4-btc-p0 + artifact-root: /Users/zach/Desktop/quant-strategy-training-data/trader-v4/artifact_bundle + feedback: + http-enabled: false + execution: + mode: SHADOW + max-api-error-count: 3 + max-exchange-latency-ms: 1500 + runtime: + redis-key-prefix: trader:v4 + require-redis-for-open-add: true + trading-enabled: false + outbox: + enabled: true + max-retry-count: 5 + release: + require-review-for-paper: true + require-review-for-live-probe: true + active-pointer-check-enabled: true risk: - leverage-screen: 10 - require-one-x-not-negative: true - max-planned-entry-legs: 3 - allow-free-scale-in: false - allow-reduce-then-add: false - require-stop: true - require-target: true - require-invalid: true - sizing: - method: SIGNAL_EXECUTION_RISK_DYNAMIC - allow-full-initial-entry: true - max-planned-entry-legs: 3 - max-total-position-ratio: 1.0 + max-daily-loss-bps: 200 + max-total-exposure-ratio: 1.0 + min-liquidation-buffer-bps: 500 + position-manager: max-single-leg-ratio: 1.0 - data-source: - hash-mode: FULL_HASH_OR_SCHEMA_ROW_TIME_MISSING_SUMMARY + max-total-position-ratio: 1.0 diff --git a/src/main/resources/db/migration/V1__trader_p0_schema.sql b/src/main/resources/db/migration/V1__trader_p0_schema.sql deleted file mode 100644 index 1a2910c..0000000 --- a/src/main/resources/db/migration/V1__trader_p0_schema.sql +++ /dev/null @@ -1,441 +0,0 @@ -create table trader_playbook_definition ( - id bigint primary key auto_increment, - playbook_id varchar(64) not null, - playbook_version varchar(64) not null, - family varchar(64) not null, - variant varchar(64) not null, - side_mode varchar(16) not null, - source_path varchar(512) not null, - definition_hash_sha256 varchar(64) not null, - definition_json json not null, - loaded_at datetime(3) not null, - status varchar(32) not null, - created_at datetime(3) not null default current_timestamp(3), - updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), - unique key uk_trader_playbook_version (playbook_id, playbook_version), - unique key uk_trader_playbook_hash (playbook_id, definition_hash_sha256), - key idx_trader_playbook_status (status, loaded_at) -); - -create table trader_data_source_manifest ( - id bigint primary key auto_increment, - source_id varchar(96) not null, - symbol varchar(32) not null, - source_type varchar(64) not null, - exchange varchar(64) not null, - granularity varchar(32) not null, - source_path varchar(512) not null, - content_hash_sha256 varchar(64) null, - schema_hash_sha256 varchar(64) null, - data_from datetime(3) not null, - data_to datetime(3) not null, - min_event_time datetime(3) null, - max_event_time datetime(3) null, - timezone varchar(64) not null, - row_count bigint null, - missing_summary_json json not null, - quality_status varchar(32) not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_data_source (source_id), - key idx_trader_data_source_symbol_type (symbol, source_type, data_from, data_to), - key idx_trader_data_source_quality (quality_status, created_at) -); - -create table trader_replay_run ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - run_mode varchar(16) not null, - symbol varchar(32) not null, - playbook_id varchar(64) not null, - playbook_version varchar(64) not null, - playbook_definition_hash varchar(64) not null, - data_from datetime(3) not null, - data_to datetime(3) not null, - feature_version varchar(64) not null, - label_version varchar(64) not null, - data_source_manifest_json json not null, - status varchar(32) not null, - config_json json not null, - started_at datetime(3) null, - finished_at datetime(3) null, - created_at datetime(3) not null default current_timestamp(3), - updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), - unique key uk_trader_replay_run_id (run_id), - key idx_trader_replay_run_status (status, started_at), - key idx_trader_replay_run_symbol_time (symbol, data_from, data_to) -); - -create table trader_market_event ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - event_id varchar(96) not null, - symbol varchar(32) not null, - event_time datetime(3) not null, - source varchar(64) not null, - source_path varchar(512) null, - payload_json json not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_market_event (run_id, event_id), - key idx_trader_market_event_symbol_time (run_id, symbol, event_time), - key idx_trader_market_event_source (run_id, source, event_time) -); - -create table trader_market_snapshot ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - snapshot_id varchar(96) not null, - symbol varchar(32) not null, - snapshot_time datetime(3) not null, - feature_version varchar(64) not null, - context_features_json json not null, - setup_features_json json not null, - trigger_features_json json not null, - execution_features_json json not null, - data_quality_json json not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_snapshot (run_id, snapshot_id), - key idx_trader_snapshot_cycle (run_id, cycle_id), - key idx_trader_snapshot_symbol_time (run_id, symbol, snapshot_time) -); - -create table trader_decision_cycle ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - snapshot_id varchar(96) not null, - symbol varchar(32) not null, - playbook_id varchar(64) not null, - playbook_version varchar(64) not null, - state varchar(48) not null, - cycle_time datetime(3) not null, - run_mode varchar(16) not null, - decision_status varchar(32) not null, - blocker varchar(128) null, - created_at datetime(3) not null default current_timestamp(3), - updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), - unique key uk_trader_cycle (run_id, cycle_id), - key idx_trader_cycle_symbol_time (run_id, symbol, cycle_time), - key idx_trader_cycle_state (run_id, state, cycle_time) -); - -create table trader_playbook_candidate ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - candidate_id varchar(96) not null, - playbook_id varchar(64) not null, - playbook_version varchar(64) not null, - side varchar(16) not null, - variant varchar(64) not null, - candidate_time datetime(3) not null, - entry_price decimal(28,10) null, - invalid_price decimal(28,10) null, - stop_price decimal(28,10) null, - target_price decimal(28,10) null, - partial_take_profit_price decimal(28,10) null, - max_entry_wait_ms bigint null, - max_hold_ms bigint null, - setup_evidence_json json not null, - status varchar(32) not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_candidate (run_id, candidate_id), - key idx_trader_candidate_cycle (run_id, cycle_id), - key idx_trader_candidate_playbook_time (run_id, playbook_id, candidate_time) -); - -create table trader_trigger_event ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - candidate_id varchar(96) not null, - trigger_id varchar(96) not null, - trigger_time datetime(3) not null, - trigger_timeframe varchar(16) not null, - feature_version varchar(64) not null, - trigger_evidence_json json not null, - markout_json json null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_trigger (run_id, trigger_id), - key idx_trader_trigger_candidate (run_id, candidate_id), - key idx_trader_trigger_time (run_id, trigger_time) -); - -create table trader_entry_plan ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - action_id varchar(96) not null, - entry_leg_id varchar(96) not null, - candidate_id varchar(96) not null, - entry_action varchar(32) not null, - planned_leg_index int not null, - planned_leg_count int not null, - planned_leg_ratio decimal(18,8) not null, - sizing_method varchar(64) not null, - signal_strength_score decimal(18,8) null, - execution_quality_score decimal(18,8) null, - risk_gate_score decimal(18,8) null, - entry_price decimal(28,10) not null, - invalid_price decimal(28,10) not null, - stop_price decimal(28,10) not null, - target_price decimal(28,10) not null, - partial_take_profit_price decimal(28,10) null, - max_entry_wait_ms bigint not null, - max_hold_ms bigint not null, - reason varchar(256) not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_entry_leg (run_id, entry_leg_id), - key idx_trader_entry_action (run_id, action_id), - key idx_trader_entry_candidate (run_id, candidate_id), - constraint chk_trader_entry_action check (entry_action in ('OPEN_INITIAL', 'OPEN_PLANNED_LEG')), - constraint chk_trader_entry_leg_index check (planned_leg_index >= 0 and planned_leg_count between 1 and 3 and planned_leg_index < planned_leg_count), - constraint chk_trader_entry_action_leg check ( - (entry_action = 'OPEN_INITIAL' and planned_leg_index = 0) - or (entry_action = 'OPEN_PLANNED_LEG' and planned_leg_index > 0) - ), - constraint chk_trader_entry_leg_ratio check (planned_leg_ratio > 0 and planned_leg_ratio <= 1) -); - -create table trader_action ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - action_id varchar(96) not null, - action_type varchar(32) not null, - playbook_id varchar(64) not null, - playbook_version varchar(64) not null, - symbol varchar(32) not null, - side varchar(16) not null, - price decimal(28,10) null, - quantity decimal(28,10) null, - action_time datetime(3) not null, - reason varchar(256) not null, - action_context_json json not null, - send_status varchar(32) not null, - created_at datetime(3) not null default current_timestamp(3), - updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), - unique key uk_trader_action (run_id, action_id), - key idx_trader_action_cycle (run_id, cycle_id), - key idx_trader_action_symbol_time (run_id, symbol, action_time), - constraint chk_trader_action_type check (action_type in ('WAIT','OPEN_INITIAL','OPEN_PLANNED_LEG','HOLD','REDUCE','MOVE_STOP','CLOSE','CANCEL','REQUOTE')) -); - -create table trader_app_feedback ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - action_id varchar(96) not null, - feedback_type varchar(32) not null, - feedback_source varchar(32) not null, - proxy_method varchar(64) null, - simulator_version varchar(64) null, - is_real_fill boolean not null, - order_id varchar(96) null, - position_id varchar(96) null, - order_status varchar(64) null, - app_received_time datetime(3) null, - exchange_ack_time datetime(3) null, - filled_time datetime(3) null, - filled_price decimal(28,10) null, - filled_quantity decimal(28,10) null, - fee decimal(28,10) null, - slippage_bps decimal(18,8) null, - close_reason varchar(128) null, - close_signal_source varchar(128) null, - exchange_error_code varchar(64) null, - platform_error_code varchar(64) null, - raw_feedback_json json not null, - created_at datetime(3) not null default current_timestamp(3), - key idx_trader_feedback_action (run_id, action_id), - key idx_trader_feedback_position (run_id, position_id), - key idx_trader_feedback_type_time (run_id, feedback_type, app_received_time), - key idx_trader_feedback_source (run_id, feedback_source, is_real_fill), - constraint chk_trader_feedback_source check (feedback_source in ('MARKET_PROXY','SHADOW_APP','PAPER_APP','REAL_APP')), - constraint chk_trader_real_fill_source check ( - (is_real_fill = true and feedback_source in ('PAPER_APP','REAL_APP')) - or (is_real_fill = false and feedback_source in ('MARKET_PROXY','SHADOW_APP')) - ) -); - -create table trader_account_state ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - account_state_id varchar(96) not null, - snapshot_time datetime(3) not null, - equity_usdt decimal(28,10) null, - available_margin_usdt decimal(28,10) null, - used_margin_usdt decimal(28,10) null, - open_position_count int not null default 0, - leverage_limit decimal(18,8) null, - daily_realized_pnl_bps decimal(18,8) null, - max_drawdown_bps decimal(18,8) null, - consecutive_loss_count int not null default 0, - raw_account_json json not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_account_state (run_id, account_state_id), - key idx_trader_account_state_time (run_id, snapshot_time) -); - -create table trader_risk_decision ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - action_id varchar(96) null, - account_state_id varchar(96) null, - action_type varchar(32) not null, - leverage_screen decimal(18,8) not null, - planned_total_position_ratio decimal(18,8) not null, - max_loss_bps decimal(18,8) not null, - liquidation_buffer_bps decimal(18,8) null, - expected_value_bps_1x decimal(18,8) null, - expected_value_bps_10x decimal(18,8) null, - uncertainty decimal(18,8) null, - ood_score decimal(18,8) null, - allow_action boolean not null, - blocker varchar(128) null, - decision_json json not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_risk_decision (run_id, cycle_id, action_type), - key idx_trader_risk_action (run_id, action_id), - key idx_trader_risk_blocker (run_id, blocker, created_at), - constraint chk_trader_risk_position_ratio check (planned_total_position_ratio >= 0 and planned_total_position_ratio <= 1), - constraint chk_trader_risk_action check (action_type in ('OPEN_INITIAL','OPEN_PLANNED_LEG','HOLD','REDUCE','MOVE_STOP','CLOSE','CANCEL','REQUOTE')) -); - -create table trader_position_leg ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - position_id varchar(96) not null, - leg_id varchar(96) not null, - action_type varchar(32) not null, - quantity decimal(28,10) not null, - price decimal(28,10) not null, - leg_ratio decimal(18,8) not null, - risk_delta_bps decimal(18,8) not null, - action_time datetime(3) not null, - reason varchar(256) not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_position_leg (run_id, leg_id), - key idx_trader_position_leg_position (run_id, position_id, action_time), - constraint chk_trader_position_leg_action check (action_type in ('OPEN_INITIAL','OPEN_PLANNED_LEG','REDUCE','CLOSE')) -); - -create table trader_position_path ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - action_id varchar(96) not null, - position_id varchar(96) not null, - entry_time datetime(3) not null, - last_event_time datetime(3) not null, - entry_price decimal(28,10) not null, - current_price decimal(28,10) not null, - mfe_bps decimal(18,8) null, - mae_bps decimal(18,8) null, - time_to_target_ms bigint null, - time_to_invalid_ms bigint null, - target_before_stop boolean not null, - stagnation_timeout_hit boolean not null, - path_summary_json json not null, - created_at datetime(3) not null default current_timestamp(3), - updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), - unique key uk_trader_position_path (run_id, position_id), - key idx_trader_position_path_cycle (run_id, cycle_id), - key idx_trader_position_path_time (run_id, entry_time, last_event_time) -); - -create table trader_management_action ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - action_id varchar(96) not null, - management_action_id varchar(96) not null, - position_id varchar(96) not null, - management_action_type varchar(32) not null, - action_time datetime(3) not null, - before_risk_bps decimal(18,8) null, - after_risk_bps decimal(18,8) null, - reason varchar(256) not null, - details_json json not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_management_action (run_id, management_action_id), - key idx_trader_management_position (run_id, position_id, action_time), - constraint chk_trader_management_action check (management_action_type in ('HOLD','REDUCE','MOVE_STOP','CLOSE','CANCEL','REQUOTE')) -); - -create table trader_evidence ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - evidence_id varchar(96) not null, - stage varchar(64) not null, - pass boolean not null, - reason varchar(256) not null, - blocker varchar(128) null, - evidence_time datetime(3) not null, - details_json json not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_evidence (run_id, evidence_id), - key idx_trader_evidence_cycle_stage (run_id, cycle_id, stage), - key idx_trader_evidence_blocker (run_id, blocker, evidence_time) -); - -create table trader_training_sample ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - cycle_id varchar(96) not null, - sample_id varchar(96) not null, - action_id varchar(96) null, - position_id varchar(96) null, - feature_version varchar(64) not null, - label_version varchar(64) not null, - sample_time datetime(3) not null, - features_json json not null, - labels_json json not null, - net_return_bps_1x decimal(18,8) null, - net_return_bps_10x decimal(18,8) null, - proxy_only boolean not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_training_sample (run_id, sample_id), - key idx_trader_training_cycle (run_id, cycle_id), - key idx_trader_training_versions (feature_version, label_version, proxy_only), - key idx_trader_training_time (run_id, sample_time) -); - -create table trader_model_manifest ( - id bigint primary key auto_increment, - model_name varchar(96) not null, - model_version varchar(96) not null, - feature_version varchar(64) not null, - label_version varchar(64) not null, - artifact_path varchar(512) not null, - trained_at datetime(3) not null, - metrics_json json not null, - status varchar(32) not null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_model_manifest (model_name, model_version), - key idx_trader_model_status (model_name, status, trained_at) -); - -create table trader_replay_report ( - id bigint primary key auto_increment, - run_id varchar(64) not null, - report_id varchar(96) not null, - symbol varchar(32) not null, - playbook_id varchar(64) not null, - playbook_version varchar(64) not null, - candidate_events int not null, - months_covered int not null, - base_net_return_bps_1x decimal(18,8) null, - leveraged_net_return_bps_10x decimal(18,8) null, - holdout_return_bps_10x decimal(18,8) null, - strict_vs_loose_json json not null, - failure_risks_json json not null, - conclusion varchar(64) not null, - report_path varchar(512) null, - created_at datetime(3) not null default current_timestamp(3), - unique key uk_trader_replay_report (run_id, report_id), - key idx_trader_replay_report_conclusion (conclusion, created_at) -); diff --git a/src/main/resources/db/migration/V1__trader_v4_p0_schema.sql b/src/main/resources/db/migration/V1__trader_v4_p0_schema.sql new file mode 100644 index 0000000..99035ca --- /dev/null +++ b/src/main/resources/db/migration/V1__trader_v4_p0_schema.sql @@ -0,0 +1,234 @@ +create table trader_run ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + run_mode varchar(16) not null, + symbol varchar(32) not null, + model_bundle_version varchar(96) not null, + calibration_bundle_version varchar(96) not null, + pm_config_version varchar(96) not null, + execution_mode varchar(32) not null, + status varchar(32) not null, + config_json json not null, + started_at datetime(3) null, + finished_at datetime(3) null, + created_at datetime(3) not null default current_timestamp(3), + updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), + unique key uk_trader_run (run_id), + constraint chk_trader_run_mode check (run_mode in ('REPLAY_SIM','SHADOW','PAPER','REAL')), + constraint chk_trader_execution_mode check (execution_mode in ('REPLAY_SIM','SHADOW','PAPER','REAL')) +); + +create table trader_decision_cycle ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + symbol varchar(32) not null, + cycle_time datetime(3) not null, + state varchar(48) not null, + decision_status varchar(32) not null, + blocker varchar(128) null, + created_at datetime(3) not null default current_timestamp(3), + updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), + unique key uk_trader_cycle (run_id, cycle_id) +); + +create table trader_model_bundle_manifest ( + id bigint primary key auto_increment, + model_bundle_version varchar(96) not null, + calibration_bundle_version varchar(96) not null, + feature_version varchar(96) not null, + label_version varchar(96) not null, + split_version varchar(96) not null, + required_models_json json not null, + provided_models_json json not null, + missing_models_json json not null, + bundle_hash_sha256 varchar(64) not null, + complete boolean not null, + status varchar(32) not null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_model_bundle (model_bundle_version, calibration_bundle_version), + constraint chk_trader_model_bundle_status check (status in ('CANDIDATE','ACTIVE','REJECTED','RETIRED')) +); + +create table trader_pm_config_manifest ( + id bigint primary key auto_increment, + pm_config_version varchar(96) not null, + model_bundle_version varchar(96) not null, + calibration_bundle_version varchar(96) not null, + threshold_stability_json json not null, + allowed_run_modes_json json not null, + config_json json not null, + config_hash_sha256 varchar(64) not null, + status varchar(32) not null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_pm_config (pm_config_version), + constraint chk_trader_pm_config_status check (status in ('CANDIDATE','ACTIVE','REJECTED','RETIRED')) +); + +create table trader_model_output ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + model_output_id varchar(128) not null, + model_bundle_version varchar(96) not null, + calibration_bundle_version varchar(96) not null, + direction_json json not null, + entry_json json not null, + continue_json json not null, + exit_json json not null, + risk_json json not null, + uncertainty decimal(18,8) not null, + ood_score decimal(18,8) not null, + usable boolean not null, + blocker varchar(128) null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_model_output (run_id, model_output_id) +); + +create table trader_position_manager_decision ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + pm_decision_id varchar(128) not null, + model_output_id varchar(128) not null, + position_state_id varchar(128) not null, + account_state_id varchar(128) not null, + execution_state_id varchar(128) not null, + candidate_action varchar(32) not null, + side varchar(16) not null, + price_plan_id varchar(128) null, + price_plan_config_hash varchar(64) null, + target_position_ratio decimal(18,8) null, + add_ratio decimal(18,8) null, + reduce_ratio decimal(18,8) null, + stop_price decimal(28,10) null, + target_price decimal(28,10) null, + reason varchar(256) not null, + decision_json json not null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_pm_decision (run_id, pm_decision_id), + constraint chk_trader_pm_action check (candidate_action in ('WAIT','OPEN_LONG','OPEN_SHORT','ADD_LONG','ADD_SHORT','HOLD','REDUCE_LONG','REDUCE_SHORT','MOVE_STOP','CLOSE_LONG','CLOSE_SHORT','CANCEL')), + constraint chk_trader_pm_price_plan_required check ( + (candidate_action in ('OPEN_LONG','OPEN_SHORT','ADD_LONG','ADD_SHORT') and price_plan_id is not null and price_plan_config_hash is not null) + or candidate_action not in ('OPEN_LONG','OPEN_SHORT','ADD_LONG','ADD_SHORT') + ) +); + +create table trader_risk_decision ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + risk_decision_id varchar(128) not null, + pm_decision_id varchar(128) not null, + original_action varchar(32) not null, + final_action varchar(32) not null, + allow_action boolean not null, + blocker varchar(128) null, + decision_json json not null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_risk_decision (run_id, risk_decision_id), + constraint chk_trader_risk_allow_blocker check ((allow_action = true and blocker is null) or (allow_action = false and blocker is not null)) +); + +create table trader_action ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + action_id varchar(128) not null, + model_output_id varchar(128) not null, + pm_decision_id varchar(128) not null, + risk_decision_id varchar(128) not null, + action_type varchar(32) not null, + symbol varchar(32) not null, + side varchar(16) not null, + price_plan_id varchar(128) null, + price_plan_config_hash varchar(64) null, + position_ratio decimal(18,8) null, + quantity decimal(28,10) null, + stop_price decimal(28,10) null, + target_price decimal(28,10) null, + reduce_only boolean not null default false, + idempotency_key varchar(128) not null, + send_status varchar(32) not null, + reason varchar(256) not null, + action_context_json json not null, + action_time datetime(3) not null default current_timestamp(3), + created_at datetime(3) not null default current_timestamp(3), + updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), + unique key uk_trader_action (run_id, action_id), + unique key uk_trader_action_idempotency (run_id, idempotency_key), + constraint chk_trader_action_type check (action_type in ('WAIT','OPEN_LONG','OPEN_SHORT','ADD_LONG','ADD_SHORT','HOLD','REDUCE_LONG','REDUCE_SHORT','MOVE_STOP','CLOSE_LONG','CLOSE_SHORT','CANCEL')), + constraint chk_trader_action_price_plan_required check ( + (action_type in ('OPEN_LONG','OPEN_SHORT','ADD_LONG','ADD_SHORT') and price_plan_id is not null and price_plan_config_hash is not null) + or action_type not in ('OPEN_LONG','OPEN_SHORT','ADD_LONG','ADD_SHORT') + ), + constraint chk_trader_action_reduce_only check ( + (action_type in ('REDUCE_LONG','REDUCE_SHORT','CLOSE_LONG','CLOSE_SHORT') and reduce_only = true) + or (action_type not in ('REDUCE_LONG','REDUCE_SHORT','CLOSE_LONG','CLOSE_SHORT') and reduce_only = false) + ) +); + +create table trader_app_feedback ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + feedback_id varchar(128) not null, + action_id varchar(128) not null, + feedback_source varchar(32) not null, + is_real_fill boolean not null, + order_id varchar(128) null, + order_status varchar(64) null, + app_received_time datetime(3) null, + exchange_ack_time datetime(3) null, + filled_time datetime(3) null, + filled_price decimal(28,10) null, + filled_quantity decimal(28,10) null, + fee decimal(28,10) null, + slippage_bps decimal(18,8) null, + reject_reason varchar(256) null, + raw_feedback_json json not null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_feedback (run_id, feedback_id), + constraint chk_trader_feedback_source check (feedback_source in ('REPLAY_SIMULATOR','SHADOW_APP','PAPER_APP','REAL_APP')), + constraint chk_trader_real_fill_source check ( + (is_real_fill = true and feedback_source in ('PAPER_APP','REAL_APP')) + or (is_real_fill = false and feedback_source in ('REPLAY_SIMULATOR','SHADOW_APP')) + ) +); + +create table trader_evidence ( + id bigint primary key auto_increment, + run_id varchar(64) not null, + cycle_id varchar(128) not null, + evidence_id varchar(128) not null, + stage varchar(64) not null, + pass boolean not null, + reason varchar(256) not null, + blocker varchar(128) null, + evidence_time datetime(3) not null, + details_json json not null, + created_at datetime(3) not null default current_timestamp(3), + unique key uk_trader_evidence (run_id, evidence_id) +); + +create table trader_outbox ( + id bigint primary key auto_increment, + outbox_id varchar(128) not null, + run_id varchar(64) not null, + cycle_id varchar(128) null, + aggregate_type varchar(64) not null, + aggregate_id varchar(128) not null, + event_type varchar(64) not null, + destination varchar(64) not null, + payload_json json not null, + idempotency_key varchar(128) not null, + status varchar(32) not null, + retry_count int not null default 0, + next_retry_at datetime(3) null, + last_error varchar(512) null, + created_at datetime(3) not null default current_timestamp(3), + updated_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3), + unique key uk_trader_outbox (outbox_id), + unique key uk_trader_outbox_idempotency (destination, idempotency_key), + constraint chk_trader_outbox_status check (status in ('PENDING','SENDING','SENT','FAILED','CANCELLED')) +); diff --git a/src/main/resources/db/migration/V2__trader_persistence_id_lengths.sql b/src/main/resources/db/migration/V2__trader_persistence_id_lengths.sql deleted file mode 100644 index 19f4123..0000000 --- a/src/main/resources/db/migration/V2__trader_persistence_id_lengths.sql +++ /dev/null @@ -1,8 +0,0 @@ -alter table trader_evidence - modify column evidence_id varchar(160) not null; - -alter table trader_training_sample - modify column sample_id varchar(160) not null; - -alter table trader_replay_run - add column failure_reason text null after finished_at; diff --git a/src/main/resources/playbooks/breakout-retest-intraday-5m-60m.yml b/src/main/resources/playbooks/breakout-retest-intraday-5m-60m.yml deleted file mode 100644 index 85ac765..0000000 --- a/src/main/resources/playbooks/breakout-retest-intraday-5m-60m.yml +++ /dev/null @@ -1,61 +0,0 @@ -playbook_id: BREAKOUT_RETEST_CONTINUATION -playbook_version: 2026-06-22.p0 -family: TREND_CONTINUATION -variant: INTRADAY_5M_60M -side_mode: BOTH -context_timeframes: [1h, 4h] -setup_timeframes: [15m, 30m, 60m] -trigger_timeframes: [1m, 3m, 5m] -execution_window: 30s-300s -management_windows: [15m, 30m, 60m] -entry_rule: - name: breakout_retest_entry - description: platform breakout, retest hold, and trigger continuation -planned_entry_leg_rule: - name: preplanned_continuation_entry_leg - description: second or third entry leg must be declared before initial entry - max_planned_entry_legs: 3 - ratio_mode: SIGNAL_EXECUTION_RISK_DYNAMIC - allow_full_initial_entry: true - ratio_template_fixed: false - description_ext: playbook declares leg boundaries, while actual ratios are calculated by SignalStrength, ExecutionQuality, and RiskGate -invalid_rule: - name: retest_platform_invalid - description: price falls back through the retest platform -stop_rule: - name: structural_stop - description: stop beyond invalid zone with leverage-aware risk cap -target_rule: - name: prior_high_or_measured_move - description: target near prior high or measured continuation level -partial_take_profit_rule: - name: action_decision_zone - description: prior high is an action decision zone, not a fixed partial close -max_hold_rule: - name: intraday_timeout - description: exit or downgrade if no progress within 60m-120m -failure_exit_rule: - name: failed_continuation_exit - description: close when continuation fails and price returns into platform -risk_constraints: - max_leverage: "10x_screen_only" - require_1x_not_negative: "true" - require_stop: "true" - require_target: "true" - max_total_position_ratio: "1.0" - max_single_leg_ratio: "1.0" -required_features: - - candles - - trades - - level_1 - - book_proxy_optional -output_actions: - - WAIT - - OPEN_INITIAL - - OPEN_PLANNED_LEG - - HOLD - - REDUCE - - MOVE_STOP - - CLOSE - - CANCEL - - REQUOTE diff --git a/src/test/java/com/quantai/trader/QuantTraderServiceApplicationTest.java b/src/test/java/com/quantai/trader/QuantTraderServiceApplicationTest.java deleted file mode 100644 index 2415636..0000000 --- a/src/test/java/com/quantai/trader/QuantTraderServiceApplicationTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.quantai.trader; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class QuantTraderServiceApplicationTest { - - @Test - void contextLoads() { - } -} diff --git a/src/test/java/com/quantai/trader/TestFixtures.java b/src/test/java/com/quantai/trader/TestFixtures.java index 4c10918..efc02fa 100644 --- a/src/test/java/com/quantai/trader/TestFixtures.java +++ b/src/test/java/com/quantai/trader/TestFixtures.java @@ -1,18 +1,12 @@ package com.quantai.trader; import com.quantai.trader.config.TraderProperties; -import com.quantai.trader.domain.PlaybookCandidate; -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.TraderPricePlan; -import com.quantai.trader.domain.TriggerDecision; +import com.quantai.trader.domain.*; +import com.quantai.trader.enums.PositionSide; import com.quantai.trader.enums.TraderActionType; +import com.quantai.trader.enums.TraderExecutionMode; import com.quantai.trader.enums.TraderRunMode; -import com.quantai.trader.enums.TraderSide; -import com.quantai.trader.enums.TraderState; +import com.quantai.trader.risk.RiskLimits; import java.math.BigDecimal; import java.time.Instant; @@ -20,160 +14,180 @@ import java.util.List; import java.util.Map; public final class TestFixtures { - - public static final Instant NOW = Instant.parse("2026-06-23T12:00:00Z"); + public static final Instant T0 = Instant.parse("2026-06-26T00:00:00Z"); private TestFixtures() { } + public static BigDecimal bd(String value) { + return new BigDecimal(value); + } + public static TraderProperties properties() { - return new TraderProperties(); + return properties(TraderRunMode.SHADOW, TraderExecutionMode.SHADOW, false, false); } - public static TraderDecisionCycle cycle(TraderState state) { - return new TraderDecisionCycle( - "trader_run_test", - "trader_cycle_test", - "trader_snapshot_test", - "BTCUSDT", - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - state, - NOW, - TraderRunMode.REPLAY, - "CREATED", - null - ); + public static TraderProperties properties(TraderRunMode runMode, TraderExecutionMode executionMode, + boolean tradingEnabled, boolean feedbackHttpEnabled) { + return new TraderProperties( + "quant-trader-service-test", + runMode, + "BTC-USDT-PERP", + new TraderProperties.Artifact("trader-v4-btc-p0", "cal-v4-btc-p0", "pm-v4-btc-p0", "/tmp/trader-v4-p0"), + new TraderProperties.Feedback(feedbackHttpEnabled), + new TraderProperties.Execution(executionMode, 3, 1500), + new TraderProperties.Runtime("trader:v4:test", true, tradingEnabled), + new TraderProperties.Outbox(true, 5), + new TraderProperties.Release(true, true, true), + new TraderProperties.Risk(bd("200"), BigDecimal.ONE, bd("500")), + new TraderProperties.PositionManager(BigDecimal.ONE, BigDecimal.ONE)); } - public static TraderPricePlan pricePlan() { - return new TraderPricePlan( - new BigDecimal("65000"), - new BigDecimal("64800"), - new BigDecimal("64920"), - new BigDecimal("65350"), - null, - 300_000, - 7_200_000 - ); + public static TraderPmConfig pmConfig() { + return new TraderPmConfig( + "pm-v4-btc-p0", + new TraderPmConfig.OpenRuleConfig( + bd("0.58"), bd("0.58"), bd("0.55"), bd("0.55"), + bd("0.45"), bd("1.0"), bd("0.03"), bd("0.10"), bd("0.80")), + new TraderPmConfig.AddRuleConfig( + bd("0.60"), bd("0.60"), bd("0.58"), bd("0.55"), bd("0.45"), + bd("0.45"), bd("0.50"), bd("1.0"), BigDecimal.ZERO, bd("0.10"), + bd("500"), 3, 5), + new TraderPmConfig.ExitRuleConfig( + bd("0.70"), bd("0.70"), bd("0.70"), bd("0.25"), bd("0.62"), + bd("0.35"), bd("0.70"), bd("5.0"), bd("80")), + new TraderPmConfig.SizingConfig( + bd("0.80"), bd("0.05"), BigDecimal.ONE, bd("0.02"), bd("0.25"), + BigDecimal.ONE, bd("1.0"), bd("80"), bd("0.20"), bd("0.50"), bd("500"))); } - public static PlaybookCandidate candidate() { - return new PlaybookCandidate( - "trader_run_test", - "trader_cycle_test", - "trader_candidate_test", - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - TraderSide.LONG, - "INTRADAY_5M_60M", - NOW, - pricePlan(), - 3, - Map.of("executionQualityScore", "0.90") - ); + public static TraderDecisionCycle cycle() { + return new TraderDecisionCycle("run-1", "cycle-1", "BTC-USDT-PERP", T0, + TraderRunMode.SHADOW, "trader-v4-btc-p0", "cal-v4-btc-p0", "pm-v4-btc-p0"); } - public static TraderMarketSnapshot labeledSnapshot() { - return new TraderMarketSnapshot( - "trader_run_test", - "trader_cycle_test", - "trader_snapshot_test", - "BTCUSDT", - NOW, - "trader_feature_v0", - Map.of("contextPass", true), - Map.of("setupPass", true, "side", "LONG"), - Map.of("triggerScore", "0.95"), - Map.of("lastPrice", "65010"), - Map.of("missing_features", List.of()), - Map.ofEntries( - Map.entry("labelSource", "CRYPTO_LAKE_1M_REPLAY"), - Map.entry("labelStatus", "REPLAY_MARKOUT_LABELED"), - Map.entry("side", "LONG"), - Map.entry("entryPrice", "65000"), - Map.entry("markoutBps1m", "5"), - Map.entry("markoutBps5m", "12"), - Map.entry("markoutBps15m", "24"), - Map.entry("mfeBps15m", "30"), - Map.entry("maeBps15m", "6"), - Map.entry("targetBeforeStop15m", true), - Map.entry("expectedSlippageBps", "1") - ) - ); + public static TraderMarketSnapshot snapshot() { + return snapshot(true, "1000"); } - public static TriggerDecision strongTrigger() { - return new TriggerDecision(true, new BigDecimal("0.95"), "TRIGGER_ACCEPTED", null, Map.of()); + public static TraderMarketSnapshot snapshot(boolean dataReady, String depthNotional5Bps) { + return new TraderMarketSnapshot("snapshot-1", "run-1", "cycle-1", "BTC-USDT-PERP", T0, + "feature-v4-p0", bd("100"), bd("99.5"), bd("1.2"), BigDecimal.ZERO, + bd(depthNotional5Bps), bd(depthNotional5Bps), bd(depthNotional5Bps), dataReady, Map.of(), Map.of()); } - public static TraderEntryPlan fullInitialPlan() { - return new TraderEntryPlan( - "trader_run_test", - "trader_cycle_test", - "trader_action_test_1", - "trader_leg_test_0", - "trader_candidate_test", - TraderActionType.OPEN_INITIAL, - 0, - 1, - BigDecimal.ONE, - "SIGNAL_EXECUTION_RISK_DYNAMIC", - new BigDecimal("0.95"), - new BigDecimal("0.90"), - new BigDecimal("0.90"), - new BigDecimal("65000"), - new BigDecimal("64800"), - new BigDecimal("64920"), - new BigDecimal("65350"), - null, - 300_000, - 7_200_000, - "TEST_INITIAL_ENTRY" - ); + public static TraderModelOutput modelOutput() { + return modelOutput("0.70", "0.20", "0.70", "0.30", "0.66", "0.34", + "0.20", "0.50", "0.18", "0.12", "1.0", "12", "4", "0.10", "0.05", "20"); } - public static TraderPositionPath openedPath(boolean reduceSeen) { - return new TraderPositionPath( - "trader_run_test", - "trader_cycle_test", - "trader_action_test_1", - "trader_position_test", - TraderSide.LONG, - NOW, - NOW, - new BigDecimal("65000"), - new BigDecimal("65010"), - BigDecimal.ZERO, - BigDecimal.ZERO, - null, - null, - false, - false, - true, - reduceSeen, - 1, - new BigDecimal("0.60"), - Map.of("proxyFill", true) - ); + public static TraderModelOutput shortModelOutput() { + return modelOutput("0.20", "0.70", "0.30", "0.70", "0.34", "0.66", + "0.50", "0.20", "0.18", "0.12", "1.0", "12", "4", "0.10", "0.05", "20"); } - public static TraderAction action() { - return new TraderAction( - "trader_run_test", - "trader_cycle_test", - "trader_action_test_1", - TraderActionType.OPEN_INITIAL, - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - "BTCUSDT", - TraderSide.LONG, - new BigDecimal("65000"), - null, - NOW, - "TEST", - Map.of("plannedLegRatio", BigDecimal.ONE), - "SHADOW_CREATED" - ); + public static TraderModelOutput modelOutput(String longProb, String shortProb, + String longEntryProb, String shortEntryProb, + String longContinueProb, String shortContinueProb, + String longExitProb, String shortExitProb, + String marketRiskProb, String positionRiskProb, + String liquidityCapacityRatio, String expectedEdgeBps, + String continueVsExitEdgeBps, String uncertainty, + String oodScore, String expectedShortfallBps) { + BigDecimal longP = bd(longProb); + BigDecimal shortP = bd(shortProb); + BigDecimal neutral = BigDecimal.ONE.subtract(longP).subtract(shortP); + return new TraderModelOutput( + "model-output-1", + "run-1", + "cycle-1", + "trader-v4-btc-p0", + "cal-v4-btc-p0", + new DirectionOutput(longP, shortP, neutral, longP.max(shortP), longP.subtract(shortP).abs(), + bd("8.0"), 45, "direction-p0", "cal-v4-btc-p0", Map.of()), + new EntryOutput(bd(longEntryProb), bd(shortEntryProb), bd("0.70"), bd(expectedEdgeBps), + "p0-plan-atr-2r", "p0-price-plan-hash", bd("35"), bd("70"), 45, bd("4.0"), + "entry-p0", "cal-v4-btc-p0", Map.of()), + new ContinueOutput(bd(longContinueProb), bd(shortContinueProb), bd("0.60"), bd("5.0"), + bd(continueVsExitEdgeBps), "continue-p0", "cal-v4-btc-p0", Map.of()), + new ExitOutput(bd(longExitProb), bd(shortExitProb), bd("0.20"), bd("0.25"), bd("0.22"), + bd("0.20"), bd("10"), "exit-p0", "cal-v4-btc-p0", Map.of()), + new RiskOutput(bd(marketRiskProb), bd(positionRiskProb), bd("20"), bd("18"), bd("0.15"), + bd(expectedShortfallBps), bd("0.20"), bd("0.10"), bd("0.12"), + bd(liquidityCapacityRatio), "risk-p0", "cal-v4-btc-p0", Map.of()), + bd(uncertainty), + bd(oodScore), + Map.of("fixture", "p0")); + } + + public static PositionManagerInput pmInput(TraderModelOutput modelOutput, TraderPositionState positionState) { + return pmInput(modelOutput, positionState, account(), execution()); + } + + public static PositionManagerInput pmInput(TraderModelOutput modelOutput, TraderPositionState positionState, + TraderAccountState accountState, TraderExecutionState executionState) { + return new PositionManagerInput(cycle(), snapshot(), modelOutput, positionState, accountState, executionState, pmConfig()); + } + + public static TraderPositionState flatPosition() { + return new TraderPositionState("position-state-1", "run-1", "cycle-1", "BTC-USDT-PERP", + PositionSide.NONE, BigDecimal.ZERO, null, bd("100"), BigDecimal.ZERO, bd("1000"), + 0, BigDecimal.ONE, null); + } + + public static TraderPositionState longPosition(String unrealizedPnlBps) { + return position(PositionSide.LONG, unrealizedPnlBps, 0, "0.40"); + } + + public static TraderPositionState shortPosition(String unrealizedPnlBps) { + return position(PositionSide.SHORT, unrealizedPnlBps, 0, "0.40"); + } + + public static TraderPositionState position(PositionSide side, String unrealizedPnlBps, int addCount, String remainingAddCapacity) { + return new TraderPositionState("position-state-1", "run-1", "cycle-1", "BTC-USDT-PERP", + side, bd("0.30"), bd("100"), bd("101"), bd(unrealizedPnlBps), bd("1000"), + addCount, bd(remainingAddCapacity), null); + } + + public static TraderAccountState account() { + return account("0", "0", "1.0"); + } + + public static TraderAccountState account(String dailyDrawdownBps, String exposureRatio, String remainingSymbolCapacityRatio) { + return new TraderAccountState("account-state-1", "run-1", "cycle-1", + bd(dailyDrawdownBps), bd(exposureRatio), bd(remainingSymbolCapacityRatio), 0); + } + + public static TraderExecutionState execution() { + return execution(List.of(), 10, 0); + } + + public static TraderExecutionState executionWithOpenOrder() { + return execution(List.of(new OpenOrderState("order-1", "NEW")), 10, 0); + } + + public static TraderExecutionState execution(List openOrders, long latencyMs, int apiErrorCount) { + return new TraderExecutionState("execution-state-1", "run-1", "cycle-1", "BTC-USDT-PERP", + openOrders, bd("1.5"), latencyMs, apiErrorCount, bd("1"), bd("4"), bd("5"), + bd("0.1"), bd("0.001"), bd("0.001"), BigDecimal.ONE); + } + + public static TraderPositionManagerDecision pmDecision(TraderActionType action, PositionSide side) { + return new TraderPositionManagerDecision( + "pm-cycle-1", "run-1", "cycle-1", "model-output-1", "position-state-1", + "account-state-1", "execution-state-1", action, side, + action.increasesExposure() ? "p0-plan-atr-2r" : null, + action.increasesExposure() ? "p0-price-plan-hash" : null, + action == TraderActionType.OPEN_LONG || action == TraderActionType.OPEN_SHORT ? bd("0.20") : null, + action == TraderActionType.ADD_LONG || action == TraderActionType.ADD_SHORT ? bd("0.10") : null, + action.reducesExposure() ? bd("0.50") : null, + action.increasesExposure() ? bd("99.65") : null, + action.increasesExposure() ? bd("100.70") : null, + "fixture", + Map.of("fixture", "p0")); + } + + public static RiskLimits riskLimits() { + return new RiskLimits(bd("200"), BigDecimal.ONE, bd("500"), 3, 1500, false, false); } } diff --git a/src/test/java/com/quantai/trader/artifact/TraderArtifactLoaderTest.java b/src/test/java/com/quantai/trader/artifact/TraderArtifactLoaderTest.java new file mode 100644 index 0000000..4c579f1 --- /dev/null +++ b/src/test/java/com/quantai/trader/artifact/TraderArtifactLoaderTest.java @@ -0,0 +1,38 @@ +package com.quantai.trader.artifact; + +import com.quantai.trader.config.TraderProperties; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static com.quantai.trader.TestFixtures.properties; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TraderArtifactLoaderTest { + @Test + void deterministicP0BundleProvidesAllFiveModelFamilies() { + TraderArtifactBundle bundle = new TraderArtifactLoader(properties()).loadActiveBundle(); + + assertThat(bundle.providedModels()).containsExactlyInAnyOrder("DIRECTION", "ENTRY", "CONTINUE", "EXIT", "RISK"); + assertThat(bundle.pmConfig().pmConfigVersion()).isEqualTo("pm-v4-btc-p0"); + } + + @Test + void rejectsArtifactConfigWithBlankVersionInsteadOfReplacingIt() { + assertThatThrownBy(() -> new TraderProperties.Artifact(" ", "cal-v4-btc-p0", "pm-v4-btc-p0", "/tmp/trader")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("artifact.modelBundleVersion"); + } + + @Test + void bundleContractRejectsMissingModelFamily() { + TraderArtifactBundle bundle = new TraderArtifactLoader(properties()).loadActiveBundle(); + + assertThatThrownBy(() -> new TraderArtifactBundle( + bundle.modelBundleVersion(), bundle.calibrationBundleVersion(), bundle.pmConfigVersion(), + bundle.bundleHashSha256(), Set.of("DIRECTION", "ENTRY", "CONTINUE", "RISK"), bundle.pmConfig())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("all five"); + } +} diff --git a/src/test/java/com/quantai/trader/brain/TraderDecisionCycleRunnerTest.java b/src/test/java/com/quantai/trader/brain/TraderDecisionCycleRunnerTest.java deleted file mode 100644 index 9d751ca..0000000 --- a/src/test/java/com/quantai/trader/brain/TraderDecisionCycleRunnerTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.quantai.trader.brain; - -import com.quantai.trader.enums.TraderRunMode; -import com.quantai.trader.enums.TraderState; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.replay.ReplayClockTick; -import com.quantai.trader.state.TraderRuntimeState; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.math.BigDecimal; -import java.time.Instant; -import java.util.Map; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@SpringBootTest -class TraderDecisionCycleRunnerTest { - - @Autowired - private TraderDecisionCycleRunner runner; - - @Test - void exportsSampleForHappyPathReplayTick() { - String runId = "trader_run_runner_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); - ReplayClockTick tick = new ReplayClockTick( - runId, - "BTCUSDT", - Instant.parse("2026-06-23T12:00:00Z"), - Map.of("contextPass", true), - Map.of( - "setupPass", true, - "side", "LONG", - "entryPrice", new BigDecimal("65000"), - "invalidPrice", new BigDecimal("64800"), - "stopPrice", new BigDecimal("64920"), - "targetPrice", new BigDecimal("65350"), - "executionQualityScore", "0.90" - ), - Map.of("triggerScore", "0.95"), - Map.of("lastPrice", "65010"), - Map.of(), - Map.of() - ); - - TraderCycleResult result = runner.runReplayTick( - tick, - new TraderRuntimeState( - runId, - TraderRunMode.REPLAY, - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0" - ) - ); - - assertThat(result.cycle().state()).isEqualTo(TraderState.SAMPLE_EXPORTED); - assertThat(result.action()).isNotNull(); - assertThat(result.sample().proxyOnly()).isTrue(); - } - - @Test - void setupPassWithoutPricePlanFailsInsteadOfUsingFallbackPrices() { - ReplayClockTick tick = new ReplayClockTick( - "trader_run_runner", - "BTCUSDT", - Instant.parse("2026-06-23T12:00:00Z"), - Map.of("contextPass", true), - Map.of("setupPass", true, "side", "LONG"), - Map.of("triggerScore", "0.95"), - Map.of("lastPrice", "65010"), - Map.of(), - Map.of() - ); - - assertThatThrownBy(() -> runner.runReplayTick( - tick, - new TraderRuntimeState( - "trader_run_runner", - TraderRunMode.REPLAY, - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0" - ) - )) - .isInstanceOf(TraderException.class) - .hasMessageContaining("entryPrice"); - } -} diff --git a/src/test/java/com/quantai/trader/controller/TraderControllerTest.java b/src/test/java/com/quantai/trader/controller/TraderControllerTest.java index 83164b4..6e145a0 100644 --- a/src/test/java/com/quantai/trader/controller/TraderControllerTest.java +++ b/src/test/java/com/quantai/trader/controller/TraderControllerTest.java @@ -1,135 +1,86 @@ package com.quantai.trader.controller; +import com.quantai.trader.domain.FeedbackValidator; +import com.quantai.trader.domain.TraderAppFeedback; +import com.quantai.trader.domain.TraderException; +import com.quantai.trader.enums.FeedbackSource; +import com.quantai.trader.enums.TraderErrorCode; +import com.quantai.trader.enums.TraderExecutionMode; +import com.quantai.trader.enums.TraderRunMode; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; +import org.springframework.http.ResponseEntity; -import java.nio.file.Path; +import java.time.Instant; +import java.util.Map; -import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static com.quantai.trader.TestFixtures.bd; +import static com.quantai.trader.TestFixtures.properties; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -@SpringBootTest -@AutoConfigureMockMvc class TraderControllerTest { - - @Autowired - private MockMvc mockMvc; - @Test - void listsLoadedPlaybook() throws Exception { - mockMvc.perform(get("/api/trader/playbooks")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].playbookId").value("BREAKOUT_RETEST_CONTINUATION")) - .andExpect(jsonPath("$[0].outputActions", hasItem("OPEN_INITIAL"))); + void feedbackEndpointRejectsWhenHttpFeedbackIsDisabled() { + TraderFeedbackController controller = new TraderFeedbackController(properties(), new FeedbackValidator()); + + assertThatThrownBy(() -> controller.feedback(feedback(FeedbackSource.SHADOW_APP, false))) + .isInstanceOf(TraderException.class) + .hasMessageContaining("HTTP feedback is disabled"); } @Test - void feedbackIsDisabledByDefault() throws Exception { - mockMvc.perform(post("/api/trader/feedback") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "runId": "run", - "cycleId": "cycle", - "actionId": "action", - "feedbackType": "FILL_EVENT", - "feedbackSource": "PAPER_APP", - "realFill": true, - "rawFeedback": {} - } - """)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.code").value("TRADER_FEEDBACK_DISABLED")); + void feedbackEndpointRejectsPaperAppSourceInP0EvenWhenHttpIsEnabledForTest() { + TraderFeedbackController controller = new TraderFeedbackController( + properties(TraderRunMode.SHADOW, TraderExecutionMode.SHADOW, false, true), new FeedbackValidator()); + + assertThatThrownBy(() -> controller.feedback(feedback(FeedbackSource.PAPER_APP, true))) + .isInstanceOf(TraderException.class) + .hasMessageContaining("P0 rejects"); } @Test - void createsReplayRunAsynchronously() throws Exception { - Path fixture = Path.of(new ClassPathResource("replay-fixtures/trend-up-breakout-happy.jsonl").getFile().toURI()); + void feedbackEndpointAcceptsShadowRecorderFeedbackWhenExplicitlyEnabled() { + TraderFeedbackController controller = new TraderFeedbackController( + properties(TraderRunMode.SHADOW, TraderExecutionMode.SHADOW, false, true), new FeedbackValidator()); - MvcResult result = mockMvc.perform(post("/api/trader/replay/runs") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "symbol": "BTCUSDT", - "playbookId": "BREAKOUT_RETEST_CONTINUATION", - "playbookVersion": "2026-06-22.p0", - "from": "2026-01-01T00:00:00Z", - "to": "2026-01-02T00:00:00Z", - "featureVersion": "trader_feature_v0", - "labelVersion": "trader_label_v0", - "dataSources": { - "ticks": { - "sourceId": "btc_ticks_test", - "path": "%s", - "hashSha256": "abc", - "timezone": "UTC", - "missingSummary": {} - } - } - } - """.formatted(fixture.toString()))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.status").value("CREATED")) - .andExpect(jsonPath("$.runId").exists()) - .andReturn(); + Map result = controller.feedback(feedback(FeedbackSource.SHADOW_APP, false)); - String runId = result.getResponse().getContentAsString() - .replaceAll(".*\\\"runId\\\":\\\"([^\\\"]+)\\\".*", "$1"); - waitForCompletedRun(runId); - mockMvc.perform(get("/api/trader/replay/runs/{runId}/report", runId)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.candidateEvents").value(1)) - .andExpect(jsonPath("$.strictVsLoose.replayEngine").value("jsonl_fixture")); + assertThat(result).containsEntry("accepted", true).containsEntry("feedbackId", "feedback-1"); } @Test - void rejectsReplayRunWithoutReadableFixture() throws Exception { - mockMvc.perform(post("/api/trader/replay/runs") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "symbol": "BTCUSDT", - "playbookId": "BREAKOUT_RETEST_CONTINUATION", - "playbookVersion": "2026-06-22.p0", - "from": "2026-01-01T00:00:00Z", - "to": "2026-01-02T00:00:00Z", - "featureVersion": "trader_feature_v0", - "labelVersion": "trader_label_v0", - "dataSources": { - "ticks": { - "sourceId": "btc_ticks_missing", - "path": "/tmp/not-a-real-trader-fixture.jsonl", - "hashSha256": "abc", - "timezone": "UTC", - "missingSummary": {} - } - } - } - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("TRADER_DATA_SOURCE_MISSING")); + void healthEndpointReportsP0RuntimeIdentity() { + Map health = new TraderHealthController(properties()).health(); + + assertThat(health).containsEntry("status", "UP") + .containsEntry("runMode", TraderRunMode.SHADOW) + .containsEntry("executionMode", TraderExecutionMode.SHADOW) + .containsEntry("tradingEnabled", false) + .containsEntry("modelBundleVersion", "trader-v4-btc-p0"); } - private void waitForCompletedRun(String runId) throws Exception { - for (int i = 0; i < 20; i++) { - MvcResult statusResult = mockMvc.perform(get("/api/trader/replay/runs/{runId}", runId)) - .andExpect(status().isOk()) - .andReturn(); - if (statusResult.getResponse().getContentAsString().contains("\"status\":\"COMPLETED\"")) { - return; - } - Thread.sleep(50); - } - mockMvc.perform(get("/api/trader/replay/runs/{runId}", runId)) - .andExpect(jsonPath("$.status").value("COMPLETED")); + @Test + void exceptionHandlerKeepsTraderErrorCodeVisible() { + TraderApiExceptionHandler handler = new TraderApiExceptionHandler(); + + ResponseEntity response = handler.traderException( + new TraderException(TraderErrorCode.TRADER_P0_MODE_BLOCKED, "P0 blocked")); + + assertThat(response.getStatusCode().is4xxClientError()).isTrue(); + assertThat(response.getBody()).isEqualTo(new TraderApiError(TraderErrorCode.TRADER_P0_MODE_BLOCKED, "P0 blocked")); + } + + private TraderAppFeedback feedback(FeedbackSource source, boolean realFill) { + return new TraderAppFeedback( + "feedback-1", "run-1", "cycle-1", "action-1", source, realFill, + realFill ? "order-1" : null, realFill ? "FILLED" : "RECORDED", + Instant.parse("2026-06-26T00:00:00Z"), + null, realFill ? Instant.parse("2026-06-26T00:00:01Z") : null, + realFill ? bd("100") : null, + realFill ? bd("0.01") : null, + realFill ? bd("0.001") : null, + realFill ? bd("1.0") : null, + null, + Map.of()); } } diff --git a/src/test/java/com/quantai/trader/core/TraderCoreDtoTest.java b/src/test/java/com/quantai/trader/core/TraderCoreDtoTest.java new file mode 100644 index 0000000..3b12b37 --- /dev/null +++ b/src/test/java/com/quantai/trader/core/TraderCoreDtoTest.java @@ -0,0 +1,48 @@ +package com.quantai.trader.core; + +import com.quantai.trader.domain.TraderActionFactory; +import com.quantai.trader.domain.TraderRiskDecision; +import com.quantai.trader.enums.FeedbackSource; +import com.quantai.trader.enums.PositionSide; +import com.quantai.trader.enums.TraderActionType; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Map; + +import static com.quantai.trader.TestFixtures.*; +import static org.assertj.core.api.Assertions.assertThat; + +class TraderCoreDtoTest { + @Test + void coreDecisionRequestAndResponseCarryStrictP0DecisionContext() { + TraderCoreDecisionRequest request = new TraderCoreDecisionRequest( + "request-1", "run-1", "cycle-1", "BTC-USDT-PERP", T0, + snapshot(), flatPosition(), account(), execution(), + "trader-v4-btc-p0", "cal-v4-btc-p0", "pm-v4-btc-p0", Map.of("mode", "SHADOW")); + TraderRiskDecision riskDecision = new TraderRiskDecision("risk-1", "run-1", "cycle-1", + "pm-cycle-1", true, TraderActionType.OPEN_LONG, TraderActionType.OPEN_LONG, null, Map.of()); + var action = new TraderActionFactory().create(pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), riskDecision, "BTC-USDT-PERP"); + TraderCoreDecisionResponse response = new TraderCoreDecisionResponse( + "response-1", request.requestId(), request.cycleId(), action.modelOutputId(), + action.pmDecisionId(), action.riskDecisionId(), true, action, null, Map.of("source", "core")); + + assertThat(response.actionAllowed()).isTrue(); + assertThat(response.action().actionType()).isEqualTo(TraderActionType.OPEN_LONG); + assertThat(request.requestContextJson()).containsEntry("mode", "SHADOW"); + } + + @Test + void coreFeedbackAndHealthDtosStayModeExplicit() { + TraderCoreFeedbackEvent feedback = new TraderCoreFeedbackEvent( + "feedback-1", "action-1", FeedbackSource.REPLAY_SIMULATOR, false, null, "RECORDED", + null, null, null, null, null, T0, Map.of("source", "replay")); + TraderCoreHealth health = new TraderCoreHealth(true, "SHADOW", "trader-v4-btc-p0", + "cal-v4-btc-p0", "pm-v4-btc-p0", null); + + assertThat(feedback.feedbackSource()).isEqualTo(FeedbackSource.REPLAY_SIMULATOR); + assertThat(feedback.realFill()).isFalse(); + assertThat(health.ready()).isTrue(); + assertThat(health.blocker()).isNull(); + } +} diff --git a/src/test/java/com/quantai/trader/domain/ModelOutputContractTest.java b/src/test/java/com/quantai/trader/domain/ModelOutputContractTest.java new file mode 100644 index 0000000..092bfff --- /dev/null +++ b/src/test/java/com/quantai/trader/domain/ModelOutputContractTest.java @@ -0,0 +1,69 @@ +package com.quantai.trader.domain; + +import com.quantai.trader.enums.FeedbackSource; +import com.quantai.trader.enums.PositionSide; +import com.quantai.trader.enums.TraderActionType; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Map; + +import static com.quantai.trader.TestFixtures.bd; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ModelOutputContractTest { + @Test + void rejectsProbabilityOutsideClosedUnitRange() { + assertThatThrownBy(() -> new DirectionOutput( + bd("1.01"), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.ONE, + bd("1"), 45, "direction-p0", "cal-v4-btc-p0", Map.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("direction.longProb"); + } + + @Test + void requiresPricePlanIdentityForExposureIncreasingDecision() { + assertThatThrownBy(() -> new TraderPositionManagerDecision( + "pm-1", "run-1", "cycle-1", "model-output-1", "position-state-1", + "account-state-1", "execution-state-1", TraderActionType.OPEN_LONG, PositionSide.LONG, + "p0-plan-atr-2r", " ", bd("0.20"), null, null, bd("99.65"), bd("100.70"), + "OPEN_LONG_PM_PASS", Map.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("pricePlanConfigHash"); + } + + @Test + void rejectsImpossibleFillSourceCombinations() { + assertThatThrownBy(() -> feedback(FeedbackSource.SHADOW_APP, true)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("realFill requires"); + + assertThatThrownBy(() -> feedback(FeedbackSource.PAPER_APP, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be realFill"); + } + + @Test + void p0FeedbackValidatorRejectsPaperAndRealFillSources() { + TraderAppFeedback paperFill = feedback(FeedbackSource.PAPER_APP, true); + + assertThatThrownBy(() -> new FeedbackValidator().validateP0(paperFill)) + .isInstanceOf(TraderException.class) + .hasMessageContaining("P0 rejects"); + } + + private TraderAppFeedback feedback(FeedbackSource source, boolean realFill) { + return new TraderAppFeedback( + "feedback-1", "run-1", "cycle-1", "action-1", source, realFill, + realFill ? "order-1" : null, realFill ? "FILLED" : "RECORDED", + Instant.parse("2026-06-26T00:00:00Z"), + null, realFill ? Instant.parse("2026-06-26T00:00:01Z") : null, + realFill ? bd("100") : null, + realFill ? bd("0.01") : null, + realFill ? bd("0.001") : null, + realFill ? bd("1.0") : null, + null, + Map.of()); + } +} diff --git a/src/test/java/com/quantai/trader/domain/TraderAppFeedbackTest.java b/src/test/java/com/quantai/trader/domain/TraderAppFeedbackTest.java deleted file mode 100644 index ccf49a6..0000000 --- a/src/test/java/com/quantai/trader/domain/TraderAppFeedbackTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.quantai.trader.domain; - -import com.quantai.trader.enums.TraderFeedbackSource; -import com.quantai.trader.enums.TraderFeedbackType; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class TraderAppFeedbackTest { - - @Test - void rejectsProxySourceMarkedAsRealFill() { - assertThatThrownBy(() -> feedback(TraderFeedbackSource.MARKET_PROXY, true)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("inconsistent"); - } - - @Test - void acceptsPaperSourceMarkedAsRealFill() { - assertThatCode(() -> feedback(TraderFeedbackSource.PAPER_APP, true)).doesNotThrowAnyException(); - } - - private TraderAppFeedback feedback(TraderFeedbackSource source, boolean realFill) { - return new TraderAppFeedback( - "run", - "cycle", - "action", - TraderFeedbackType.FILL_EVENT, - source, - null, - null, - realFill, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - Map.of() - ); - } -} diff --git a/src/test/java/com/quantai/trader/domain/TraderDataSourceManifestTest.java b/src/test/java/com/quantai/trader/domain/TraderDataSourceManifestTest.java deleted file mode 100644 index f503a84..0000000 --- a/src/test/java/com/quantai/trader/domain/TraderDataSourceManifestTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.quantai.trader.domain; - -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class TraderDataSourceManifestTest { - - @Test - void requiresFullHashOrSchemaRowTimeLineage() { - assertThatThrownBy(() -> manifest(null, null, null, null, null)) - .isInstanceOf(TraderException.class) - .hasMessageContaining("content hash"); - } - - @Test - void acceptsFullHash() { - assertThatCode(() -> manifest("abc", null, null, null, null)).doesNotThrowAnyException(); - } - - @Test - void acceptsSchemaLineageForLargeFiles() { - assertThatCode(() -> manifest(null, "schema", 100L, Instant.parse("2026-01-01T00:00:00Z"), Instant.parse("2026-01-02T00:00:00Z"))) - .doesNotThrowAnyException(); - } - - private TraderDataSourceManifest manifest(String hash, String schema, Long rows, Instant min, Instant max) { - return new TraderDataSourceManifest( - "source-1", - "BTCUSDT", - "candles", - "BINANCE", - "1m", - "/tmp/candles.parquet", - hash, - schema, - Instant.parse("2026-01-01T00:00:00Z"), - Instant.parse("2026-01-02T00:00:00Z"), - min, - max, - "UTC", - rows, - Map.of(), - "P0_ACCEPTED" - ); - } -} diff --git a/src/test/java/com/quantai/trader/evidence/EvidenceAppenderTest.java b/src/test/java/com/quantai/trader/evidence/EvidenceAppenderTest.java index 4666600..8679396 100644 --- a/src/test/java/com/quantai/trader/evidence/EvidenceAppenderTest.java +++ b/src/test/java/com/quantai/trader/evidence/EvidenceAppenderTest.java @@ -1,50 +1,23 @@ package com.quantai.trader.evidence; -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.StageDecision; import com.quantai.trader.domain.TraderEvidence; -import com.quantai.trader.persistence.TraderEvidenceRepository; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class EvidenceAppenderTest { - @Test - void appendsBlockingEvidenceToRepository() { - CapturingEvidenceRepository repository = new CapturingEvidenceRepository(); - EvidenceAppender appender = new EvidenceAppender(repository); + void appendsEvidenceWithStageReasonAndDetails() { + EvidenceAppender appender = new EvidenceAppender(); - appender.append( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.CONTEXT_CHECK), - "CONTEXT_GATE", - StageDecision.block("DATA_MISSING", "TRADER_DATA_QUALITY_FAILED") - ); + TraderEvidence item = appender.append("run-1", "cycle-1", "PM_DECISION", true, + "OPEN_LONG_PM_PASS", null, Map.of("action", "OPEN_LONG")); - assertThat(repository.findByCycleId("trader_run_test", "trader_cycle_test")) - .singleElement() - .satisfies(evidence -> { - assertThat(evidence.pass()).isFalse(); - assertThat(evidence.blocker()).isEqualTo("TRADER_DATA_QUALITY_FAILED"); - }); - } - - private static class CapturingEvidenceRepository implements TraderEvidenceRepository { - private final List evidence = new ArrayList<>(); - - @Override - public void insert(TraderEvidence evidence) { - this.evidence.add(evidence); - } - - @Override - public List findByCycleId(String runId, String cycleId) { - return evidence.stream() - .filter(item -> item.runId().equals(runId) && item.cycleId().equals(cycleId)) - .toList(); - } + assertThat(item.evidenceId()).isEqualTo("evidence_cycle-1_0"); + assertThat(item.pass()).isTrue(); + assertThat(item.detailsJson()).containsEntry("action", "OPEN_LONG"); + assertThat(appender.all()).containsExactly(item); } } diff --git a/src/test/java/com/quantai/trader/execution/TraderEntryPlannerTest.java b/src/test/java/com/quantai/trader/execution/TraderEntryPlannerTest.java deleted file mode 100644 index 87c5361..0000000 --- a/src/test/java/com/quantai/trader/execution/TraderEntryPlannerTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.quantai.trader.execution; - -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.PlaybookCandidate; -import com.quantai.trader.domain.TraderEntryPlan; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.domain.TraderPricePlan; -import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.enums.TraderSide; -import com.quantai.trader.enums.TraderState; -import com.quantai.trader.risk.TraderPositionSizer; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class TraderEntryPlannerTest { - - private final TraderEntryPlanner planner = new TraderEntryPlanner(new TraderPositionSizer(TestFixtures.properties())); - - @Test - void createsCompleteInitialEntryPlan() { - TraderEntryPlan plan = planner.planInitialEntry( - TestFixtures.cycle(TraderState.ENTRY_PLANNED), - TestFixtures.candidate(), - TestFixtures.strongTrigger() - ); - - assertThat(plan.entryAction()).isEqualTo(TraderActionType.OPEN_INITIAL); - assertThat(plan.completeForEntry()).isTrue(); - assertThat(plan.plannedLegIndex()).isZero(); - } - - @Test - void rejectsCandidateWithoutStopTargetInvalidOrMaxHold() { - TraderPricePlan incomplete = new TraderPricePlan(new BigDecimal("1"), null, null, null, null, 0, 0); - PlaybookCandidate candidate = new PlaybookCandidate( - "run", - "cycle", - "candidate", - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - TraderSide.LONG, - "INTRADAY_5M_60M", - TestFixtures.NOW, - incomplete, - 3, - Map.of() - ); - - assertThatThrownBy(() -> planner.planInitialEntry(TestFixtures.cycle(TraderState.ENTRY_PLANNED), candidate, TestFixtures.strongTrigger())) - .isInstanceOf(TraderException.class) - .hasMessageContaining("entry/invalid/stop/target"); - } - - @Test - void skipsPlannedLegAfterReduce() { - assertThat(planner.planNextDeclaredLeg( - TestFixtures.cycle(TraderState.PLANNED_LEG_WAIT), - TestFixtures.candidate(), - TestFixtures.openedPath(true) - )).isEmpty(); - } -} diff --git a/src/test/java/com/quantai/trader/outbox/InMemoryOutboxRepositoryTest.java b/src/test/java/com/quantai/trader/outbox/InMemoryOutboxRepositoryTest.java new file mode 100644 index 0000000..326ad30 --- /dev/null +++ b/src/test/java/com/quantai/trader/outbox/InMemoryOutboxRepositoryTest.java @@ -0,0 +1,40 @@ +package com.quantai.trader.outbox; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class InMemoryOutboxRepositoryTest { + @Test + void rejectsDuplicateDestinationAndIdempotencyKey() { + InMemoryOutboxRepository repository = new InMemoryOutboxRepository(); + TraderOutboxEvent event = event("outbox-1", "SHADOW_RECORDER", "idem-1"); + + repository.insert(event); + + assertThatThrownBy(() -> repository.insert(event("outbox-2", "SHADOW_RECORDER", "idem-1"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("duplicate outbox idempotency key"); + assertThat(repository.all()).containsExactly(event); + } + + @Test + void allowsSameIdempotencyKeyForDifferentDestination() { + InMemoryOutboxRepository repository = new InMemoryOutboxRepository(); + + repository.insert(event("outbox-1", "REPLAY_SIM_RECORDER", "idem-1")); + repository.insert(event("outbox-2", "SHADOW_RECORDER", "idem-1")); + + assertThat(repository.all()).hasSize(2); + } + + private TraderOutboxEvent event(String id, String destination, String idempotencyKey) { + return new TraderOutboxEvent(id, "run-1", "cycle-1", "TRADER_ACTION", "action-1", + "ACTION_CREATED", destination, Map.of("actionType", "OPEN_LONG"), idempotencyKey, + "PENDING", Instant.parse("2026-06-26T00:00:00Z")); + } +} diff --git a/src/test/java/com/quantai/trader/persistence/MybatisReplayPersistenceTest.java b/src/test/java/com/quantai/trader/persistence/MybatisReplayPersistenceTest.java deleted file mode 100644 index ee0988d..0000000 --- a/src/test/java/com/quantai/trader/persistence/MybatisReplayPersistenceTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.quantai.trader.persistence; - -import com.quantai.trader.enums.ReplayRunStatus; -import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot; -import com.quantai.trader.replay.DataSourceSpec; -import com.quantai.trader.replay.ReplayRun; -import com.quantai.trader.replay.ReplayRunConfig; -import com.quantai.trader.replay.ReplayRunResponse; -import com.quantai.trader.replay.ReplayRunService; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.io.ClassPathResource; - -import java.nio.file.Path; -import java.time.Instant; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -class MybatisReplayPersistenceTest { - - @Autowired - private ReplayRunService replayRunService; - - @Autowired - private ReplayReportRepository reportRepository; - - @Autowired - private TraderSampleRepository sampleRepository; - - @Autowired - private TraderEvidenceRepository evidenceRepository; - - @Autowired - private TraderRiskDecisionRepository riskDecisionRepository; - - @Autowired - private TraderPlaybookDefinitionRepository playbookDefinitionRepository; - - @Test - void persistsReplayArtifactsThroughMybatisPlusRepositories() throws Exception { - ReplayRunResponse response = replayRunService.createRun(configFor("trend-up-breakout-happy.jsonl")); - ReplayRun run = waitForTerminalRun(response.runId()); - - assertThat(run.status()).isEqualTo(ReplayRunStatus.COMPLETED); - assertThat(reportRepository.findByRunId(run.runId())).isPresent() - .get() - .extracting(report -> report.strictVsLoose().get("replayEngine")) - .isEqualTo("jsonl_fixture"); - - var samples = sampleRepository.findByRunId(run.runId()); - assertThat(samples).hasSize(1); - String cycleId = samples.getFirst().cycleId(); - assertThat(evidenceRepository.findByCycleId(run.runId(), cycleId)).isNotEmpty(); - assertThat(riskDecisionRepository.findByCycleId(run.runId(), cycleId)).isNotEmpty(); - - TraderPlaybookDefinitionSnapshot playbook = playbookDefinitionRepository - .findPlaybookDefinition("BREAKOUT_RETEST_CONTINUATION", "2026-06-22.p0") - .orElseThrow(); - assertThat(playbook.definitionHashSha256()).isNotBlank(); - } - - private ReplayRun waitForTerminalRun(String runId) throws InterruptedException { - for (int i = 0; i < 200; i++) { - ReplayRun run = replayRunService.find(runId).orElseThrow(); - if (run.status() == ReplayRunStatus.COMPLETED - || run.status() == ReplayRunStatus.FAILED - || run.status() == ReplayRunStatus.CANCELLED) { - return run; - } - Thread.sleep(50); - } - return replayRunService.find(runId).orElseThrow(); - } - - private ReplayRunConfig configFor(String fixtureName) throws Exception { - Path path = Path.of(new ClassPathResource("replay-fixtures/" + fixtureName).getFile().toURI()); - return new ReplayRunConfig( - null, - "BTCUSDT", - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - Instant.parse("2026-01-01T00:00:00Z"), - Instant.parse("2026-01-02T00:00:00Z"), - "trader_feature_v0", - "trader_label_v0", - Map.of("ticks", new DataSourceSpec( - fixtureName.replace(".jsonl", ""), - path.toString(), - "fixture-hash-not-used-in-p0", - null, - 1L, - null, - null, - "UTC", - Map.of() - )) - ); - } -} diff --git a/src/test/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepositoryTest.java b/src/test/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepositoryTest.java deleted file mode 100644 index 67e1752..0000000 --- a/src/test/java/com/quantai/trader/persistence/TraderPlaybookDefinitionRepositoryTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.quantai.trader.persistence; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.playbook.RuleDefinition; -import com.quantai.trader.playbook.TraderPlaybookDefinition; -import com.quantai.trader.playbook.TraderPlaybookDefinitionSnapshot; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@SpringBootTest -class TraderPlaybookDefinitionRepositoryTest { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().findAndRegisterModules(); - private static final String HASH_A = "a".repeat(64); - private static final String HASH_B = "b".repeat(64); - - @Autowired - private TraderPlaybookDefinitionRepository repository; - - @Test - void rejectsSameVersionWithDifferentHash() throws Exception { - String playbookId = nextPlaybookId(); - String version = "2026-06-23.test-conflict"; - repository.insertPlaybookDefinitionIfAbsent(snapshot(playbookId, version, HASH_A)); - - assertThatThrownBy(() -> repository.insertPlaybookDefinitionIfAbsent(snapshot(playbookId, version, HASH_B))) - .isInstanceOf(TraderException.class) - .hasMessageContaining("another definition hash"); - } - - @Test - void allowsSameVersionWithSameHash() throws Exception { - String playbookId = nextPlaybookId(); - String version = "2026-06-23.test-same"; - repository.insertPlaybookDefinitionIfAbsent(snapshot(playbookId, version, HASH_A)); - repository.insertPlaybookDefinitionIfAbsent(snapshot(playbookId, version, HASH_A)); - - assertThat(repository.findPlaybookDefinition(playbookId, version)).isPresent(); - } - - private TraderPlaybookDefinitionSnapshot snapshot(String playbookId, String version, String hash) throws Exception { - TraderPlaybookDefinition definition = definition(playbookId, version); - return new TraderPlaybookDefinitionSnapshot( - playbookId, - version, - "TREND_CONTINUATION", - "INTRADAY_5M_60M", - "test.yml", - hash, - OBJECT_MAPPER.writeValueAsString(definition), - Instant.now(), - "ACTIVE", - definition - ); - } - - private TraderPlaybookDefinition definition(String playbookId, String version) { - RuleDefinition rule = new RuleDefinition("test_rule", "test rule", 3, "FIXED", true, true, "test"); - return new TraderPlaybookDefinition( - playbookId, - version, - "TREND_CONTINUATION", - "INTRADAY_5M_60M", - "BOTH", - List.of("1h"), - List.of("15m"), - List.of("1m"), - "30s-300s", - List.of("15m"), - rule, - rule, - rule, - rule, - rule, - rule, - rule, - rule, - Map.of("max_leverage", "10x_screen_only"), - List.of("candles", "trades", "level_1"), - List.of("WAIT", "OPEN_INITIAL", "OPEN_PLANNED_LEG", "HOLD", "REDUCE", "MOVE_STOP", "CLOSE", "CANCEL", "REQUOTE") - ); - } - - private String nextPlaybookId() { - return "TEST_PLAYBOOK_" + UUID.randomUUID().toString().substring(0, 8); - } -} diff --git a/src/test/java/com/quantai/trader/playbook/TraderPlaybookValidatorTest.java b/src/test/java/com/quantai/trader/playbook/TraderPlaybookValidatorTest.java deleted file mode 100644 index 6199907..0000000 --- a/src/test/java/com/quantai/trader/playbook/TraderPlaybookValidatorTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.quantai.trader.playbook; - -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class TraderPlaybookValidatorTest { - - private final TraderPlaybookValidator validator = new TraderPlaybookValidator(); - - @Test - void rejectsMissingRequiredRule() { - TraderPlaybookDefinition definition = validDefinition(null, List.of("WAIT")); - - assertThatThrownBy(() -> validator.validate(definition)) - .isInstanceOf(TraderPlaybookValidationException.class) - .hasMessageContaining("entry_rule"); - } - - @Test - void rejectsScaleInAction() { - TraderPlaybookDefinition definition = validDefinition(rule("entry"), List.of("WAIT", "SCALE_IN")); - - assertThatThrownBy(() -> validator.validate(definition)) - .isInstanceOf(TraderPlaybookValidationException.class) - .hasMessageContaining("SCALE_IN"); - } - - @Test - void rejectsFixedLegRatioTemplate() { - RuleDefinition fixed = new RuleDefinition("legs", "fixed", 3, "FIXED", true, true, null); - TraderPlaybookDefinition definition = new TraderPlaybookDefinition( - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - "TREND_CONTINUATION", - "INTRADAY_5M_60M", - "BOTH", - List.of("1h"), - List.of("15m"), - List.of("5m"), - "30s-300s", - List.of("60m"), - rule("entry"), - fixed, - rule("invalid"), - rule("stop"), - rule("target"), - null, - rule("max_hold"), - null, - Map.of(), - List.of("candles", "trades", "level_1"), - List.of("WAIT", "OPEN_PLANNED_LEG") - ); - - assertThatThrownBy(() -> validator.validate(definition)) - .isInstanceOf(TraderPlaybookValidationException.class) - .hasMessageContaining("fixed"); - } - - private TraderPlaybookDefinition validDefinition(RuleDefinition entryRule, List outputActions) { - return new TraderPlaybookDefinition( - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - "TREND_CONTINUATION", - "INTRADAY_5M_60M", - "BOTH", - List.of("1h"), - List.of("15m"), - List.of("5m"), - "30s-300s", - List.of("60m"), - entryRule, - new RuleDefinition("legs", "dynamic", 3, "SIGNAL_EXECUTION_RISK_DYNAMIC", true, false, null), - rule("invalid"), - rule("stop"), - rule("target"), - null, - rule("max_hold"), - null, - Map.of(), - List.of("candles", "trades", "level_1"), - outputActions - ); - } - - private RuleDefinition rule(String name) { - return new RuleDefinition(name, name, null, null, null, null, null); - } -} diff --git a/src/test/java/com/quantai/trader/position/TraderPositionManagerTest.java b/src/test/java/com/quantai/trader/position/TraderPositionManagerTest.java index 6974aa1..9e9e58c 100644 --- a/src/test/java/com/quantai/trader/position/TraderPositionManagerTest.java +++ b/src/test/java/com/quantai/trader/position/TraderPositionManagerTest.java @@ -1,63 +1,86 @@ package com.quantai.trader.position; -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.TraderAction; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.domain.TraderMarketSnapshot; +import com.quantai.trader.domain.TraderModelOutput; +import com.quantai.trader.domain.TraderPositionManagerDecision; import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.enums.TraderSide; import org.junit.jupiter.api.Test; import java.math.BigDecimal; -import java.util.Map; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static com.quantai.trader.TestFixtures.*; +import static org.assertj.core.api.Assertions.assertThat; class TraderPositionManagerTest { + private final TraderPositionManager positionManager = new TraderPositionManager(); @Test - void plannedLegWithoutExistingPathFailsInsteadOfNoOp() { - TraderPositionManager manager = new TraderPositionManager(); - TraderAction action = new TraderAction( - "trader_run_test", - "trader_cycle_test", - "trader_action_test_2", - TraderActionType.OPEN_PLANNED_LEG, - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - "BTCUSDT", - TraderSide.LONG, - new BigDecimal("65000"), - null, - TestFixtures.NOW, - "TEST_PLANNED_LEG", - Map.of("positionId", "missing_position", "plannedLegRatio", new BigDecimal("0.20")), - "SHADOW_CREATED" - ); + void opensLongWithDynamicRatioAndSideAwareBpsPrices() { + TraderPositionManagerDecision decision = positionManager.decide(pmInput(modelOutput(), flatPosition())); - assertThatThrownBy(() -> manager.simulateOrUpdate( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.PLANNED_LEG_WAIT), - action, - snapshot() - )) - .isInstanceOf(TraderException.class) - .hasMessageContaining("not found"); + assertThat(decision.candidateAction()).isEqualTo(TraderActionType.OPEN_LONG); + assertThat(decision.targetPositionRatio()).isGreaterThan(bd("0.05")).isLessThanOrEqualTo(bd("0.20")); + assertThat(decision.stopPrice()).isEqualByComparingTo("99.65"); + assertThat(decision.targetPrice()).isEqualByComparingTo("100.70"); + assertThat(decision.pricePlanConfigHash()).isEqualTo("p0-price-plan-hash"); } - private TraderMarketSnapshot snapshot() { - return new TraderMarketSnapshot( - "trader_run_test", - "trader_cycle_test", - "trader_snapshot_test", - "BTCUSDT", - TestFixtures.NOW, - "trader_feature_v0", - Map.of(), - Map.of(), - Map.of(), - Map.of("lastPrice", "65010"), - Map.of(), - Map.of() - ); + @Test + void opensShortWithInvertedStopAndTargetBpsPrices() { + TraderPositionManagerDecision decision = positionManager.decide(pmInput(shortModelOutput(), flatPosition())); + + assertThat(decision.candidateAction()).isEqualTo(TraderActionType.OPEN_SHORT); + assertThat(decision.stopPrice()).isEqualByComparingTo("100.35"); + assertThat(decision.targetPrice()).isEqualByComparingTo("99.30"); + } + + @Test + void waitsWhenDataOrLiquidityIsInsufficient() { + TraderModelOutput noLiquidity = modelOutput("0.70", "0.20", "0.70", "0.30", "0.66", "0.34", + "0.20", "0.50", "0.18", "0.12", "0", "12", "4", "0.10", "0.05", "20"); + + assertThat(positionManager.decide(pmInput(noLiquidity, flatPosition())).candidateAction()) + .isEqualTo(TraderActionType.WAIT); + assertThat(positionManager.decide(new com.quantai.trader.domain.PositionManagerInput( + cycle(), snapshot(false, "1000"), modelOutput(), flatPosition(), account(), execution(), pmConfig())).candidateAction()) + .isEqualTo(TraderActionType.WAIT); + } + + @Test + void addsOnlyWhenExistingPositionIsProfitable() { + TraderPositionManagerDecision add = positionManager.decide(pmInput(modelOutput(), longPosition("10"))); + TraderPositionManagerDecision hold = positionManager.decide(pmInput(modelOutput(), longPosition("-1"))); + + assertThat(add.candidateAction()).isEqualTo(TraderActionType.ADD_LONG); + assertThat(add.addRatio()).isEqualByComparingTo("0.032"); + assertThat(hold.candidateAction()).isEqualTo(TraderActionType.HOLD); + } + + @Test + void usesPositionSideContinuationProbabilityForShortAdd() { + TraderPositionManagerDecision decision = positionManager.decide(pmInput(shortModelOutput(), shortPosition("10"))); + + assertThat(decision.candidateAction()).isEqualTo(TraderActionType.ADD_SHORT); + assertThat(decision.addRatio()).isEqualByComparingTo("0.032"); + } + + @Test + void closesExistingPositionWhenExitOrRiskSignalIsHigh() { + TraderModelOutput exitHigh = modelOutput("0.70", "0.20", "0.70", "0.30", "0.66", "0.34", + "0.80", "0.50", "0.18", "0.12", "1.0", "12", "4", "0.10", "0.05", "20"); + + TraderPositionManagerDecision decision = positionManager.decide(pmInput(exitHigh, longPosition("10"))); + + assertThat(decision.candidateAction()).isEqualTo(TraderActionType.CLOSE_LONG); + assertThat(decision.reason()).isEqualTo("EXIT_OR_RISK_HIGH"); + } + + @Test + void returnsZeroInitialRatioWhenExpectedEdgeIsBelowSizingFloor() { + TraderModelOutput weakEdge = modelOutput("0.70", "0.20", "0.70", "0.30", "0.66", "0.34", + "0.20", "0.50", "0.18", "0.12", "1.0", "0.5", "4", "0.10", "0.05", "20"); + + BigDecimal ratio = positionManager.calculateInitialRatio(pmInput(weakEdge, flatPosition()), com.quantai.trader.enums.PositionSide.LONG); + + assertThat(ratio).isEqualByComparingTo(BigDecimal.ZERO); } } diff --git a/src/test/java/com/quantai/trader/quality/NoLegacyTraderTermsTest.java b/src/test/java/com/quantai/trader/quality/NoLegacyTraderTermsTest.java new file mode 100644 index 0000000..26ff8b0 --- /dev/null +++ b/src/test/java/com/quantai/trader/quality/NoLegacyTraderTermsTest.java @@ -0,0 +1,48 @@ +package com.quantai.trader.quality; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class NoLegacyTraderTermsTest { + private static final Pattern FORBIDDEN = Pattern.compile( + "Playbook|Trigger|Markout|MARKET_PROXY|fallback|\\u515c\\u5e95|latest-promoted|longEntryScore|marketRiskScore|min_order_size|quantity_step_size"); + + @Test + void mainSourceAndMigrationsDoNotReintroduceOldTraderContracts() throws IOException { + List files; + try (Stream stream = Stream.concat( + Files.walk(Path.of("src/main/java")).filter(Files::isRegularFile), + Stream.concat( + Files.walk(Path.of("src/main/resources")).filter(Files::isRegularFile), + Files.walk(Path.of("src/test/resources")).filter(Files::isRegularFile)))) { + files = stream.toList(); + } + + List hits = files.stream() + .flatMap(path -> matches(path).stream()) + .toList(); + + assertThat(hits).isEmpty(); + } + + private List matches(Path path) { + try { + String text = Files.readString(path, StandardCharsets.UTF_8); + if (FORBIDDEN.matcher(text).find()) { + return List.of(path.toString()); + } + return List.of(); + } catch (IOException exception) { + throw new IllegalStateException("failed to scan " + path, exception); + } + } +} diff --git a/src/test/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReaderTest.java b/src/test/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReaderTest.java deleted file mode 100644 index cc79423..0000000 --- a/src/test/java/com/quantai/trader/replay/CryptoLakeReplayCsvMarketEventReaderTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.quantai.trader.replay; - -import org.junit.jupiter.api.Test; -import org.springframework.core.io.ClassPathResource; - -import java.nio.file.Path; -import java.time.Instant; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -class CryptoLakeReplayCsvMarketEventReaderTest { - - private final CryptoLakeReplayCsvMarketEventReader reader = new CryptoLakeReplayCsvMarketEventReader(); - - @Test - void readsCandidateTicksAndRecomputesReplayMarkoutLabels() throws Exception { - ReplayRunConfig config = config(); - - var ticks = reader.readTicks(config); - - assertThat(ticks).hasSize(2); - assertThat(ticks.getFirst().setupFeatures()) - .containsEntry("setupPass", true) - .containsEntry("side", "LONG") - .containsEntry("entryPrice", "100"); - assertThat(ticks.getFirst().labelInputs()) - .containsEntry("labelStatus", "REPLAY_MARKOUT_LABELED") - .containsEntry("side", "LONG") - .containsEntry("markoutBps15m", "200"); - assertThat(ticks.get(1).setupFeatures()) - .containsEntry("side", "SHORT"); - assertThat(ticks.get(1).labelInputs()) - .containsEntry("labelStatus", "REPLAY_MARKOUT_LABELED") - .containsEntry("side", "SHORT"); - } - - private ReplayRunConfig config() throws Exception { - Path replay = Path.of(new ClassPathResource("replay-fixtures/crypto-lake-replay-mini.csv").getFile().toURI()); - Path candidates = Path.of(new ClassPathResource("replay-fixtures/crypto-lake-candidate-events-mini.csv").getFile().toURI()); - return new ReplayRunConfig( - "trader_run_csv_test", - "BTCUSDT", - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - Instant.parse("2026-01-01T00:00:00Z"), - Instant.parse("2026-01-01T00:05:00Z"), - "trader_feature_v0", - "trader_label_v0", - Map.of( - "cryptoLakeReplay1m", new DataSourceSpec( - "crypto-lake-mini", - replay.toString(), - "fixture-hash-not-used", - null, - 18L, - null, - null, - "UTC", - Map.of() - ), - "candidateEvents", new DataSourceSpec( - "candidate-events-mini", - candidates.toString(), - "fixture-hash-not-used", - null, - 2L, - null, - null, - "UTC", - Map.of() - ) - ) - ); - } -} diff --git a/src/test/java/com/quantai/trader/replay/TraderP0CycleRunnerTest.java b/src/test/java/com/quantai/trader/replay/TraderP0CycleRunnerTest.java new file mode 100644 index 0000000..de9235a --- /dev/null +++ b/src/test/java/com/quantai/trader/replay/TraderP0CycleRunnerTest.java @@ -0,0 +1,68 @@ +package com.quantai.trader.replay; + +import com.quantai.trader.artifact.TraderArtifactLoader; +import com.quantai.trader.domain.TraderActionFactory; +import com.quantai.trader.enums.TraderActionType; +import com.quantai.trader.evidence.EvidenceAppender; +import com.quantai.trader.model.DeterministicTraderModelService; +import com.quantai.trader.outbox.InMemoryOutboxRepository; +import com.quantai.trader.position.TraderPositionManager; +import com.quantai.trader.risk.TraderRiskGate; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static com.quantai.trader.TestFixtures.T0; +import static com.quantai.trader.TestFixtures.properties; +import static org.assertj.core.api.Assertions.assertThat; + +class TraderP0CycleRunnerTest { + @Test + void runsReplayShadowCycleThroughModelPmRiskActionOutboxAndEvidence() { + EvidenceAppender evidenceAppender = new EvidenceAppender(); + InMemoryOutboxRepository outboxRepository = new InMemoryOutboxRepository(); + TraderP0CycleRunner runner = new TraderP0CycleRunner( + properties(), + new TraderArtifactLoader(properties()), + new DeterministicTraderModelService(), + new TraderPositionManager(), + new TraderRiskGate(), + new TraderActionFactory(), + evidenceAppender, + outboxRepository); + + TraderCycleResult result = runner.runFlatCycle(new ReplayMarketEvent( + "run-1", "BTC-USDT-PERP", T0, new BigDecimal("100"), new BigDecimal("99.5"), + new BigDecimal("1.2"), new BigDecimal("1000"))); + + assertThat(result.action().actionType()).isEqualTo(TraderActionType.OPEN_LONG); + assertThat(result.action().reduceOnly()).isFalse(); + assertThat(outboxRepository.all()).hasSize(1); + assertThat(outboxRepository.all().getFirst().destination()).isEqualTo("SHADOW_RECORDER"); + assertThat(evidenceAppender.all()).extracting("stage") + .containsExactly("MARKET_SNAPSHOT", "MODEL_OUTPUT", "PM_DECISION", "RISK_DECISION"); + } + + @Test + void recordsWaitActionWhenReplaySnapshotHasNoLiquidity() { + EvidenceAppender evidenceAppender = new EvidenceAppender(); + InMemoryOutboxRepository outboxRepository = new InMemoryOutboxRepository(); + TraderP0CycleRunner runner = new TraderP0CycleRunner( + properties(), + new TraderArtifactLoader(properties()), + new DeterministicTraderModelService(), + new TraderPositionManager(), + new TraderRiskGate(), + new TraderActionFactory(), + evidenceAppender, + outboxRepository); + + TraderCycleResult result = runner.runFlatCycle(new ReplayMarketEvent( + "run-1", "BTC-USDT-PERP", T0.plusSeconds(60), new BigDecimal("100"), new BigDecimal("99.5"), + new BigDecimal("1.2"), BigDecimal.ZERO)); + + assertThat(result.action().actionType()).isEqualTo(TraderActionType.WAIT); + assertThat(result.action().pricePlanId()).isNull(); + assertThat(outboxRepository.all()).hasSize(1); + } +} diff --git a/src/test/java/com/quantai/trader/replay/TraderReplayFixtureAcceptanceTest.java b/src/test/java/com/quantai/trader/replay/TraderReplayFixtureAcceptanceTest.java deleted file mode 100644 index 39353c4..0000000 --- a/src/test/java/com/quantai/trader/replay/TraderReplayFixtureAcceptanceTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.quantai.trader.replay; - -import com.quantai.trader.domain.TraderReplayReport; -import com.quantai.trader.enums.ReplayRunStatus; -import com.quantai.trader.persistence.ReplayReportRepository; -import com.quantai.trader.persistence.TraderSampleRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.io.ClassPathResource; - -import java.nio.file.Path; -import java.time.Instant; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -class TraderReplayFixtureAcceptanceTest { - - @Autowired - private ReplayRunService replayRunService; - - @Autowired - private ReplayReportRepository reportRepository; - - @Autowired - private TraderSampleRepository sampleRepository; - - @Test - void completesReplayForRepresentativeMarketFixtures() throws Exception { - List fixtures = List.of( - new ExpectedCompletedFixture("trend-up-breakout-happy.jsonl", 1, "CLOSE"), - new ExpectedCompletedFixture("trend-down-no-setup.jsonl", 0, "NONE"), - new ExpectedCompletedFixture("sideways-range-no-setup.jsonl", 0, "NONE"), - new ExpectedCompletedFixture("false-breakout-trigger-wait.jsonl", 0, "NONE"), - new ExpectedCompletedFixture("missing-features-data-quality.jsonl", 0, "NONE") - ); - - for (ExpectedCompletedFixture fixture : fixtures) { - ReplayRun run = runFixtureToTerminalStatus(fixture.fileName()); - - assertThat(run.status()).as(fixture.fileName()).isEqualTo(ReplayRunStatus.COMPLETED); - TraderReplayReport report = reportRepository.findByRunId(run.runId()).orElseThrow(); - assertThat(report.strictVsLoose()) - .containsEntry("replayEngine", "jsonl_fixture") - .containsEntry("tickCount", 1) - .containsEntry("sampleCount", 1) - .containsEntry("actionCount", fixture.expectedActionCount()) - .containsEntry("labeledSampleCount", 0) - .containsEntry("proxyOnlySampleCount", 1); - assertThat(report.candidateEvents()).isEqualTo(fixture.expectedActionCount()); - - var samples = sampleRepository.findByRunId(run.runId()); - assertThat(samples).hasSize(1); - assertThat(samples.getFirst().features()).containsEntry("actionType", fixture.expectedSampleActionType()); - assertThat(samples.getFirst().labels()).containsEntry("label_status", "PROXY_ONLY_NO_REPLAY_LABEL"); - } - } - - @Test - void failsReplayWhenSetupPassLacksRequiredEntryPlanPrices() throws Exception { - ReplayRun run = runFixtureToTerminalStatus("incomplete-entry-plan-hard-fail.jsonl"); - - assertThat(run.status()).isEqualTo(ReplayRunStatus.FAILED); - assertThat(run.failureReason()).contains("entryPrice"); - assertThat(reportRepository.findByRunId(run.runId())).isEmpty(); - assertThat(sampleRepository.findByRunId(run.runId())).isEmpty(); - } - - private ReplayRun runFixtureToTerminalStatus(String fixtureName) throws Exception { - ReplayRunResponse response = replayRunService.createRun(configFor(fixtureName)); - return waitForTerminalRun(response.runId()); - } - - private ReplayRun waitForTerminalRun(String runId) throws InterruptedException { - for (int i = 0; i < 100; i++) { - ReplayRun run = replayRunService.find(runId).orElseThrow(); - if (run.status() == ReplayRunStatus.COMPLETED - || run.status() == ReplayRunStatus.FAILED - || run.status() == ReplayRunStatus.CANCELLED) { - return run; - } - Thread.sleep(25); - } - return replayRunService.find(runId).orElseThrow(); - } - - private ReplayRunConfig configFor(String fixtureName) throws Exception { - Path path = Path.of(new ClassPathResource("replay-fixtures/" + fixtureName).getFile().toURI()); - return new ReplayRunConfig( - null, - "BTCUSDT", - "BREAKOUT_RETEST_CONTINUATION", - "2026-06-22.p0", - Instant.parse("2026-01-01T00:00:00Z"), - Instant.parse("2026-01-02T00:00:00Z"), - "trader_feature_v0", - "trader_label_v0", - Map.of("ticks", new DataSourceSpec( - fixtureName.replace(".jsonl", ""), - path.toString(), - "fixture-hash-not-used-in-p0", - null, - 1L, - null, - null, - "UTC", - Map.of() - )) - ); - } - - private record ExpectedCompletedFixture( - String fileName, - int expectedActionCount, - String expectedSampleActionType - ) { - } -} diff --git a/src/test/java/com/quantai/trader/risk/TraderPositionSizerTest.java b/src/test/java/com/quantai/trader/risk/TraderPositionSizerTest.java deleted file mode 100644 index 32fa6b4..0000000 --- a/src/test/java/com/quantai/trader/risk/TraderPositionSizerTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.quantai.trader.risk; - -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.PositionSizingPlan; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; - -import static org.assertj.core.api.Assertions.assertThat; - -class TraderPositionSizerTest { - - @Test - void allowsFullInitialEntryForStrongSignal() { - TraderPositionSizer sizer = new TraderPositionSizer(TestFixtures.properties()); - - PositionSizingPlan plan = sizer.sizeInitialPlan( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.CONTEXT_CHECK), - TestFixtures.candidate(), - TestFixtures.strongTrigger(), - TestFixtures.pricePlan() - ); - - assertThat(plan.initialLegRatio()).isEqualByComparingTo(BigDecimal.ONE); - assertThat(plan.plannedLegCount()).isEqualTo(1); - assertThat(plan.sizingMethod()).isEqualTo("SIGNAL_EXECUTION_RISK_DYNAMIC"); - } - - @Test - void weakSignalUsesDynamicPartialSizeInsteadOfFixedTemplate() { - TraderPositionSizer sizer = new TraderPositionSizer(TestFixtures.properties()); - var weakTrigger = new com.quantai.trader.domain.TriggerDecision( - true, - new BigDecimal("0.25"), - "TRIGGER_ACCEPTED", - null, - java.util.Map.of() - ); - - PositionSizingPlan plan = sizer.sizeInitialPlan( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.CONTEXT_CHECK), - TestFixtures.candidate(), - weakTrigger, - TestFixtures.pricePlan() - ); - - assertThat(plan.initialLegRatio()).isLessThan(BigDecimal.ONE); - assertThat(plan.plannedLegCount()).isEqualTo(3); - } -} diff --git a/src/test/java/com/quantai/trader/risk/TraderRiskGateTest.java b/src/test/java/com/quantai/trader/risk/TraderRiskGateTest.java index d47df8c..d32e098 100644 --- a/src/test/java/com/quantai/trader/risk/TraderRiskGateTest.java +++ b/src/test/java/com/quantai/trader/risk/TraderRiskGateTest.java @@ -1,63 +1,83 @@ package com.quantai.trader.risk; -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.ExecutionDecision; +import com.quantai.trader.domain.TraderPositionState; import com.quantai.trader.domain.TraderRiskDecision; -import com.quantai.trader.persistence.TraderRiskDecisionRepository; +import com.quantai.trader.enums.PositionSide; +import com.quantai.trader.enums.TraderActionType; import org.junit.jupiter.api.Test; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - +import static com.quantai.trader.TestFixtures.*; import static org.assertj.core.api.Assertions.assertThat; class TraderRiskGateTest { + private final TraderRiskGate riskGate = new TraderRiskGate(); @Test - void recordsRiskDecisionForAllowedAction() { - CapturingRiskDecisionRepository repository = new CapturingRiskDecisionRepository(); - TraderRiskGate gate = new TraderRiskGate(TestFixtures.properties(), repository); + void killSwitchBlocksOnlyExposureIncreasingActions() { + RiskLimits limits = new RiskLimits(bd("200"), java.math.BigDecimal.ONE, bd("500"), 3, 1500, true, false); - var decision = gate.evaluate( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.ENTRY_PLANNED), - TestFixtures.fullInitialPlan(), - new ExecutionDecision(true, BigDecimal.ONE, "PASS", null, Map.of()) - ); + TraderRiskDecision open = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), flatPosition(), account(), execution(), snapshot(), limits)); + TraderRiskDecision reduce = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.REDUCE_LONG, PositionSide.LONG), longPosition("10"), account(), execution(), snapshot(), limits)); + + assertThat(open.allowAction()).isFalse(); + assertThat(open.finalAction()).isEqualTo(TraderActionType.WAIT); + assertThat(open.blocker()).isEqualTo("KILL_SWITCH_ACTIVE"); + assertThat(reduce.allowAction()).isTrue(); + assertThat(reduce.finalAction()).isEqualTo(TraderActionType.REDUCE_LONG); + } + + @Test + void lowLiquidationBufferForcesCloseOnExistingPosition() { + TraderPositionState lowBufferPosition = new TraderPositionState( + "position-state-1", "run-1", "cycle-1", "BTC-USDT-PERP", + PositionSide.LONG, bd("0.30"), bd("100"), bd("101"), bd("12"), bd("100"), + 0, bd("0.40"), null); + + TraderRiskDecision decision = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.HOLD, PositionSide.LONG), lowBufferPosition, account(), execution(), snapshot(), riskLimits())); assertThat(decision.allowAction()).isTrue(); - assertThat(repository.findByCycleId("trader_run_test", "trader_cycle_test")).hasSize(1); + assertThat(decision.finalAction()).isEqualTo(TraderActionType.CLOSE_LONG); + assertThat(decision.blocker()).isEqualTo("LIQUIDATION_BUFFER_LOW"); } @Test - void blocksWhenExecutionIsBlockedAndStillRecordsDecision() { - CapturingRiskDecisionRepository repository = new CapturingRiskDecisionRepository(); - TraderRiskGate gate = new TraderRiskGate(TestFixtures.properties(), repository); + void exchangeInstabilityBlocksOpenAddActions() { + TraderRiskDecision latency = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), flatPosition(), account(), + execution(java.util.List.of(), 1600, 0), snapshot(), riskLimits())); + TraderRiskDecision errors = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.ADD_LONG, PositionSide.LONG), longPosition("10"), account(), + execution(java.util.List.of(), 10, 3), snapshot(), riskLimits())); - var decision = gate.evaluate( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.ENTRY_PLANNED), - TestFixtures.fullInitialPlan(), - new ExecutionDecision(false, BigDecimal.ZERO, "BAD_EXECUTION", "TRADER_RISK_BLOCKED", Map.of()) - ); - - assertThat(decision.blocked()).isTrue(); - assertThat(repository.findByCycleId("trader_run_test", "trader_cycle_test").getFirst().allowAction()).isFalse(); + assertThat(latency.allowAction()).isFalse(); + assertThat(latency.blocker()).isEqualTo("EXCHANGE_LATENCY_HIGH"); + assertThat(errors.allowAction()).isFalse(); + assertThat(errors.blocker()).isEqualTo("EXCHANGE_UNSTABLE"); } - private static class CapturingRiskDecisionRepository implements TraderRiskDecisionRepository { - private final List decisions = new ArrayList<>(); + @Test + void dailyLossAndDataQualityBlockActions() { + TraderRiskDecision dailyLoss = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), flatPosition(), + account("200", "0", "1.0"), execution(), snapshot(), riskLimits())); + TraderRiskDecision dataNotReady = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), flatPosition(), + account(), execution(), snapshot(false, "1000"), riskLimits())); - @Override - public void insert(TraderRiskDecision decision) { - decisions.add(decision); - } + assertThat(dailyLoss.blocker()).isEqualTo("MAX_DAILY_LOSS"); + assertThat(dataNotReady.blocker()).isEqualTo("DATA_NOT_READY"); + } - @Override - public List findByCycleId(String runId, String cycleId) { - return decisions.stream() - .filter(item -> item.runId().equals(runId) && item.cycleId().equals(cycleId)) - .toList(); - } + @Test + void allowsCleanP0Decision() { + TraderRiskDecision decision = riskGate.evaluate(new RiskGateInput( + pmDecision(TraderActionType.OPEN_LONG, PositionSide.LONG), flatPosition(), account(), execution(), snapshot(), riskLimits())); + + assertThat(decision.allowAction()).isTrue(); + assertThat(decision.finalAction()).isEqualTo(TraderActionType.OPEN_LONG); + assertThat(decision.blocker()).isNull(); } } diff --git a/src/test/java/com/quantai/trader/runtime/P0RuntimeGuardTest.java b/src/test/java/com/quantai/trader/runtime/P0RuntimeGuardTest.java new file mode 100644 index 0000000..95e52b5 --- /dev/null +++ b/src/test/java/com/quantai/trader/runtime/P0RuntimeGuardTest.java @@ -0,0 +1,38 @@ +package com.quantai.trader.runtime; + +import com.quantai.trader.domain.TraderException; +import com.quantai.trader.enums.TraderExecutionMode; +import com.quantai.trader.enums.TraderRunMode; +import org.junit.jupiter.api.Test; + +import static com.quantai.trader.TestFixtures.properties; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class P0RuntimeGuardTest { + @Test + void allowsReplaySimAndShadowWhenTradingIsDisabled() { + assertThatCode(() -> new P0RuntimeGuard(properties(TraderRunMode.REPLAY_SIM, TraderExecutionMode.REPLAY_SIM, false, false)).validateStartup()) + .doesNotThrowAnyException(); + assertThatCode(() -> new P0RuntimeGuard(properties(TraderRunMode.SHADOW, TraderExecutionMode.SHADOW, false, false)).validateStartup()) + .doesNotThrowAnyException(); + } + + @Test + void rejectsPaperOrRealModesInsteadOfConvertingThem() { + assertThatThrownBy(() -> new P0RuntimeGuard(properties(TraderRunMode.PAPER, TraderExecutionMode.SHADOW, false, false)).validateStartup()) + .isInstanceOf(TraderException.class) + .hasMessageContaining("REPLAY_SIM or SHADOW"); + + assertThatThrownBy(() -> new P0RuntimeGuard(properties(TraderRunMode.SHADOW, TraderExecutionMode.REAL, false, false)).validateStartup()) + .isInstanceOf(TraderException.class) + .hasMessageContaining("execution.mode"); + } + + @Test + void rejectsAnyTradingEnabledP0Runtime() { + assertThatThrownBy(() -> new P0RuntimeGuard(properties(TraderRunMode.REPLAY_SIM, TraderExecutionMode.REPLAY_SIM, true, false)).validateStartup()) + .isInstanceOf(TraderException.class) + .hasMessageContaining("trading-enabled=false"); + } +} diff --git a/src/test/java/com/quantai/trader/sample/TrainingSampleExporterTest.java b/src/test/java/com/quantai/trader/sample/TrainingSampleExporterTest.java deleted file mode 100644 index dc766dc..0000000 --- a/src/test/java/com/quantai/trader/sample/TrainingSampleExporterTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.quantai.trader.sample; - -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.TraderTrainingSample; -import com.quantai.trader.persistence.TraderSampleRepository; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class TrainingSampleExporterTest { - - @Test - void exportsReplayMarkoutSampleWithFeatureAndLabelVersions() { - CapturingSampleRepository repository = new CapturingSampleRepository(); - TrainingSampleExporter exporter = new TrainingSampleExporter(TestFixtures.properties(), repository, new TriggerMarkoutLabeler()); - - var sample = exporter.export( - TestFixtures.cycle(com.quantai.trader.enums.TraderState.SAMPLE_EXPORTED), - TestFixtures.labeledSnapshot(), - TestFixtures.candidate(), - TestFixtures.action(), - TestFixtures.openedPath(false) - ); - - assertThat(sample.proxyOnly()).isFalse(); - assertThat(sample.featureVersion()).isEqualTo("trader_feature_v0"); - assertThat(sample.labelVersion()).isEqualTo("trader_label_v0"); - assertThat(sample.netReturnBps1x()).isEqualByComparingTo("14.00000000"); - assertThat(sample.labels()).containsEntry("label_status", "REPLAY_MARKOUT_LABELED"); - assertThat(repository.findByRunId("trader_run_test")).hasSize(1); - } - - private static class CapturingSampleRepository implements TraderSampleRepository { - private final List samples = new ArrayList<>(); - - @Override - public void insert(TraderTrainingSample sample) { - samples.add(sample); - } - - @Override - public List findByRunId(String runId) { - return samples.stream() - .filter(item -> item.runId().equals(runId)) - .toList(); - } - } -} diff --git a/src/test/java/com/quantai/trader/state/TraderStateMachineTest.java b/src/test/java/com/quantai/trader/state/TraderStateMachineTest.java deleted file mode 100644 index db5ebb0..0000000 --- a/src/test/java/com/quantai/trader/state/TraderStateMachineTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.quantai.trader.state; - -import com.quantai.trader.TestFixtures; -import com.quantai.trader.domain.TraderException; -import com.quantai.trader.enums.TraderActionType; -import com.quantai.trader.enums.TraderState; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class TraderStateMachineTest { - - private final TraderStateMachine stateMachine = new TraderStateMachine(); - - @Test - void initialEntryOnlyFromEntryPlanned() { - var action = stateMachine.toInitialEntryAction( - TestFixtures.cycle(TraderState.ENTRY_PLANNED), - TestFixtures.candidate(), - TestFixtures.fullInitialPlan() - ); - - assertThat(action.actionType()).isEqualTo(TraderActionType.OPEN_INITIAL); - } - - @Test - void blocksInitialEntryFromWrongState() { - assertThatThrownBy(() -> stateMachine.toInitialEntryAction( - TestFixtures.cycle(TraderState.CONTEXT_CHECK), - TestFixtures.candidate(), - TestFixtures.fullInitialPlan() - )).isInstanceOf(TraderException.class) - .hasMessageContaining("requires ENTRY_PLANNED"); - } - - @Test - void blocksPlannedLegAfterReduce() { - var plan = new com.quantai.trader.domain.TraderEntryPlan( - "trader_run_test", - "trader_cycle_test", - "trader_action_test_2", - "trader_leg_test_1", - "trader_candidate_test", - TraderActionType.OPEN_PLANNED_LEG, - 1, - 3, - new java.math.BigDecimal("0.20"), - "SIGNAL_EXECUTION_RISK_DYNAMIC", - java.math.BigDecimal.ZERO, - java.math.BigDecimal.ZERO, - java.math.BigDecimal.ONE, - new java.math.BigDecimal("65000"), - new java.math.BigDecimal("64800"), - new java.math.BigDecimal("64920"), - new java.math.BigDecimal("65350"), - null, - 300_000, - 7_200_000, - "TEST_PLANNED_LEG" - ); - - assertThatThrownBy(() -> stateMachine.toPlannedLegAction( - TestFixtures.cycle(TraderState.PLANNED_LEG_WAIT), - plan, - TestFixtures.openedPath(true) - )).isInstanceOf(TraderException.class) - .hasMessageContaining("after reduce"); - } -} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 066b244..c8ead5e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,8 +1,37 @@ spring: - datasource: - url: jdbc:mysql://127.0.0.1:3306/quant_app_test_codex?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false - driver-class-name: com.mysql.cj.jdbc.Driver - username: quant_app - password: quant_app flyway: + enabled: false + +trader: + service-name: quant-trader-service-test + run-mode: SHADOW + symbol: BTC-USDT-PERP + artifact: + model-bundle-version: trader-v4-btc-p0-test + calibration-bundle-version: cal-v4-btc-p0-test + pm-config-version: pm-v4-btc-p0-test + artifact-root: /tmp/trader-v4-p0-test-artifacts + feedback: + http-enabled: false + execution: + mode: SHADOW + max-api-error-count: 3 + max-exchange-latency-ms: 1500 + runtime: + redis-key-prefix: trader:v4:test + require-redis-for-open-add: true + trading-enabled: false + outbox: enabled: true + max-retry-count: 5 + release: + require-review-for-paper: true + require-review-for-live-probe: true + active-pointer-check-enabled: true + risk: + max-daily-loss-bps: 200 + max-total-exposure-ratio: 1.0 + min-liquidation-buffer-bps: 500 + position-manager: + max-single-leg-ratio: 1.0 + max-total-position-ratio: 1.0 diff --git a/src/test/resources/replay-fixtures/README.md b/src/test/resources/replay-fixtures/README.md deleted file mode 100644 index 0c9e240..0000000 --- a/src/test/resources/replay-fixtures/README.md +++ /dev/null @@ -1,22 +0,0 @@ -P0 replay fixtures -================== - -These JSONL files are deterministic acceptance fixtures for the P0 replay loop. -They are not a profitability backtest corpus. Each line is one replay clock tick -with the schema consumed by JsonlReplayMarketEventReader: - -- eventTime -- contextFeatures -- setupFeatures -- triggerFeatures -- executionFeatures -- dataQuality - -Scenario notes: - -- trend-up-breakout-happy.jsonl: accepted continuation setup; should produce one action and one sample. -- trend-down-no-setup.jsonl: bearish regime without a valid long continuation setup; should complete with no action. -- sideways-range-no-setup.jsonl: range regime without a setup; should complete with no action. -- false-breakout-trigger-wait.jsonl: setup exists but trigger score is too weak; should complete with no action. -- missing-features-data-quality.jsonl: data-quality blocker; should complete with a blocked sample. -- incomplete-entry-plan-hard-fail.jsonl: setupPass is true but required prices are missing; should fail the replay run. diff --git a/src/test/resources/replay-fixtures/crypto-lake-candidate-events-mini.csv b/src/test/resources/replay-fixtures/crypto-lake-candidate-events-mini.csv deleted file mode 100644 index 10b4740..0000000 --- a/src/test/resources/replay-fixtures/crypto-lake-candidate-events-mini.csv +++ /dev/null @@ -1,3 +0,0 @@ -event_id,bar_time,signal_type,direction,source_service,symbol,old_fusion_score -mini-long-1,1767225600000,BREAKOUT_RETEST_CONTINUATION,LONG,TEST_CANDIDATE_EVENT,BTCUSDT,0.95 -mini-short-1,1767225720000,BREAKOUT_RETEST_CONTINUATION,SHORT,TEST_CANDIDATE_EVENT,BTCUSDT,0.90 diff --git a/src/test/resources/replay-fixtures/crypto-lake-replay-mini.csv b/src/test/resources/replay-fixtures/crypto-lake-replay-mini.csv deleted file mode 100644 index ba26988..0000000 --- a/src/test/resources/replay-fixtures/crypto-lake-replay-mini.csv +++ /dev/null @@ -1,19 +0,0 @@ -symbol,timeframe,open_time,open,high,low,close,volume,taker_buy_volume,funding_bps,open_interest,best_bid_price,best_ask_price,observed_spread_bps,observed_slippage_bps,expected_slippage_bps,p95_latency_ms,source_coverage -BTCUSDT,1m,2026-01-01T00:00:00Z,100.0,100.4,99.8,100.0,10,5,0.1,1000,99.99,100.01,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:01:00Z,100.0,100.7,99.9,100.4,11,6,0.1,1001,100.39,100.41,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:02:00Z,100.4,100.6,100.0,100.2,12,7,0.1,1002,100.19,100.21,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:03:00Z,100.2,100.8,100.1,100.5,13,8,0.1,1003,100.49,100.51,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:04:00Z,100.5,101.0,100.2,100.8,14,9,0.1,1004,100.79,100.81,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:05:00Z,100.8,101.3,100.5,101.0,15,10,0.1,1005,100.99,101.01,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:06:00Z,101.0,101.6,100.7,101.2,16,11,0.1,1006,101.19,101.21,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:07:00Z,101.2,101.7,100.8,101.3,17,12,0.1,1007,101.29,101.31,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:08:00Z,101.3,101.8,100.9,101.4,18,13,0.1,1008,101.39,101.41,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:09:00Z,101.4,101.9,101.0,101.5,19,14,0.1,1009,101.49,101.51,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:10:00Z,101.5,102.0,101.1,101.6,20,15,0.1,1010,101.59,101.61,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:11:00Z,101.6,102.1,101.2,101.7,21,16,0.1,1011,101.69,101.71,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:12:00Z,101.7,102.2,101.3,101.8,22,17,0.1,1012,101.79,101.81,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:13:00Z,101.8,102.3,101.4,101.9,23,18,0.1,1013,101.89,101.91,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:14:00Z,101.9,102.4,101.5,102.0,24,19,0.1,1014,101.99,102.01,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:15:00Z,102.0,102.5,101.6,102.0,25,20,0.1,1015,101.99,102.01,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:16:00Z,102.0,102.1,99.5,99.8,26,6,0.1,1016,99.79,99.81,2,1,1,30,1 -BTCUSDT,1m,2026-01-01T00:17:00Z,99.8,100.0,98.8,99.0,27,5,0.1,1017,98.99,99.01,2,1,1,30,1 diff --git a/src/test/resources/replay-fixtures/false-breakout-trigger-wait.jsonl b/src/test/resources/replay-fixtures/false-breakout-trigger-wait.jsonl deleted file mode 100644 index 08286d8..0000000 --- a/src/test/resources/replay-fixtures/false-breakout-trigger-wait.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"eventTime":"2026-01-01T03:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"weak_breakout_retest","timeframeAlignment":"1h_up_4h_flat"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","entryPrice":"65120","invalidPrice":"64980","stopPrice":"65040","targetPrice":"65450","executionQualityScore":"0.76","volumeImpulse":"0.88","retestHold":true},"triggerFeatures":{"triggerScore":"0.32","triggerName":"false_breakout_probe","breakoutFollowThrough":false},"executionFeatures":{"lastPrice":"65105","spreadBps":"1.4","bookImbalance":"0.49"},"dataQuality":{"missing_features":[]}} diff --git a/src/test/resources/replay-fixtures/incomplete-entry-plan-hard-fail.jsonl b/src/test/resources/replay-fixtures/incomplete-entry-plan-hard-fail.jsonl deleted file mode 100644 index a6ba6a5..0000000 --- a/src/test/resources/replay-fixtures/incomplete-entry-plan-hard-fail.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"eventTime":"2026-01-01T05:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","executionQualityScore":"0.90"},"triggerFeatures":{"triggerScore":"0.95","triggerName":"micro_continuation"},"executionFeatures":{"lastPrice":"65300"},"dataQuality":{"missing_features":[]}} diff --git a/src/test/resources/replay-fixtures/missing-features-data-quality.jsonl b/src/test/resources/replay-fixtures/missing-features-data-quality.jsonl deleted file mode 100644 index 060ee44..0000000 --- a/src/test/resources/replay-fixtures/missing-features-data-quality.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"eventTime":"2026-01-01T04:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","entryPrice":"65200","invalidPrice":"65020","stopPrice":"65100","targetPrice":"65580","executionQualityScore":"0.84"},"triggerFeatures":{"triggerScore":"0.91","triggerName":"micro_continuation"},"executionFeatures":{"lastPrice":"65210"},"dataQuality":{"missing_features":["level_1.best_bid","level_1.best_ask"]}} diff --git a/src/test/resources/replay-fixtures/sideways-range-no-setup.jsonl b/src/test/resources/replay-fixtures/sideways-range-no-setup.jsonl deleted file mode 100644 index 40cf49c..0000000 --- a/src/test/resources/replay-fixtures/sideways-range-no-setup.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"eventTime":"2026-01-01T02:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"RANGE","marketStructure":"mid_range_chop","timeframeAlignment":"mixed"},"setupFeatures":{"setupPass":false,"setupName":"range_filter_rejected","rejectReason":"no_clean_platform_breakout"},"triggerFeatures":{"triggerScore":"0.35","triggerName":"range_noise"},"executionFeatures":{"lastPrice":"64680","spreadBps":"1.1"},"dataQuality":{"missing_features":[]}} diff --git a/src/test/resources/replay-fixtures/trend-down-no-setup.jsonl b/src/test/resources/replay-fixtures/trend-down-no-setup.jsonl deleted file mode 100644 index d7a9b84..0000000 --- a/src/test/resources/replay-fixtures/trend-down-no-setup.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"eventTime":"2026-01-01T01:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"DOWN","marketStructure":"lower_high_break","timeframeAlignment":"1h_4h_down"},"setupFeatures":{"setupPass":false,"setupName":"long_continuation_rejected","rejectReason":"trend_direction_mismatch"},"triggerFeatures":{"triggerScore":"0.20","triggerName":"none"},"executionFeatures":{"lastPrice":"64240","spreadBps":"0.9"},"dataQuality":{"missing_features":[]}} diff --git a/src/test/resources/replay-fixtures/trend-up-breakout-happy.jsonl b/src/test/resources/replay-fixtures/trend-up-breakout-happy.jsonl deleted file mode 100644 index 444da3e..0000000 --- a/src/test/resources/replay-fixtures/trend-up-breakout-happy.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"eventTime":"2026-01-01T00:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest","timeframeAlignment":"1h_4h_up"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","entryPrice":"65000","invalidPrice":"64800","stopPrice":"64920","targetPrice":"65350","executionQualityScore":"0.92","volumeImpulse":"1.48","retestHold":true},"triggerFeatures":{"triggerScore":"0.95","triggerName":"micro_continuation","breakoutFollowThrough":true},"executionFeatures":{"lastPrice":"65010","spreadBps":"0.8","bookImbalance":"0.57"},"dataQuality":{"missing_features":[]}}