diff --git a/SESSION.md b/SESSION.md index 7828198..169b3f1 100644 --- a/SESSION.md +++ b/SESSION.md @@ -850,10 +850,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - D3 Liquidity Kernel is complete locally: added `DOCTRINE_LIQUIDITY_PHASE=D3_LIQUIDITY_KERNEL`, `LiquidityTargetRule`, `GET /trading/liquidity-kernel`, Strategy Backtest `liquidity_backtest_features` / `liquidity_kernel_status` / `liquidity_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`. BSL / SSL / liquidity pool / stop run / sweep / raid are now target-ranking and no-trade/wait rules; `sweep_is_not_setup`, `missing_liquidity_target`, `missing_liquidity_pool`, `missing_working_draw`, and `manipulation_window_wait` block promotion until the Working Draw, ranked pool, post-take reaction, and objective reorder evidence are present. No execution/readiness/risk/trust/operator-confirmation gate was changed. - D4 PD Array / Reaction Kernel is complete, deployed, and remote-verified: added `DOCTRINE_REACTION_PHASE=D4_PD_ARRAY_REACTION_KERNEL`, `PdArrayReactionRule`, `GET /trading/pd-array-kernel`, Strategy Backtest `pd_array_backtest_features` / `pd_array_reaction_status` / `pd_array_reaction_summary`, and `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`. FVG / OB / Breaker / Mitigation / BPR/Rebalancing now stay reaction-location quality rankings rather than direction tags; `missing_pd_array`, `missing_pd_array_role`, `missing_array_location`, `fvg_is_not_setup`, `order_block_not_authorized`, and `breaker_without_failure_precondition` block promotion until role, rank, location, freshness, origin, reaction/retest, and invalidation are explicit. Validation passed focused D4/API/backtest/docs tests, full unit discovery (`839` tests), script static checks (`79` scripts), JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/pd-array-kernel` field smoke, `/trading/backtests` D4 field smoke, and Playwright screenshot evidence at `output/playwright/d4-pd-array-strategy-backtest-1366x768.png`. The read-only smoke initially exposed the expected external proxy blocker; starting the local v2rayN/sing-box proxy restored `192.168.3.73:10808` and the gate passed. No execution/readiness/risk/trust/operator-confirmation gate was changed. - D5 Time / Session Kernel is complete, deployed, and remote-verified: added `DOCTRINE_SESSION_PHASE=D5_TIME_SESSION_KERNEL`, `TimeSessionRule`, `GET /trading/time-session-kernel`, Strategy Backtest `time_session_backtest_features` / `time_session_kernel_status` / `time_session_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`. Asia / London / NY AM / NY PM, killzone, Judas/manipulation window, opening range, weekly profile, and news/no-trade time are now measurable session-quality gates; `missing_session_window`, `session_window_unverified`, `outside_session_window`, `session_not_allowed`, `missing_killzone`, `killzone_not_active`, `judas_window_wait`, `opening_range_unresolved`, `weekly_profile_missing`, `news_time_no_trade`, and `session_invalidation_missing` block promotion until timing is explicit. Validation passed focused D5/API/backtest/docs tests, full unit discovery (`846` tests), script static checks (`79` scripts), secret static checks, JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/time-session-kernel` field smoke, `/trading/backtests` D5 field smoke, and Playwright screenshot evidence at `output/playwright/d5-time-session-strategy-backtest-1366x768.png`. Remote latest backtest `#513` is correctly blocked by D5 as `study_only / session_window_unverified` instead of treating `ASIA` plus `mark_outside` as a verified session window. No execution/readiness/risk/trust/operator-confirmation gate was changed. +- D6 Model Playbook Kernel is complete, deployed, and remote-verified: added `DOCTRINE_MODEL_PLAYBOOK_PHASE=D6_MODEL_PLAYBOOK_KERNEL`, `ModelPlaybookRule`, `GET /trading/model-playbook-kernel`, Strategy Backtest `model_playbook_backtest_features` / `model_playbook_kernel_status` / `model_playbook_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md`. Raw `model_code` values such as `SWEEP_MSS_FVG` now stay `study_only` until they resolve to a registered setup playbook with explicit Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target, batch cohort, quality-gate, journal-feedback, and operator-runbook contracts. Validation passed focused D6/API/backtest/docs tests, full unit discovery (`853` tests), script static checks (`79` scripts), secret static checks, JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/model-playbook-kernel` field smoke, `/trading/backtests` D6 field smoke, and Playwright screenshot evidence at `output/playwright/d6-model-playbook-strategy-backtest-fullpage-zh.png`. Remote latest backtest `#513` is correctly blocked by D6 as `study_only / missing_model_playbook` instead of promoting a surface-shape model as a tradeable setup. No execution/readiness/risk/trust/operator-confirmation gate was changed. ## Exact Next Step -Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`, `docs/ICT交易全书_出版终校版.md`, `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md`, `SESSION.md`, and `TASKS.md` as the active anchors. Start D6 Model Playbook Kernel: convert the book-derived models into full setup playbooks with required Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target fields and batch-backtest cohorts. Do not restart external-context/archive/digest long-tail work, do not add order-submission features, do not physically delete S1 deprecated surfaces yet, and do not fabricate R2 paper samples from blocked candidates. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged. +Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md`, `docs/ICT交易全书_出版终校版.md`, `docs/AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md`, `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md`, `SESSION.md`, and `TASKS.md` as the active anchors. Start D7 Trader Product Rebuild / Playbook Promotion Gate: only playbook-mapped, backtest-supported, plan-approved setups can re-enter Trader Cockpit focus; keep study-only evidence in Strategy Backtest / Study Notes / System Ops, not trader-facing Review Now. Do not restart external-context/archive/digest long-tail work, do not add order-submission features, do not physically delete S1 deprecated surfaces yet, and do not fabricate R2 paper samples from blocked candidates. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged. ## Validation Commands Used Recently diff --git a/TASKS.md b/TASKS.md index b5743ba..12ad229 100644 --- a/TASKS.md +++ b/TASKS.md @@ -957,7 +957,8 @@ - [x] D3 Liquidity Kernel: rebuilt BSL / SSL / liquidity pool / stop run / sweep / raid from shape labels into target-ranking and no-trade/wait rules; sweep remains evidence component only, Strategy Backtest exposes liquidity features/status/summary, and `/trading/liquidity-kernel` exposes the D3 registry - [x] D4 PD Array / Reaction Kernel: rebuilt FVG / OB / Breaker / Mitigation / Rebalance / Void into reaction-location quality ranking and no-trade/wait rules; PD Array remains location/reaction evidence only, Strategy Backtest exposes PD Array features/status/summary, `/trading/pd-array-kernel` exposes the D4 registry, and remote deploy/ops/read-only smoke passed after the local proxy was restarted - [x] D5 Time / Session Kernel: rebuilt Asia / London / NY AM / NY PM / killzone / Judas window / opening range / weekly profile / news time into measurable session-quality gates instead of loose time labels; Strategy Backtest now exposes `time_session_backtest_features`, `time_session_kernel_status`, `time_session_kernel_summary`, and `/trading/time-session-kernel` without changing execution/readiness gates -- [ ] D6 next product gate: rebuild Model Playbook Kernel so book-derived models become full setup playbooks with required Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target fields and batch-backtest cohorts +- [x] D6 Model Playbook Kernel: raw model codes now stay study-only until they resolve to registered setup playbooks with Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target, batch-cohort, quality-gate, journal-feedback, and operator-runbook contracts; Strategy Backtest exposes D6 features/status/summary and `/trading/model-playbook-kernel` exposes the D6 registry without changing execution/readiness gates +- [ ] D7 Trader Product Rebuild / Playbook Promotion Gate: only playbook-mapped, backtest-supported, plan-approved setups may re-enter Trader Cockpit focus; keep study-only evidence in Strategy Backtest / Study Notes / System Ops rather than Review Now - [x] R3 OKX sandbox isolated smoke tooling: guarded script and service can call OKX demo submit/query/cancel/query/fills only when `OKX_SANDBOX_SMOKE_EXECUTE=1`; local validation, deploy smoke, and remote ops smoke passed - [ ] R3 sandbox credential permission gate deferred: current phase is assisted analysis / decision support only, so do not request trade-permission credentials or make submit/cancel smoke the next gate - [ ] Long-term target: complete `Real-time Assisted Trading V1` without enabling default live auto-trading diff --git a/docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md b/docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md new file mode 100644 index 0000000..aea749f --- /dev/null +++ b/docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md @@ -0,0 +1,89 @@ +# AI_ICT Doctrine Kernel Model Playbook Kernel D6 2026-05-22 v1 + +## Scope + +D6 rebuilds model identity from raw shape labels into book-derived setup playbooks. It follows `docs/ICT交易全书_出版终校版.md` and the hard mainline chain: + +`Range / Draw / Location -> Liquidity / Sweep / Inducement / Path -> Displacement / MSS / FVG / Retest / Session -> backtest evidence -> pre-market plan -> intraday unique focus or no-trade reason -> journal / study-notes feedback`. + +## Product Rule + +Raw `model_code` values such as `SWEEP_MSS_FVG` or `OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE` are not tradable setups. They can remain historical research material, but they cannot support Trader Cockpit `Review Now` unless they resolve into a registered full-chain setup playbook. + +Every model playbook must declare: + +- Range contract +- Draw contract +- Liquidity contract +- PD Array contract +- Session contract +- Confirmation / trigger contract +- Invalidation contract +- Target / management contract +- Batch backtest cohort +- Journal / study-note feedback contract + +If any contract is missing, Strategy Backtest and Trader Cockpit must keep the setup as `study_only` or `wait`. + +## Implemented Kernel + +New phase: + +- `DOCTRINE_MODEL_PLAYBOOK_PHASE = D6_MODEL_PLAYBOOK_KERNEL` + +New registry/API: + +- `ModelPlaybookRule` +- `D6_MODEL_PLAYBOOK_RULES` +- `build_model_playbook_kernel_registry` +- `list_model_playbook_rules` +- `GET /trading/model-playbook-kernel` + +New backtest surface fields: + +- `model_playbook_backtest_features` +- `model_playbook_kernel_status` +- `model_playbook_kernel_summary` +- `model_playbook_kernel_status_counts` +- `model_playbook_kernel_reason_counts` +- `by_model_playbook` + +## D6 Rules + +1. `model_family_declared` + A model must point to a registered book-derived playbook before any backtest result can be trusted. + +2. `full_chain_contract_complete` + A playbook must define Range, Draw, Liquidity, PD Array, Session, Confirmation, Invalidation, and Target contracts. + +3. `confirmation_trigger_bound` + Displacement / MSS / FVG / Retest can confirm only after they serve the declared Draw. + +4. `risk_target_contract_bound` + Invalidation and IRL / ERL target architecture must be explicit before promotion. + +5. `batch_cohort_contract_ready` + Backtest evidence must be grouped by playbook cohort, not raw model code. + +6. `journal_feedback_contract_ready` + Every model failure must map back to journal and study-note error classes. + +## Promotion Gate + +The Doctrine Backtest Cohort now blocks promotion unless: + +- D2 playbook schema is complete, +- D3 Liquidity Kernel passes, +- D4 PD Array / Reaction Kernel passes, +- D5 Time / Session Kernel passes, +- D6 Model Playbook Kernel passes, +- Strategy Backtest quality gate passes, +- active Trading Plan gate passes. + +## Read-Only Boundary + +D6 does not add order submission, does not change `execution_allowed`, does not change `decision_support_ready`, does not change `real_live_ready`, and does not loosen risk governor / trust bucket / operator confirmation protocol. + +## Next Slice + +D7 should rebuild the trader-facing product gate: only playbook-mapped, backtest-supported, plan-approved setups can re-enter Trader Cockpit focus; all other evidence should remain under Strategy Backtest / Trading Plan / Journal / System Ops as study material. diff --git a/output/playwright/d6-model-playbook-strategy-backtest-fullpage-zh.png b/output/playwright/d6-model-playbook-strategy-backtest-fullpage-zh.png new file mode 100644 index 0000000..725e936 Binary files /dev/null and b/output/playwright/d6-model-playbook-strategy-backtest-fullpage-zh.png differ diff --git a/src/api/backtest.py b/src/api/backtest.py index de81e4f..af4b03f 100644 --- a/src/api/backtest.py +++ b/src/api/backtest.py @@ -8,17 +8,20 @@ from src.services.backtest import evaluate_backtest_quality from src.services.ict_mainline import ( build_doctrine_backtest_cohort, build_liquidity_backtest_features, + build_model_playbook_backtest_features, build_pd_array_backtest_features, build_time_session_backtest_features, build_backtest_quality_gate, build_ict_rulebook_mapping, evaluate_doctrine_research_status, evaluate_liquidity_kernel_status, + evaluate_model_playbook_kernel_status, evaluate_pd_array_reaction_status, evaluate_time_session_kernel_status, summarize_doctrine_backtest_cohorts, summarize_doctrine_rule_gaps, summarize_liquidity_kernel_statuses, + summarize_model_playbook_kernel_statuses, summarize_pd_array_reaction_statuses, summarize_time_session_kernel_statuses, ) @@ -45,6 +48,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in liquidity_kernel_summary = summarize_liquidity_kernel_statuses(items) pd_array_reaction_summary = summarize_pd_array_reaction_statuses(items) time_session_kernel_summary = summarize_time_session_kernel_statuses(items) + model_playbook_kernel_summary = summarize_model_playbook_kernel_statuses(items) return { "instrument_id": instrument_id, "item_count": len(items), @@ -57,6 +61,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in "liquidity_kernel_summary": liquidity_kernel_summary, "pd_array_reaction_summary": pd_array_reaction_summary, "time_session_kernel_summary": time_session_kernel_summary, + "model_playbook_kernel_summary": model_playbook_kernel_summary, "ict_rulebook_mapping": ict_rulebook_mapping, "items": items, } @@ -183,6 +188,25 @@ def normalize_backtest_summary_row(row: dict) -> dict: "time_session_backtest_features": time_session_backtest_features, } ) + model_playbook_backtest_features = build_model_playbook_backtest_features( + { + "subject_type": row.get("subject_type"), + "model_code": row.get("model_code"), + "candidate_model_code": row.get("candidate_model_code"), + "config": config, + "summary": summary, + } + ) + model_playbook_kernel_status = evaluate_model_playbook_kernel_status( + { + "subject_type": row.get("subject_type"), + "model_code": row.get("model_code"), + "candidate_model_code": row.get("candidate_model_code"), + "config": config, + "summary": summary, + "model_playbook_backtest_features": model_playbook_backtest_features, + } + ) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { "subject_type": row.get("subject_type"), @@ -194,6 +218,7 @@ def normalize_backtest_summary_row(row: dict) -> dict: "liquidity_backtest_features": liquidity_backtest_features, "pd_array_backtest_features": pd_array_backtest_features, "time_session_backtest_features": time_session_backtest_features, + "model_playbook_backtest_features": model_playbook_backtest_features, }, quality_gate=dict(summary.get("quality_gate") or {}), ) @@ -210,6 +235,8 @@ def normalize_backtest_summary_row(row: dict) -> dict: "pd_array_reaction_status": pd_array_reaction_status, "time_session_backtest_features": time_session_backtest_features, "time_session_kernel_status": time_session_kernel_status, + "model_playbook_backtest_features": model_playbook_backtest_features, + "model_playbook_kernel_status": model_playbook_kernel_status, "doctrine_backtest_cohort": doctrine_backtest_cohort, } @@ -261,6 +288,13 @@ def _build_operator_summary(latest: Optional[dict]) -> dict: status = "time_session_kernel_blocked" decision = "do_not_promote_strategy" reason = str(time_session_reasons[0]) + model_playbook_status = dict(latest.get("model_playbook_kernel_status") or {}) + if not model_playbook_status.get("passed"): + model_playbook_reasons = list(model_playbook_status.get("reasons") or []) + if model_playbook_reasons: + status = "model_playbook_kernel_blocked" + decision = "do_not_promote_strategy" + reason = str(model_playbook_reasons[0]) if not (doctrine_cohort.get("promotion_gate") or {}).get("passed"): cohort_reasons = list((doctrine_cohort.get("promotion_gate") or {}).get("reasons") or []) if cohort_reasons: diff --git a/src/api/server.py b/src/api/server.py index 70c1f26..2566a3e 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -126,6 +126,7 @@ from src.services.ict_mainline import ( build_doctrine_playbook_registry, build_doctrine_rule_matrix, build_liquidity_kernel_registry, + build_model_playbook_kernel_registry, build_pd_array_reaction_registry, build_time_session_kernel_registry, ) @@ -233,6 +234,7 @@ def build_api_dependencies() -> dict: "trading_liquidity_kernel": build_liquidity_kernel_registry, "trading_pd_array_kernel": build_pd_array_reaction_registry, "trading_time_session_kernel": build_time_session_kernel_registry, + "trading_model_playbook_kernel": build_model_playbook_kernel_registry, "training_cases": get_training_cases, "training_case": get_training_case, "training_export_cases": export_training_case_package, @@ -864,6 +866,8 @@ def route_request( return 200, deps["trading_pd_array_kernel"]() if method == "GET" and path == "/trading/time-session-kernel": return 200, deps["trading_time_session_kernel"]() + if method == "GET" and path == "/trading/model-playbook-kernel": + return 200, deps["trading_model_playbook_kernel"]() if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "candidates"]: candidate = deps["trading_candidate_by_id"](_parse_path_int(path_parts[2], "candidate_id")) if candidate is None: diff --git a/src/services/backtest/backtest_service.py b/src/services/backtest/backtest_service.py index faf4f11..375974f 100644 --- a/src/services/backtest/backtest_service.py +++ b/src/services/backtest/backtest_service.py @@ -7,9 +7,11 @@ from src.services.backtest.replay import ReplaySwing, iter_historical_replay from src.services.ict_mainline import ( build_doctrine_backtest_cohort, build_liquidity_backtest_features, + build_model_playbook_backtest_features, build_pd_array_backtest_features, build_time_session_backtest_features, evaluate_liquidity_kernel_status, + evaluate_model_playbook_kernel_status, evaluate_pd_array_reaction_status, evaluate_time_session_kernel_status, extract_setup_playbook_id, @@ -167,6 +169,17 @@ class BacktestService: 'active_trading_plan': active_trading_plan, } ) + model_playbook_backtest_features = build_model_playbook_backtest_features( + { + 'subject_type': subject_type, + 'model_code': 'SWEEP_MSS_FVG', + 'candidate_model_code': candidate_model_code, + 'timeframe_set': timeframe_set, + 'session_codes': list(session_codes or []), + 'run_metadata': run_metadata, + 'active_trading_plan': active_trading_plan, + } + ) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -177,6 +190,7 @@ class BacktestService: 'liquidity_backtest_features': liquidity_backtest_features, 'pd_array_backtest_features': pd_array_backtest_features, 'time_session_backtest_features': time_session_backtest_features, + 'model_playbook_backtest_features': model_playbook_backtest_features, } ) run_id = self.repository.insert_backtest_run( @@ -205,6 +219,8 @@ class BacktestService: 'pd_array_reaction_status': evaluate_pd_array_reaction_status(pd_array_backtest_features), 'time_session_backtest_features': time_session_backtest_features, 'time_session_kernel_status': evaluate_time_session_kernel_status(time_session_backtest_features), + 'model_playbook_backtest_features': model_playbook_backtest_features, + 'model_playbook_kernel_status': evaluate_model_playbook_kernel_status(model_playbook_backtest_features), 'doctrine_backtest_cohort': doctrine_backtest_cohort, 'run_metadata': run_metadata, }, @@ -227,12 +243,15 @@ class BacktestService: outcome_liquidity_features = _first_outcome_meta_value(outcomes, 'liquidity_backtest_features') or liquidity_backtest_features outcome_pd_array_features = _first_outcome_meta_value(outcomes, 'pd_array_backtest_features') or pd_array_backtest_features outcome_time_session_features = _first_outcome_meta_value(outcomes, 'time_session_backtest_features') or time_session_backtest_features + outcome_model_playbook_features = _first_outcome_meta_value(outcomes, 'model_playbook_backtest_features') or model_playbook_backtest_features summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {}) summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features']) summary['pd_array_backtest_features'] = dict(outcome_pd_array_features or {}) summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features']) summary['time_session_backtest_features'] = dict(outcome_time_session_features or {}) summary['time_session_kernel_status'] = evaluate_time_session_kernel_status(summary['time_session_backtest_features']) + summary['model_playbook_backtest_features'] = dict(outcome_model_playbook_features or {}) + summary['model_playbook_kernel_status'] = evaluate_model_playbook_kernel_status(summary['model_playbook_backtest_features']) summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -244,6 +263,7 @@ class BacktestService: 'liquidity_backtest_features': summary['liquidity_backtest_features'], 'pd_array_backtest_features': summary['pd_array_backtest_features'], 'time_session_backtest_features': summary['time_session_backtest_features'], + 'model_playbook_backtest_features': summary['model_playbook_backtest_features'], }, quality_gate=summary['quality_gate'], ) @@ -297,6 +317,15 @@ class BacktestService: 'session_filter': 'outside_window' if len(sessions) == 0 else 'covered', } ) + signal_model_playbook_features = build_model_playbook_backtest_features( + { + 'signal': signal, + 'target_plan': target_plan, + 'run_metadata': run_metadata, + 'timeframe_set': timeframe_set, + 'session_codes': session_codes, + } + ) error_tags = ['no_future_candles'] if len(sessions) == 0: error_tags.append('outside_session_window') @@ -339,6 +368,8 @@ class BacktestService: 'pd_array_reaction_status': evaluate_pd_array_reaction_status(signal_pd_array_features), 'time_session_backtest_features': signal_time_session_features, 'time_session_kernel_status': evaluate_time_session_kernel_status(signal_time_session_features), + 'model_playbook_backtest_features': signal_model_playbook_features, + 'model_playbook_kernel_status': evaluate_model_playbook_kernel_status(signal_model_playbook_features), 'cost_r': 0.0, 'cost_model': cost_config.__dict__, 'backtest_mode': mode, @@ -438,12 +469,22 @@ class BacktestService: 'session_filter': 'outside_window' if session_filtered else 'covered', } ), + 'model_playbook_backtest_features': build_model_playbook_backtest_features( + { + 'signal': signal, + 'target_plan': extract_target_plan_from_signal_row(signal), + 'run_metadata': run_metadata, + 'timeframe_set': timeframe_set, + 'session_codes': session_codes, + } + ), 'weekday': _extract_weekday(signal['created_ts']), } ) record.meta['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(record.meta['liquidity_backtest_features']) record.meta['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(record.meta['pd_array_backtest_features']) record.meta['time_session_kernel_status'] = evaluate_time_session_kernel_status(record.meta['time_session_backtest_features']) + record.meta['model_playbook_kernel_status'] = evaluate_model_playbook_kernel_status(record.meta['model_playbook_backtest_features']) outcomes.append(record) self.repository.insert_backtest_result( backtest_run_id=run_id, @@ -465,12 +506,15 @@ class BacktestService: outcome_liquidity_features = _first_outcome_meta_value(outcomes, 'liquidity_backtest_features') or liquidity_backtest_features outcome_pd_array_features = _first_outcome_meta_value(outcomes, 'pd_array_backtest_features') or pd_array_backtest_features outcome_time_session_features = _first_outcome_meta_value(outcomes, 'time_session_backtest_features') or time_session_backtest_features + outcome_model_playbook_features = _first_outcome_meta_value(outcomes, 'model_playbook_backtest_features') or model_playbook_backtest_features summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {}) summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features']) summary['pd_array_backtest_features'] = dict(outcome_pd_array_features or {}) summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features']) summary['time_session_backtest_features'] = dict(outcome_time_session_features or {}) summary['time_session_kernel_status'] = evaluate_time_session_kernel_status(summary['time_session_backtest_features']) + summary['model_playbook_backtest_features'] = dict(outcome_model_playbook_features or {}) + summary['model_playbook_kernel_status'] = evaluate_model_playbook_kernel_status(summary['model_playbook_backtest_features']) summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -482,6 +526,7 @@ class BacktestService: 'liquidity_backtest_features': summary['liquidity_backtest_features'], 'pd_array_backtest_features': summary['pd_array_backtest_features'], 'time_session_backtest_features': summary['time_session_backtest_features'], + 'model_playbook_backtest_features': summary['model_playbook_backtest_features'], }, quality_gate=summary['quality_gate'], ) @@ -1072,6 +1117,22 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: } ) time_session_kernel_status = evaluate_time_session_kernel_status(time_session_backtest_features) + model_playbook_backtest_features = build_model_playbook_backtest_features( + { + 'first_candidate': first, + 'latest_candidate': latest, + 'confirmed_candidate': confirmed_row or {}, + 'entry_payload': entry_payload, + 'target_plan': target_plan, + 'setup_playbook_id': setup_playbook_id, + 'candidate_model_code': first.get('candidate_model_code'), + 'timeframe_set': { + 'setup_tf': first.get('setup_timeframe'), + 'trigger_tf': first.get('trigger_timeframe'), + }, + } + ) + model_playbook_kernel_status = evaluate_model_playbook_kernel_status(model_playbook_backtest_features) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { 'setup_playbook_id': setup_playbook_id, @@ -1083,6 +1144,7 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: 'liquidity_backtest_features': liquidity_backtest_features, 'pd_array_backtest_features': pd_array_backtest_features, 'time_session_backtest_features': time_session_backtest_features, + 'model_playbook_backtest_features': model_playbook_backtest_features, } ) units.append( @@ -1097,6 +1159,8 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: 'pd_array_reaction_status': pd_array_reaction_status, 'time_session_backtest_features': time_session_backtest_features, 'time_session_kernel_status': time_session_kernel_status, + 'model_playbook_backtest_features': model_playbook_backtest_features, + 'model_playbook_kernel_status': model_playbook_kernel_status, 'candidate_model_code': first.get('candidate_model_code'), 'side': first.get('side'), 'session_code': first.get('session_code'), @@ -1190,6 +1254,14 @@ def build_plan_replay_meta( } ) time_session_kernel_status = evaluate_time_session_kernel_status(time_session_backtest_features) + model_playbook_backtest_features = build_model_playbook_backtest_features( + { + 'unit': unit, + 'model_playbook_backtest_features': dict(unit.get('model_playbook_backtest_features') or {}), + 'timeframe_set': timeframe_set, + } + ) + model_playbook_kernel_status = evaluate_model_playbook_kernel_status(model_playbook_backtest_features) return { 'subject_type': BACKTEST_SUBJECT_PLAN_REPLAY, 'trading_plan_id': unit.get('trading_plan_id'), @@ -1203,6 +1275,8 @@ def build_plan_replay_meta( 'pd_array_reaction_status': dict(unit.get('pd_array_reaction_status') or {}), 'time_session_backtest_features': time_session_backtest_features, 'time_session_kernel_status': time_session_kernel_status, + 'model_playbook_backtest_features': model_playbook_backtest_features, + 'model_playbook_kernel_status': model_playbook_kernel_status, 'plan_activated_ts': _normalize_meta_ts(unit.get('plan_activated_ts')), 'plan_created_ts': _normalize_meta_ts(unit.get('plan_created_ts')), 'candidate_id': unit.get('candidate_id'), @@ -1392,6 +1466,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s grouped_by_liquidity_target_side: dict[str, dict] = {} grouped_by_pd_array_type: dict[str, dict] = {} grouped_by_time_session_code: dict[str, dict] = {} + grouped_by_model_playbook: dict[str, dict] = {} r_multiples: list[float] = [] cost_values: list[float] = [] mfe_r_values: list[float] = [] @@ -1408,6 +1483,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s pd_array_reaction_status_counts: dict[str, int] = {} time_session_kernel_reason_counts: dict[str, int] = {} time_session_kernel_status_counts: dict[str, int] = {} + model_playbook_kernel_reason_counts: dict[str, int] = {} + model_playbook_kernel_status_counts: dict[str, int] = {} plan_total_r: dict[int, float] = {} plan_seen: set[int] = set() @@ -1464,6 +1541,13 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s time_session_kernel_status_counts[time_session_status_key] = time_session_kernel_status_counts.get(time_session_status_key, 0) + 1 for reason in list(time_session_status.get('reasons') or []): time_session_kernel_reason_counts[str(reason)] = time_session_kernel_reason_counts.get(str(reason), 0) + 1 + model_playbook_features = dict(outcome.meta.get('model_playbook_backtest_features') or {}) + model_playbook_id = str(model_playbook_features.get('model_playbook_id') or outcome.meta.get('setup_playbook_id') or 'missing_model_playbook') + model_playbook_status = dict(outcome.meta.get('model_playbook_kernel_status') or evaluate_model_playbook_kernel_status(model_playbook_features)) + model_playbook_status_key = str(model_playbook_status.get('status') or 'unknown') + model_playbook_kernel_status_counts[model_playbook_status_key] = model_playbook_kernel_status_counts.get(model_playbook_status_key, 0) + 1 + for reason in list(model_playbook_status.get('reasons') or []): + model_playbook_kernel_reason_counts[str(reason)] = model_playbook_kernel_reason_counts.get(str(reason), 0) + 1 r_multiples.append(float(outcome.meta.get('r_multiple') or 0.0)) cost_values.append(float(outcome.meta.get('cost_r') or 0.0)) mfe_r_values.append(float(outcome.meta.get('mfe_r') or 0.0)) @@ -1477,6 +1561,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s _accumulate_bucket(grouped_by_liquidity_target_side, liquidity_target_side, outcome) _accumulate_bucket(grouped_by_pd_array_type, pd_array_type, outcome) _accumulate_bucket(grouped_by_time_session_code, time_session_code, outcome) + _accumulate_bucket(grouped_by_model_playbook, model_playbook_id, outcome) for session_code in session_codes: _accumulate_bucket(grouped_by_session, str(session_code), outcome) @@ -1513,6 +1598,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s 'pd_array_reaction_reason_counts': pd_array_reaction_reason_counts, 'time_session_kernel_status_counts': time_session_kernel_status_counts, 'time_session_kernel_reason_counts': time_session_kernel_reason_counts, + 'model_playbook_kernel_status_counts': model_playbook_kernel_status_counts, + 'model_playbook_kernel_reason_counts': model_playbook_kernel_reason_counts, 'avg_mfe': _average([outcome.mfe for outcome in outcomes]), 'avg_mae': _average([outcome.mae for outcome in outcomes]), 'avg_mfe_r': _average(mfe_r_values), @@ -1534,6 +1621,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s 'by_liquidity_target_side': grouped_by_liquidity_target_side, 'by_pd_array_type': grouped_by_pd_array_type, 'by_time_session_code': grouped_by_time_session_code, + 'by_model_playbook': grouped_by_model_playbook, } result['session_performance'] = result['by_session'] return result diff --git a/src/services/ict_mainline/__init__.py b/src/services/ict_mainline/__init__.py index dc534ef..efbade2 100644 --- a/src/services/ict_mainline/__init__.py +++ b/src/services/ict_mainline/__init__.py @@ -20,21 +20,25 @@ from src.services.ict_mainline.feedback import ( from src.services.ict_mainline.doctrine import ( DOCTRINE_KERNEL_PHASE, DOCTRINE_LIQUIDITY_PHASE, + DOCTRINE_MODEL_PLAYBOOK_PHASE, DOCTRINE_PLAYBOOK_PHASE, DOCTRINE_REACTION_PHASE, DOCTRINE_SESSION_PHASE, build_doctrine_backtest_cohort, build_liquidity_backtest_features, + build_model_playbook_backtest_features, build_pd_array_backtest_features, build_time_session_backtest_features, build_doctrine_rule_matrix, build_doctrine_playbook_gate, build_doctrine_playbook_registry, build_liquidity_kernel_registry, + build_model_playbook_kernel_registry, build_pd_array_reaction_registry, build_time_session_kernel_registry, evaluate_doctrine_research_status, evaluate_liquidity_kernel_status, + evaluate_model_playbook_kernel_status, evaluate_pd_array_reaction_status, evaluate_time_session_kernel_status, extract_setup_playbook_id, @@ -42,11 +46,13 @@ from src.services.ict_mainline.doctrine import ( list_doctrine_setup_playbooks, list_doctrine_rules, list_liquidity_rules, + list_model_playbook_rules, list_pd_array_rules, list_time_session_rules, summarize_doctrine_backtest_cohorts, summarize_doctrine_rule_gaps, summarize_liquidity_kernel_statuses, + summarize_model_playbook_kernel_statuses, summarize_pd_array_reaction_statuses, summarize_time_session_kernel_statuses, ) @@ -54,18 +60,21 @@ from src.services.ict_mainline.doctrine import ( __all__ = [ "DOCTRINE_KERNEL_PHASE", "DOCTRINE_LIQUIDITY_PHASE", + "DOCTRINE_MODEL_PLAYBOOK_PHASE", "DOCTRINE_PLAYBOOK_PHASE", "DOCTRINE_REACTION_PHASE", "DOCTRINE_SESSION_PHASE", "ICT_MAINLINE_CHAIN", "build_doctrine_backtest_cohort", "build_liquidity_backtest_features", + "build_model_playbook_backtest_features", "build_pd_array_backtest_features", "build_time_session_backtest_features", "build_doctrine_rule_matrix", "build_doctrine_playbook_gate", "build_doctrine_playbook_registry", "build_liquidity_kernel_registry", + "build_model_playbook_kernel_registry", "build_pd_array_reaction_registry", "build_time_session_kernel_registry", "build_ict_rulebook_mapping", @@ -79,6 +88,7 @@ __all__ = [ "classify_ict_concepts", "evaluate_doctrine_research_status", "evaluate_liquidity_kernel_status", + "evaluate_model_playbook_kernel_status", "evaluate_pd_array_reaction_status", "evaluate_time_session_kernel_status", "extract_setup_playbook_id", @@ -87,12 +97,14 @@ __all__ = [ "list_doctrine_setup_playbooks", "list_doctrine_rules", "list_liquidity_rules", + "list_model_playbook_rules", "list_pd_array_rules", "list_time_session_rules", "list_ict_concepts", "summarize_doctrine_backtest_cohorts", "summarize_doctrine_rule_gaps", "summarize_liquidity_kernel_statuses", + "summarize_model_playbook_kernel_statuses", "summarize_pd_array_reaction_statuses", "summarize_time_session_kernel_statuses", "summarize_book_rule_errors", diff --git a/src/services/ict_mainline/doctrine.py b/src/services/ict_mainline/doctrine.py index 9f5b659..26aca02 100644 --- a/src/services/ict_mainline/doctrine.py +++ b/src/services/ict_mainline/doctrine.py @@ -11,6 +11,7 @@ DOCTRINE_PLAYBOOK_PHASE = "D2_PLAYBOOK_BACKTEST_COHORT" DOCTRINE_LIQUIDITY_PHASE = "D3_LIQUIDITY_KERNEL" DOCTRINE_REACTION_PHASE = "D4_PD_ARRAY_REACTION_KERNEL" DOCTRINE_SESSION_PHASE = "D5_TIME_SESSION_KERNEL" +DOCTRINE_MODEL_PLAYBOOK_PHASE = "D6_MODEL_PLAYBOOK_KERNEL" @dataclass(frozen=True) @@ -163,6 +164,34 @@ class TimeSessionRule: return payload +@dataclass(frozen=True) +class ModelPlaybookRule: + rule_id: str + stage: str + book_anchor: str + model_family: str + ranking_weight: int + required_inputs: tuple[str, ...] + measurable_fields: tuple[str, ...] + no_trade_when: tuple[str, ...] + wait_when: tuple[str, ...] + backtest_observable: str + operator_rule: str + aliases: tuple[str, ...] + + def to_public_dict(self) -> dict: + payload = asdict(self) + for key in ( + "required_inputs", + "measurable_fields", + "no_trade_when", + "wait_when", + "aliases", + ): + payload[key] = list(payload[key]) + return payload + + D1_DOCTRINE_RULES: tuple[DoctrineRule, ...] = ( DoctrineRule( rule_id="range_defined", @@ -796,6 +825,121 @@ D5_TIME_SESSION_RULES: tuple[TimeSessionRule, ...] = ( ) +D6_MODEL_PLAYBOOK_RULES: tuple[ModelPlaybookRule, ...] = ( + ModelPlaybookRule( + rule_id="model_family_declared", + stage="Model Identity", + book_anchor="ICT trading book models: continuation, reversal, sweep-to-displacement, HTF draw pullback, London/NY model", + model_family="book_derived_model", + ranking_weight=100, + required_inputs=("setup_playbook_id", "model_family", "model_variant"), + measurable_fields=("model_playbook_id", "model_family", "model_variant", "model_stage"), + no_trade_when=("missing_model_playbook", "unknown_model_playbook", "missing_model_family"), + wait_when=(), + backtest_observable="run_without_registered_model_playbook", + operator_rule="A model must point to a registered book-derived playbook before any backtest result can be trusted.", + aliases=("model_playbook_id", "setup_playbook_id", "model_family", "model_variant", "setup_family"), + ), + ModelPlaybookRule( + rule_id="full_chain_contract_complete", + stage="Full ICT Chain", + book_anchor="ICT trading book full setup chain: Range/Draw/Liquidity/PD Array/Session/Confirmation/Invalidation/Target", + model_family="full_chain_contract", + ranking_weight=95, + required_inputs=( + "range_contract", + "draw_contract", + "liquidity_contract", + "pd_array_contract", + "session_contract", + "confirmation_contract", + "invalidation_contract", + "target_contract", + ), + measurable_fields=( + "range_contract", + "draw_contract", + "liquidity_contract", + "pd_array_contract", + "session_contract", + "confirmation_contract", + "invalidation_contract", + "target_contract", + ), + no_trade_when=( + "missing_model_range_contract", + "missing_model_draw_contract", + "missing_model_liquidity_contract", + "missing_model_pd_array_contract", + "missing_model_session_contract", + "missing_model_confirmation_contract", + "missing_model_invalidation_contract", + "missing_model_target_contract", + ), + wait_when=(), + backtest_observable="model_playbook_without_full_chain_contract", + operator_rule="A playbook must define every upstream and downstream contract; no single shape is a setup by itself.", + aliases=("range_definition", "draw_rule", "liquidity_event_rule", "pd_array_rule", "session_timing_rule", "target_rule"), + ), + ModelPlaybookRule( + rule_id="confirmation_trigger_bound", + stage="Confirmation Trigger", + book_anchor="ICT trading book structure confirmation: displacement, MSS, FVG, and retest must serve the Draw", + model_family="confirmation_chain", + ranking_weight=90, + required_inputs=("confirmation_contract", "displacement_contract", "mss_contract", "fvg_retest_contract"), + measurable_fields=("confirmation_contract", "displacement_contract", "mss_contract", "fvg_retest_contract"), + no_trade_when=("missing_model_confirmation_contract",), + wait_when=("missing_model_trigger_contract",), + backtest_observable="model_without_confirmation_trigger_contract", + operator_rule="The model must state what confirms the setup and what is still only a wait condition.", + aliases=("confirmation_rule", "displacement_structure_rule", "mss", "fvg_retest", "trigger_contract"), + ), + ModelPlaybookRule( + rule_id="risk_target_contract_bound", + stage="Risk / Target", + book_anchor="ICT trading book invalidation and IRL/ERL target architecture", + model_family="risk_target_contract", + ranking_weight=90, + required_inputs=("invalidation_contract", "target_contract", "management_contract"), + measurable_fields=("invalidation_contract", "target_contract", "management_contract"), + no_trade_when=("missing_model_invalidation_contract", "missing_model_target_contract"), + wait_when=("missing_model_management_contract",), + backtest_observable="model_without_risk_target_contract", + operator_rule="The playbook must define invalidation and IRL/ERL targets before any historical result can become live focus evidence.", + aliases=("invalidation_rule", "target_rule", "management_rules", "irl", "erl"), + ), + ModelPlaybookRule( + rule_id="batch_cohort_contract_ready", + stage="Backtest Cohort", + book_anchor="ICT trading book study/backtesting: test the exact model, session, timeframe, and context cohort", + model_family="batch_backtest_cohort", + ranking_weight=85, + required_inputs=("backtest_cohort_family", "cohort_key_fields", "sample_basis", "quality_gate_contract"), + measurable_fields=("backtest_cohort_family", "cohort_key_fields", "sample_basis", "quality_gate_contract"), + no_trade_when=("missing_model_backtest_cohort",), + wait_when=("missing_model_quality_gate_contract",), + backtest_observable="model_without_batch_backtest_cohort", + operator_rule="Backtest evidence must be grouped by playbook cohort rather than raw model_code.", + aliases=("backtest_cohort", "cohort_family", "cohort_key_fields", "sample_basis", "quality_gate"), + ), + ModelPlaybookRule( + rule_id="journal_feedback_contract_ready", + stage="Journal / Study Notes", + book_anchor="ICT trading book study notes and journal feedback loop", + model_family="feedback_contract", + ranking_weight=75, + required_inputs=("journal_error_classes", "study_note_contract", "operator_runbook"), + measurable_fields=("journal_error_classes", "study_note_contract", "operator_runbook"), + no_trade_when=("missing_model_journal_feedback",), + wait_when=("missing_model_operator_runbook",), + backtest_observable="model_without_journal_feedback_contract", + operator_rule="Every model failure must map back to journal and study-note error classes.", + aliases=("journal_error_classes", "study_notes", "operator_runbook", "cockpit_sentence"), + ), +) + + def list_doctrine_rules() -> list[dict]: return [rule.to_public_dict() for rule in D1_DOCTRINE_RULES] @@ -846,6 +990,10 @@ def list_time_session_rules() -> list[dict]: return [rule.to_public_dict() for rule in D5_TIME_SESSION_RULES] +def list_model_playbook_rules() -> list[dict]: + return [rule.to_public_dict() for rule in D6_MODEL_PLAYBOOK_RULES] + + def build_liquidity_kernel_registry() -> dict: return { "phase": DOCTRINE_LIQUIDITY_PHASE, @@ -915,6 +1063,31 @@ def build_time_session_kernel_registry() -> dict: } +def build_model_playbook_kernel_registry() -> dict: + return { + "phase": DOCTRINE_MODEL_PLAYBOOK_PHASE, + "source": "docs/ICT交易全书_出版终校版.md model playbook chapters and docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md", + "status": "active", + "rule_count": len(D6_MODEL_PLAYBOOK_RULES), + "registered_playbook_count": len(D2_DOCTRINE_SETUP_PLAYBOOKS), + "hard_rules": [ + "Raw model_code is not a tradable setup.", + "Every model must resolve into a registered setup playbook before promotion.", + "A playbook must include Range, Draw, Liquidity, PD Array, Session, Confirmation, Invalidation, and Target contracts.", + "Batch backtest cohorts must be playbook-based, not shape-label based.", + "Journal and study-note errors must map back to the playbook rules.", + ], + "required_backtest_feature_fields": _dedupe( + field + for rule in D6_MODEL_PLAYBOOK_RULES + for field in rule.measurable_fields + ), + "rules": list_model_playbook_rules(), + "registered_playbooks": list_doctrine_setup_playbooks(), + "execution_boundary": "read_only_analysis_no_order_submission", + } + + def get_doctrine_setup_playbook(setup_playbook_id: str | None) -> dict | None: key = str(setup_playbook_id or "").strip() if not key: @@ -1001,6 +1174,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None liquidity_status = evaluate_liquidity_kernel_status(payload) pd_array_status = evaluate_pd_array_reaction_status(payload) time_session_status = evaluate_time_session_kernel_status(payload) + model_playbook_status = evaluate_model_playbook_kernel_status(payload) quality = dict(quality_gate or _extract_quality_gate(payload) or {}) quality_reasons = [str(item) for item in list(quality.get("reasons") or []) if item] quality_passed = bool(quality.get("passed")) @@ -1016,6 +1190,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None *([] if liquidity_status.get("passed") else [str(item) for item in list(liquidity_status.get("reasons") or []) if item]), *([] if pd_array_status.get("passed") else [str(item) for item in list(pd_array_status.get("reasons") or []) if item]), *([] if time_session_status.get("passed") else [str(item) for item in list(time_session_status.get("reasons") or []) if item]), + *([] if model_playbook_status.get("passed") else [str(item) for item in list(model_playbook_status.get("reasons") or []) if item]), *([] if quality_passed else quality_reasons), ] promotion_reasons = _dedupe(promotion_reasons) @@ -1035,6 +1210,8 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None "pd_array_backtest_features": dict(pd_array_status.get("features") or {}), "time_session_kernel_status": time_session_status, "time_session_backtest_features": dict(time_session_status.get("features") or {}), + "model_playbook_kernel_status": model_playbook_status, + "model_playbook_backtest_features": dict(model_playbook_status.get("features") or {}), "promotion_gate": { "passed": not promotion_reasons, "reasons": promotion_reasons, @@ -1042,6 +1219,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None "requires_liquidity_kernel_complete": True, "requires_pd_array_reaction_complete": True, "requires_time_session_kernel_complete": True, + "requires_model_playbook_kernel_complete": True, "requires_backtest_quality_gate": True, "requires_active_trading_plan": True, "execution_boundary": "read_only_analysis_no_order_submission", @@ -1072,6 +1250,8 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: "promotion_blocker_counts": {}, "liquidity_status_counts": {}, "pd_array_status_counts": {}, + "time_session_status_counts": {}, + "model_playbook_status_counts": {}, }, ) bucket["run_count"] += 1 @@ -1083,6 +1263,10 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: bucket["liquidity_status_counts"][liquidity_status] = int(bucket["liquidity_status_counts"].get(liquidity_status) or 0) + 1 pd_array_status = str((cohort.get("pd_array_reaction_status") or {}).get("status") or "unknown") bucket["pd_array_status_counts"][pd_array_status] = int(bucket["pd_array_status_counts"].get(pd_array_status) or 0) + 1 + time_session_status = str((cohort.get("time_session_kernel_status") or {}).get("status") or "unknown") + bucket["time_session_status_counts"][time_session_status] = int(bucket["time_session_status_counts"].get(time_session_status) or 0) + 1 + model_playbook_status = str((cohort.get("model_playbook_kernel_status") or {}).get("status") or "unknown") + bucket["model_playbook_status_counts"][model_playbook_status] = int(bucket["model_playbook_status_counts"].get(model_playbook_status) or 0) + 1 for reason in list((cohort.get("promotion_gate") or {}).get("reasons") or []): reason_key = str(reason) bucket["promotion_blocker_counts"][reason_key] = int(bucket["promotion_blocker_counts"].get(reason_key) or 0) + 1 @@ -1651,6 +1835,225 @@ def summarize_time_session_kernel_statuses(items: Iterable[dict]) -> dict: } +def build_model_playbook_backtest_features(payload: object) -> dict: + model_playbook_id = _string_or_empty( + _extract_first_present_value( + payload, + keys=("model_playbook_id", "setup_playbook_id", "doctrine_setup_id", "playbook_id"), + ) + ) + playbook = get_doctrine_setup_playbook(model_playbook_id) + playbook_backtest_cohort = dict((playbook or {}).get("backtest_cohort") or {}) + playbook_promotion_gate = dict((playbook or {}).get("promotion_gate") or {}) + model_family = _string_or_empty( + _extract_first_present_value( + payload, + keys=("model_family", "playbook_model_family", "setup_family", "cohort_family"), + ) + or playbook_backtest_cohort.get("cohort_family") + ) + model_variant = _string_or_empty( + _extract_first_present_value(payload, keys=("model_variant", "setup_variant", "model_name")) + or (playbook or {}).get("setup_name") + ) + cohort_key_fields = _as_string_list( + _extract_first_present_value(payload, keys=("cohort_key_fields",)) + or playbook_backtest_cohort.get("cohort_key_fields") + ) + model_timeframe_contract = _as_string_list( + _extract_first_present_value(payload, keys=("model_timeframe_contract", "timeframe_contract")) + or [field for field in cohort_key_fields if str(field).endswith("_tf")] + ) + journal_error_classes = _as_string_list( + _extract_first_present_value(payload, keys=("journal_error_classes", "journal_errors", "book_rule_error_classes")) + or (playbook or {}).get("journal_error_classes") + ) + target_contract = _contract_fields(payload, playbook, "target_contract", "target_rule") + features = { + "model_playbook_id": model_playbook_id, + "model_playbook_known": bool(playbook), + "model_family": model_family, + "model_variant": model_variant, + "model_stage": _string_or_empty( + _extract_first_present_value(payload, keys=("model_stage", "playbook_stage")) + or ("registered_setup_playbook" if playbook else "") + ), + "model_timeframe_contract": model_timeframe_contract, + "required_rule_ids": _as_string_list((playbook or {}).get("required_rule_ids")), + "range_contract": _contract_fields(payload, playbook, "range_contract", "range_definition"), + "draw_contract": _contract_fields(payload, playbook, "draw_contract", "draw_rule"), + "liquidity_contract": _contract_fields(payload, playbook, "liquidity_contract", "liquidity_event_rule"), + "pd_array_contract": _contract_fields(payload, playbook, "pd_array_contract", "pd_array_rule"), + "session_contract": _contract_fields(payload, playbook, "session_contract", "session_timing_rule"), + "confirmation_contract": _contract_fields(payload, playbook, "confirmation_contract", "confirmation_rule"), + "displacement_contract": _contract_fields(payload, playbook, "displacement_contract", "displacement_structure_rule"), + "mss_contract": _as_string_list( + _extract_first_present_value(payload, keys=("mss_contract",)) + or _contract_fields(payload, playbook, "confirmation_contract", "confirmation_rule") + ), + "fvg_retest_contract": _as_string_list( + _extract_first_present_value(payload, keys=("fvg_retest_contract",)) + or _contract_fields(payload, playbook, "pd_array_contract", "pd_array_rule") + ), + "invalidation_contract": _contract_fields(payload, playbook, "invalidation_contract", "invalidation_rule"), + "target_contract": target_contract, + "management_contract": _as_string_list( + _extract_first_present_value(payload, keys=("management_contract", "management_rules")) + or [field for field in target_contract if "management" in str(field).lower()] + ), + "backtest_cohort_family": _string_or_empty( + _extract_first_present_value(payload, keys=("backtest_cohort_family", "cohort_family")) + or playbook_backtest_cohort.get("cohort_family") + ), + "cohort_key_fields": cohort_key_fields, + "sample_basis": _string_or_empty( + _extract_first_present_value(payload, keys=("sample_basis",)) + or playbook_backtest_cohort.get("sample_basis") + ), + "quality_gate_contract": _as_string_list( + _extract_first_present_value(payload, keys=("quality_gate_contract",)) + or ( + ["requires_backtest_quality_gate"] + if playbook_promotion_gate.get("requires_backtest_quality_gate") + else [] + ) + ), + "journal_error_classes": journal_error_classes, + "study_note_contract": _as_string_list( + _extract_first_present_value(payload, keys=("study_note_contract", "study_notes_contract")) + or journal_error_classes + ), + "operator_runbook": _string_or_empty( + _extract_first_present_value(payload, keys=("operator_runbook", "playbook_operator_runbook")) + or (playbook or {}).get("cockpit_sentence") + ), + } + features["has_full_chain_contracts"] = all( + bool(features.get(key)) + for key in ( + "range_contract", + "draw_contract", + "liquidity_contract", + "pd_array_contract", + "session_contract", + "confirmation_contract", + "invalidation_contract", + "target_contract", + ) + ) + features["has_batch_backtest_cohort"] = bool( + features["backtest_cohort_family"] and features["cohort_key_fields"] and features["sample_basis"] + ) + features["has_journal_feedback_contract"] = bool(features["journal_error_classes"] and features["study_note_contract"]) + return features + + +def evaluate_model_playbook_kernel_status(payload: object) -> dict: + features = build_model_playbook_backtest_features(payload) + no_trade_reasons: list[str] = [] + wait_reasons: list[str] = [] + + if not features["model_playbook_id"]: + no_trade_reasons.append("missing_model_playbook") + elif not features["model_playbook_known"]: + no_trade_reasons.append("unknown_model_playbook") + + if not features["model_family"]: + no_trade_reasons.append("missing_model_family") + for contract_key, reason in ( + ("range_contract", "missing_model_range_contract"), + ("draw_contract", "missing_model_draw_contract"), + ("liquidity_contract", "missing_model_liquidity_contract"), + ("pd_array_contract", "missing_model_pd_array_contract"), + ("session_contract", "missing_model_session_contract"), + ("confirmation_contract", "missing_model_confirmation_contract"), + ("invalidation_contract", "missing_model_invalidation_contract"), + ("target_contract", "missing_model_target_contract"), + ): + if not features.get(contract_key): + no_trade_reasons.append(reason) + + if not features["model_timeframe_contract"]: + wait_reasons.append("missing_model_timeframe_contract") + if not features["displacement_contract"] or not features["mss_contract"] or not features["fvg_retest_contract"]: + wait_reasons.append("missing_model_trigger_contract") + if not features["management_contract"]: + wait_reasons.append("missing_model_management_contract") + if not features["has_batch_backtest_cohort"]: + no_trade_reasons.append("missing_model_backtest_cohort") + elif not features["quality_gate_contract"]: + wait_reasons.append("missing_model_quality_gate_contract") + if not features["has_journal_feedback_contract"]: + no_trade_reasons.append("missing_model_journal_feedback") + elif not features["operator_runbook"]: + wait_reasons.append("missing_model_operator_runbook") + + reasons = _dedupe([*no_trade_reasons, *wait_reasons]) + passed = not reasons + if passed: + status = "model_playbook_mapped" + elif no_trade_reasons: + status = "study_only" + else: + status = "wait" + return { + "phase": DOCTRINE_MODEL_PLAYBOOK_PHASE, + "status": status, + "passed": passed, + "reason": "" if passed else reasons[0], + "reasons": reasons, + "no_trade_reasons": _dedupe(no_trade_reasons), + "wait_reasons": _dedupe(wait_reasons), + "features": features, + "can_promote_to_playbook": passed, + "operator_summary": ( + "Model resolves to a full book-derived setup playbook with batch cohort and feedback contracts." + if passed + else "Keep this model study-only until it maps to a registered full-chain playbook and cohort contract." + ), + "execution_boundary": "read_only_analysis_no_order_submission", + } + + +def summarize_model_playbook_kernel_statuses(items: Iterable[dict]) -> dict: + reason_counts: dict[str, int] = {} + status_counts: dict[str, int] = {} + playbook_counts: dict[str, int] = {} + model_family_counts: dict[str, int] = {} + total = 0 + passed_count = 0 + for item in items: + total += 1 + status = dict(item.get("model_playbook_kernel_status") or evaluate_model_playbook_kernel_status(item)) + status_key = str(status.get("status") or "unknown") + status_counts[status_key] = int(status_counts.get(status_key) or 0) + 1 + if status.get("passed"): + passed_count += 1 + features = dict(status.get("features") or {}) + playbook_id = str(features.get("model_playbook_id") or "missing_model_playbook") + model_family = str(features.get("model_family") or "missing_model_family") + playbook_counts[playbook_id] = int(playbook_counts.get(playbook_id) or 0) + 1 + model_family_counts[model_family] = int(model_family_counts.get(model_family) or 0) + 1 + for reason in list(status.get("reasons") or []): + key = str(reason) + reason_counts[key] = int(reason_counts.get(key) or 0) + 1 + return { + "phase": DOCTRINE_MODEL_PLAYBOOK_PHASE, + "item_count": total, + "passed_count": passed_count, + "blocked_count": max(0, total - passed_count), + "status_counts": status_counts, + "reason_counts": reason_counts, + "playbook_counts": playbook_counts, + "model_family_counts": model_family_counts, + "operator_summary": ( + "No backtest items available for Model Playbook review." + if not total + else "Review Model Playbook blockers before trusting raw model_code backtests." + ), + } + + def evaluate_doctrine_research_status( payload: object, *, @@ -1823,6 +2226,14 @@ def _missing_playbook_sections(playbook: dict) -> list[str]: return missing +def _contract_fields(payload: object, playbook: dict | None, payload_key: str, playbook_section: str) -> list[str]: + payload_fields = _as_string_list(_extract_first_present_value(payload, keys=(payload_key,))) + if payload_fields: + return payload_fields + section = dict((playbook or {}).get(playbook_section) or {}) + return _as_string_list(section.get("required_fields")) + + def _extract_quality_gate(payload: object) -> dict | None: if isinstance(payload, dict): quality_gate = payload.get("quality_gate") diff --git a/src/web/app.js b/src/web/app.js index d9c239f..a3ae701 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -5561,6 +5561,17 @@ function renderStrategyBacktests(payload) { doctrineCohort.time_session_backtest_features || timeSessionStatus.features || {}; + const modelPlaybookStatus = + latest.model_playbook_kernel_status || + summary.model_playbook_kernel_status || + doctrineCohort.model_playbook_kernel_status || + {}; + const modelPlaybookFeatures = + latest.model_playbook_backtest_features || + summary.model_playbook_backtest_features || + doctrineCohort.model_playbook_backtest_features || + modelPlaybookStatus.features || + {}; strategyBacktestPanel.appendChild( buildInfoGrid([ @@ -5583,6 +5594,10 @@ function renderStrategyBacktests(payload) { ['Time / Session Reason', formatBacktestDecision(timeSessionStatus.reason || 'missing_session_window')], ['Session', timeSessionFeatures.session_code || 'missing_session_window'], ['Killzone', timeSessionFeatures.killzone_name || 'missing_killzone'], + ['Model Playbook Kernel', formatBacktestDecision(modelPlaybookStatus.status || 'study_only')], + ['Model Playbook Reason', formatBacktestDecision(modelPlaybookStatus.reason || 'missing_model_playbook')], + ['Model Playbook', modelPlaybookFeatures.model_playbook_id || 'missing_model_playbook'], + ['Model Family', modelPlaybookFeatures.model_family || 'missing_model_family'], ['Promotion Gate', doctrinePromotionGate.passed ? 'Passed' : 'Blocked'], ['Instrument ID', latest.instrument_id ?? 'n/a'], ['Timeframe', formatJsonInline(latest.timeframe_set || {})], @@ -5632,6 +5647,13 @@ function renderStrategyBacktests(payload) { formatList((timeSessionStatus.reasons || []).map(formatBacktestDecision), 'Time / Session chain mapped') ) ); + cards.appendChild( + buildMetricCard( + 'Model Playbook Kernel', + modelPlaybookStatus.passed ? 'Mapped' : 'Blocked', + formatList((modelPlaybookStatus.reasons || []).map(formatBacktestDecision), 'Model Playbook chain mapped') + ) + ); strategyBacktestPanel.appendChild(cards); strategyBacktestPanel.appendChild(buildBacktestCurve(summary.cumulative_r_curve || [])); @@ -5673,6 +5695,15 @@ function renderStrategyBacktests(payload) { .map((item) => [formatBacktestDecision(item.key), String(item.value)]) ) ); + detailGrid.appendChild( + buildItemCard( + 'Model Playbook Kernel', + `${payload?.model_playbook_kernel_summary?.passed_count ?? 0} mapped / ${payload?.model_playbook_kernel_summary?.blocked_count ?? 0} blocked`, + topCountEntries(payload?.model_playbook_kernel_summary?.reason_counts || summary.model_playbook_kernel_reason_counts || {}) + .slice(0, 5) + .map((item) => [formatBacktestDecision(item.key), String(item.value)]) + ) + ); detailGrid.appendChild( buildItemCard( 'PD Array Kernel', @@ -5753,8 +5784,13 @@ function topCountEntries(countMap) { } function formatBacktestDecision(value) { - const text = String(value || 'n/a').replace(/_/g, ' '); - return text.replace(/\b\w/g, (letter) => letter.toUpperCase()); + const raw = String(value || 'n/a'); + const translate = window.I18N?.t; + const translatedRaw = typeof translate === 'function' ? translate(raw) : raw; + if (translatedRaw !== raw) return translatedRaw; + const text = raw.replace(/_/g, ' '); + const title = text.replace(/\b\w/g, (letter) => letter.toUpperCase()); + return typeof translate === 'function' ? translate(title) : title; } function renderLiveReadiness(payload) { diff --git a/src/web/i18n.js b/src/web/i18n.js index 487ef4c..da74f1f 100644 --- a/src/web/i18n.js +++ b/src/web/i18n.js @@ -90,6 +90,11 @@ 'Time / Session Kernel': '时间 / 时段内核', 'Time / Session Reason': '时间 / 时段原因', 'Time / Session chain mapped': '时间 / 时段链路已映射', + 'Model Playbook Kernel': '模型 Playbook 内核', + 'Model Playbook Reason': '模型 Playbook 原因', + 'Model Playbook': '模型 Playbook', + 'Model Family': '模型族', + 'Model Playbook chain mapped': '模型 Playbook 链路已映射', 'Killzone': 'Killzone 时段', 'Mapped': '已映射', 'mapped': '已映射', @@ -810,7 +815,7 @@ ['missing_playbook', '缺少 playbook'], ['Doctrine Cohort', 'Doctrine 回测分组'], ['Doctrine Cohorts', 'Doctrine 回测分组'], - ['Setup Playbook', 'Setup Playbook'], + ['Setup Playbook', 'Setup 剧本'], ['Doctrine Completeness', 'Doctrine 完整度'], ['Liquidity Kernel', '流动性内核'], ['Liquidity Reason', '流动性原因'], @@ -820,7 +825,39 @@ ['Time / Session Kernel', '时间 / 时段内核'], ['Time / Session Reason', '时间 / 时段原因'], ['Time / Session chain mapped', '时间 / 时段链路已映射'], + ['Model Playbook Kernel', '模型 Playbook 内核'], + ['Model Playbook Reason', '模型 Playbook 原因'], + ['Model Playbook', '模型 Playbook'], + ['Model Family', '模型族'], + ['Model Playbook chain mapped', '模型 Playbook 链路已映射'], + ['model_playbook_mapped', '模型 Playbook 已映射'], + ['model_playbook_kernel_blocked', '模型 Playbook 内核阻塞'], + ['missing_model_playbook', '缺少模型 Playbook'], + ['unknown_model_playbook', '未知模型 Playbook'], + ['missing_model_family', '缺少模型族'], + ['missing_model_range_contract', '缺少模型区间合同'], + ['missing_model_draw_contract', '缺少模型 Draw 合同'], + ['missing_model_liquidity_contract', '缺少模型流动性合同'], + ['missing_model_pd_array_contract', '缺少模型 PD Array 合同'], + ['missing_model_session_contract', '缺少模型时段合同'], + ['missing_model_confirmation_contract', '缺少模型确认合同'], + ['missing_model_invalidation_contract', '缺少模型失效合同'], + ['missing_model_target_contract', '缺少模型目标合同'], + ['missing_model_timeframe_contract', '缺少模型周期合同'], + ['missing_model_trigger_contract', '缺少模型触发合同'], + ['missing_model_management_contract', '缺少模型管理合同'], + ['missing_model_backtest_cohort', '缺少模型回测分组'], + ['missing_model_quality_gate_contract', '缺少模型质量门合同'], + ['missing_model_journal_feedback', '缺少模型复盘反馈合同'], + ['missing_model_operator_runbook', '缺少模型操作话术'], ['Killzone', 'Killzone 时段'], + ['cohorts', '个分组'], + ['cohort', '个分组'], + ['runs', '次运行'], + ['run', '次运行'], + ['passed', '通过'], + ['mapped', '已映射'], + ['blocked', '已阻塞'], ['Promotion Gate', '晋级门'], ['complete', '完整'], ['incomplete', '不完整'], diff --git a/tests/unit/test_api_and_main.py b/tests/unit/test_api_and_main.py index 15f9757..40a80ca 100644 --- a/tests/unit/test_api_and_main.py +++ b/tests/unit/test_api_and_main.py @@ -99,6 +99,8 @@ class ApiTests(unittest.TestCase): self.assertIn("missing_pd_array", result[0]["pd_array_reaction_status"]["reasons"]) self.assertEqual(result[0]["time_session_kernel_status"]["status"], "study_only") self.assertIn("missing_session_window", result[0]["time_session_kernel_status"]["reasons"]) + self.assertEqual(result[0]["model_playbook_kernel_status"]["status"], "study_only") + self.assertIn("missing_model_playbook", result[0]["model_playbook_kernel_status"]["reasons"]) def test_normalize_backtest_summary_row_preserves_existing_quality_gate(self) -> None: row = {"id": 10, "summary": {"total_signals": 3, "quality_gate": {"passed": True}, "by_symbol": {"BTC-USDT": {"count": 3}}}} @@ -134,6 +136,8 @@ class ApiTests(unittest.TestCase): self.assertIn("missing_pd_array", result["pd_array_reaction_summary"]["reason_counts"]) self.assertEqual(result["time_session_kernel_summary"]["item_count"], 1) self.assertIn("missing_session_window", result["time_session_kernel_summary"]["reason_counts"]) + self.assertEqual(result["model_playbook_kernel_summary"]["item_count"], 1) + self.assertIn("missing_model_playbook", result["model_playbook_kernel_summary"]["reason_counts"]) def test_route_request_trading_doctrine_rules_returns_d1_matrix(self) -> None: status, payload = route_request( @@ -210,6 +214,22 @@ class ApiTests(unittest.TestCase): self.assertEqual(payload["phase"], "D5_TIME_SESSION_KERNEL") self.assertEqual(payload["rules"][0]["rule_id"], "session_window_verifiable") + def test_route_request_trading_model_playbook_kernel_returns_d6_registry(self) -> None: + status, payload = route_request( + "/trading/model-playbook-kernel", + {}, + dependencies={ + "trading_model_playbook_kernel": lambda: { + "phase": "D6_MODEL_PLAYBOOK_KERNEL", + "rules": [{"rule_id": "model_family_declared"}], + } + }, + ) + + self.assertEqual(status, 200) + self.assertEqual(payload["phase"], "D6_MODEL_PLAYBOOK_KERNEL") + self.assertEqual(payload["rules"][0]["rule_id"], "model_family_declared") + def test_route_request_trading_candidates_returns_items_payload(self) -> None: status, payload = route_request( "/trading/candidates", diff --git a/tests/unit/test_api_server.py b/tests/unit/test_api_server.py index b0039c5..18448cb 100644 --- a/tests/unit/test_api_server.py +++ b/tests/unit/test_api_server.py @@ -166,6 +166,22 @@ class ApiServerRoutingTests(unittest.TestCase): self.assertEqual(payload["phase"], "D5_TIME_SESSION_KERNEL") self.assertEqual(payload["rule_count"], 6) + def test_routes_trading_model_playbook_kernel(self) -> None: + status, payload = route_request( + "/trading/model-playbook-kernel", + {}, + dependencies={ + "trading_model_playbook_kernel": lambda: { + "phase": "D6_MODEL_PLAYBOOK_KERNEL", + "rule_count": 6, + } + }, + ) + + self.assertEqual(status, 200) + self.assertEqual(payload["phase"], "D6_MODEL_PLAYBOOK_KERNEL") + self.assertEqual(payload["rule_count"], 6) + def test_routes_trading_alert_quality_summary(self) -> None: status, payload = route_request( "/trading/alerts/quality-summary", diff --git a/tests/unit/test_backtest_service.py b/tests/unit/test_backtest_service.py index 2d05921..0dd4d26 100644 --- a/tests/unit/test_backtest_service.py +++ b/tests/unit/test_backtest_service.py @@ -324,6 +324,9 @@ class BacktestOutcomeTests(unittest.TestCase): self.assertEqual(summary["time_session_kernel_status_counts"]["study_only"], 4) self.assertEqual(summary["time_session_kernel_reason_counts"]["missing_session_window"], 4) self.assertEqual(summary["by_time_session_code"]["LONDON"]["count"], 3) + self.assertEqual(summary["model_playbook_kernel_status_counts"]["study_only"], 4) + self.assertEqual(summary["model_playbook_kernel_reason_counts"]["missing_model_playbook"], 4) + self.assertEqual(summary["by_model_playbook"]["missing_model_playbook"]["count"], 4) self.assertEqual(summary["by_session"]["LONDON"]["max_consecutive_losses"], 2) self.assertEqual(summary["by_session"]["LONDON"]["executed"], 3) self.assertAlmostEqual(summary["by_session"]["LONDON"]["executed_expectancy_r"], 0.2 / 3) @@ -411,6 +414,7 @@ class BacktestOutcomeTests(unittest.TestCase): "candidate_id": 1, "trading_plan_id": 77, "candidate_model_code": "OSOK", + "setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL", "side": "buy", "setup_timeframe": "15m", "trigger_timeframe": "5m", @@ -428,6 +432,7 @@ class BacktestOutcomeTests(unittest.TestCase): "candidate_id": 2, "trading_plan_id": 77, "candidate_model_code": "OSOK", + "setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL", "side": "buy", "setup_timeframe": "15m", "trigger_timeframe": "5m", @@ -510,6 +515,8 @@ class BacktestOutcomeTests(unittest.TestCase): self.assertEqual(units[0]["pd_array_reaction_status"]["status"], "pd_array_mapped") self.assertEqual(units[0]["time_session_backtest_features"]["session_code"], "LONDON") self.assertEqual(units[0]["time_session_kernel_status"]["status"], "time_session_mapped") + self.assertEqual(units[0]["model_playbook_backtest_features"]["model_playbook_id"], "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL") + self.assertEqual(units[0]["model_playbook_kernel_status"]["status"], "model_playbook_mapped") class BacktestServiceRunTests(unittest.TestCase): @@ -883,6 +890,11 @@ class BacktestServiceRunTests(unittest.TestCase): self.assertEqual(repository.summary["by_pd_array_type"]["fvg"]["count"], 1) self.assertEqual(repository.summary["time_session_kernel_status"]["status"], "time_session_mapped") self.assertEqual(repository.summary["by_time_session_code"]["LONDON"]["count"], 1) + self.assertEqual(repository.summary["model_playbook_kernel_status"]["status"], "model_playbook_mapped") + self.assertEqual( + repository.summary["by_model_playbook"]["ICT-CH1-6-LONDON-DISCOUNT-REVERSAL"]["count"], + 1, + ) def test_normalize_timeframe_set_and_bucket_helpers(self) -> None: normalized = normalize_timeframe_set({"bias_tf": "1h", "setup_tf": "15m"}) diff --git a/tests/unit/test_documentation_consistency.py b/tests/unit/test_documentation_consistency.py index 9971688..cc95693 100644 --- a/tests/unit/test_documentation_consistency.py +++ b/tests/unit/test_documentation_consistency.py @@ -23,6 +23,7 @@ ACTIVE_TOP_LEVEL_DOCS = { "AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md", "AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md", "AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md", + "AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md", "AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md", "AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md", "AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md", diff --git a/tests/unit/test_ict_doctrine.py b/tests/unit/test_ict_doctrine.py index 8a87f35..4e002ca 100644 --- a/tests/unit/test_ict_doctrine.py +++ b/tests/unit/test_ict_doctrine.py @@ -3,24 +3,29 @@ import unittest from src.services.ict_mainline import ( DOCTRINE_KERNEL_PHASE, DOCTRINE_LIQUIDITY_PHASE, + DOCTRINE_MODEL_PLAYBOOK_PHASE, DOCTRINE_PLAYBOOK_PHASE, DOCTRINE_REACTION_PHASE, DOCTRINE_SESSION_PHASE, build_doctrine_backtest_cohort, build_liquidity_backtest_features, + build_model_playbook_backtest_features, build_pd_array_backtest_features, build_time_session_backtest_features, build_doctrine_playbook_gate, build_doctrine_playbook_registry, build_doctrine_rule_matrix, build_liquidity_kernel_registry, + build_model_playbook_kernel_registry, build_pd_array_reaction_registry, build_time_session_kernel_registry, evaluate_doctrine_research_status, evaluate_liquidity_kernel_status, + evaluate_model_playbook_kernel_status, evaluate_pd_array_reaction_status, evaluate_time_session_kernel_status, summarize_liquidity_kernel_statuses, + summarize_model_playbook_kernel_statuses, summarize_pd_array_reaction_statuses, summarize_time_session_kernel_statuses, summarize_doctrine_rule_gaps, @@ -214,6 +219,51 @@ class IctDoctrineKernelTests(unittest.TestCase): self.assertGreater(summary["reason_counts"]["outside_session_window"], 0) self.assertEqual(summary["session_code_counts"]["LONDON"], 1) + def test_d6_model_playbook_kernel_registry_exposes_full_chain_contracts(self) -> None: + registry = build_model_playbook_kernel_registry() + + self.assertEqual(registry["phase"], DOCTRINE_MODEL_PLAYBOOK_PHASE) + self.assertEqual(registry["status"], "active") + self.assertGreaterEqual(registry["rule_count"], 6) + self.assertIn("model_playbook_id", registry["required_backtest_feature_fields"]) + self.assertIn("Raw model_code is not a tradable setup.", registry["hard_rules"]) + + def test_raw_model_without_playbook_stays_study_only(self) -> None: + status = evaluate_model_playbook_kernel_status( + {"model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE"} + ) + + self.assertFalse(status["passed"]) + self.assertEqual(status["phase"], DOCTRINE_MODEL_PLAYBOOK_PHASE) + self.assertEqual(status["status"], "study_only") + self.assertIn("missing_model_playbook", status["reasons"]) + self.assertIn("missing_model_range_contract", status["reasons"]) + self.assertIn("missing_model_backtest_cohort", status["reasons"]) + + def test_registered_model_playbook_maps_without_execution_permission(self) -> None: + status = evaluate_model_playbook_kernel_status( + {"setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL"} + ) + + self.assertTrue(status["passed"]) + self.assertEqual(status["status"], "model_playbook_mapped") + self.assertEqual(status["features"]["model_family"], "london_discount_reversal") + self.assertIn("range_id", status["features"]["range_contract"]) + self.assertEqual(status["execution_boundary"], "read_only_analysis_no_order_submission") + + def test_model_playbook_summary_counts_blockers(self) -> None: + summary = summarize_model_playbook_kernel_statuses( + [ + {"model_code": "SWEEP_MSS_FVG"}, + {"setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL"}, + ] + ) + + self.assertEqual(summary["item_count"], 2) + self.assertEqual(summary["passed_count"], 1) + self.assertGreater(summary["reason_counts"]["missing_model_playbook"], 0) + self.assertEqual(summary["playbook_counts"]["ICT-CH1-6-LONDON-DISCOUNT-REVERSAL"], 1) + def test_playbook_gate_blocks_missing_and_passes_registered_schema(self) -> None: missing = build_doctrine_playbook_gate({"model_code": "SWEEP_MSS_FVG"}) @@ -313,6 +363,8 @@ class IctDoctrineKernelTests(unittest.TestCase): self.assertEqual(cohort["liquidity_kernel_status"]["status"], "liquidity_mapped") self.assertEqual(cohort["pd_array_reaction_status"]["status"], "pd_array_mapped") self.assertEqual(cohort["time_session_kernel_status"]["status"], "time_session_mapped") + self.assertEqual(cohort["model_playbook_kernel_status"]["status"], "model_playbook_mapped") + self.assertTrue(cohort["promotion_gate"]["requires_model_playbook_kernel_complete"]) missing = build_doctrine_backtest_cohort({"model_code": "SWEEP_MSS_FVG"}) @@ -321,6 +373,7 @@ class IctDoctrineKernelTests(unittest.TestCase): self.assertIn("missing_liquidity_target", missing["promotion_gate"]["reasons"]) self.assertIn("missing_pd_array", missing["promotion_gate"]["reasons"]) self.assertIn("missing_session_window", missing["promotion_gate"]["reasons"]) + self.assertIn("missing_model_playbook", missing["promotion_gate"]["reasons"]) def test_liquidity_backtest_features_extract_nested_candidate_fields(self) -> None: features = build_liquidity_backtest_features( @@ -385,6 +438,21 @@ class IctDoctrineKernelTests(unittest.TestCase): self.assertEqual(features["killzone_name"], "NY_AM_KILLZONE") self.assertEqual(features["opening_range_state"], "defined") + def test_model_playbook_features_extract_nested_playbook_fields(self) -> None: + features = build_model_playbook_backtest_features( + { + "candidate": { + "entry": { + "setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL", + } + } + } + ) + + self.assertEqual(features["model_playbook_id"], "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL") + self.assertEqual(features["model_family"], "london_discount_reversal") + self.assertIn("liquidity_target_side", features["liquidity_contract"]) + def _complete_pd_array_payload() -> dict: return {