diff --git a/SESSION.md b/SESSION.md index 652b781..84e202d 100644 --- a/SESSION.md +++ b/SESSION.md @@ -847,10 +847,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - D0 Doctrine Kernel stage reset is complete locally: added `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md` as the new phase anchor. D0 explicitly downgrades existing surface-shape candidates (`sweep / MSS / FVG / retrace`, including `OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE`) to `research_only` / `study_only` evidence components unless they are remapped through a full Doctrine setup playbook and pass historical evidence gates. The reset keeps the current infrastructure but changes product priority: do not continue expanding old candidates; first extract Doctrine Rules from the full book, define setup playbooks, then backtest by playbook. No execution/readiness/risk/trust/operator-confirmation gate was changed. - D1 Doctrine Market Language Kernel is complete locally: added `src/services/ict_mainline/doctrine.py`, `GET /trading/doctrine-rules`, Strategy Backtest `doctrine_research_status` / `doctrine_rule_gap_summary`, and Trader Cockpit watchlist/readout `research_only` gating. Old sweep/MSS/FVG/retrace candidates now expose `missing_doctrine_setup_playbook` or the specific missing Doctrine rule reason unless mapped to a full setup playbook. No execution/readiness/risk/trust/operator-confirmation gate was changed. - D2 Doctrine Playbook / Backtest Cohort is complete locally: added the first registered setup playbook `ICT-CH1-6-LONDON-DISCOUNT-REVERSAL`, `GET /trading/doctrine-playbooks`, `doctrine_backtest_cohort` / `doctrine_cohort_summary`, `BACKTEST_SETUP_PLAYBOOK_ID`, `BACKTEST_BATCH_SETUP_PLAYBOOK_IDS`, and a Review Now gate that requires Doctrine playbook, active Trading Plan, and Strategy Backtest quality evidence to all pass. No execution/readiness/risk/trust/operator-confirmation gate was changed. +- 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. ## 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/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 D3 Liquidity Kernel: extract BSL / SSL / liquidity pool / stop run / sweep / raid from the book into target-ranking and no-trade/wait rules; keep sweep as evidence component only, not a setup; wire the playbook `liquidity_event_rule` toward measurable backtest fields. 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/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 D4 PD Array / Reaction Kernel: extract FVG / Order Block / Breaker / Mitigation / Rebalance / Void into reaction-location quality ranking rules; keep PD Array as a reaction/entry-location layer, not a direction source; wire the playbook `pd_array_rule` toward measurable backtest fields. 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 89ff854..1768aac 100644 --- a/TASKS.md +++ b/TASKS.md @@ -954,7 +954,8 @@ - [ ] R2 weekly review visible gate still needs a real execution-derived journal/weekly review entry (`weekly_review_entry_count=0`) - [x] D1 Doctrine Market Language Kernel: built the first `Range / Draw / Location / Context / Time / Invalidation / IRL-ERL` registry, added `/trading/doctrine-rules`, Strategy Backtest Doctrine gap aggregation, and Trader Cockpit `research_only_reason` gating for old surface-shape candidates; no live order submission and no fabricated samples - [x] D2 Doctrine Playbook / Backtest Cohort: defined first registered setup playbook `ICT-CH1-6-LONDON-DISCOUNT-REVERSAL`, added `/trading/doctrine-playbooks`, `doctrine_backtest_cohort` / `doctrine_cohort_summary`, setup-playbook batch envs, Strategy Backtest UI cohort display, and Review Now gating that requires playbook mapping + historical quality gate + active Trading Plan -- [ ] D3 next product gate: rebuild Liquidity Kernel from the book by turning BSL / SSL / liquidity pool / stop run / sweep / raid into target-ranking and no-trade/wait rules; sweep remains evidence component only, not a setup +- [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 +- [ ] D4 next product gate: rebuild PD Array / Reaction Kernel so FVG / OB / Breaker / Mitigation / Rebalance / Void become reaction-location quality rankings instead of standalone direction tags - [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_LiquidityKernel_D3_2026-05-21_v1.md b/docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md new file mode 100644 index 0000000..86ab3c2 --- /dev/null +++ b/docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md @@ -0,0 +1,118 @@ +# AI_ICT Doctrine Kernel Liquidity Kernel D3 2026-05-21 v1 + +## 1. 本阶段结论 + +D3 把 BSL / SSL / liquidity pool / stop run / sweep / raid 从旧候选器里的“形态标签”,重建为全书第二部分的流动性目标排序规则。 + +核心口径: + +```text +Narrative -> Working Draw -> Ranked Liquidity Pool -> Liquidity Action -> Post-take Reaction -> Entry Anchor +``` + +其中 `sweep` / `raid` / `stop_run` 只能是 evidence component,不能单独成为 setup,更不能直接推导 `Review Now`。 + +## 2. Liquidity Kernel 注册表 + +新增 `src/services/ict_mainline/doctrine.py` 中的 D3 注册表: + +- `DOCTRINE_LIQUIDITY_PHASE = D3_LIQUIDITY_KERNEL` +- `LiquidityTargetRule` +- `build_liquidity_kernel_registry()` +- `evaluate_liquidity_kernel_status()` +- `build_liquidity_backtest_features()` +- `summarize_liquidity_kernel_statuses()` + +公开接口: + +```text +GET /trading/liquidity-kernel +``` + +## 3. D3 规则层 + +D3 第一版规则覆盖《ICT交易全书》第 7-12 章: + +- `bsl_ssl_object_not_direction` +- `liquidity_pool_quality_ranked` +- `stop_run_sweep_raid_validated` +- `judas_manipulation_two_stage` +- `void_path_requires_trigger_pool` +- `working_draw_target_priority` + +硬规则: + +- BSL / SSL 是流动性对象,不是方向结论。 +- Liquidity Pool 必须先分主池 / 次池 / 噪音池。 +- Stop Run / Sweep / Raid 只是动作事件,必须等扫后位移、回收/接受、锚点承接。 +- Judas / Manipulation 的诱导阶段只观察,不表达风险。 +- Working Draw 只能由结构 / 时段 / 叙事触发器客观重排。 + +## 4. 回测字段 + +Strategy Backtest 现在会暴露: + +- `liquidity_backtest_features` +- `liquidity_kernel_status` +- `liquidity_kernel_summary` +- `by_liquidity_target_side` +- `liquidity_kernel_reason_counts` + +关键字段包括: + +- `liquidity_target_side` +- `working_draw` +- `secondary_draw` +- `observation_draw` +- `liquidity_pool_id` +- `liquidity_pool_rank` +- `liquidity_event` +- `swept_level` +- `sweep_confirmed` +- `raid_stop_run_kind` +- `post_take_reaction` +- `manipulation_window_state` +- `reorder_trigger_type` + +缺少这些字段时,历史回测仍可保存,但必须显示为 `study_only` / `wait`,不得晋级为可信 setup。 + +## 5. Playbook 变化 + +`ICT-CH1-6-LONDON-DISCOUNT-REVERSAL` 的 `liquidity_event_rule` 已从自然语言升级为可回测字段要求。 + +新增阻塞原因: + +- `missing_liquidity_target` +- `missing_liquidity_pool` +- `missing_working_draw` +- `sweep_is_not_setup` +- `manipulation_window_wait` +- `unranked_liquidity_pool` + +Doctrine cohort 的 promotion gate 现在同时要求: + +1. Doctrine setup playbook schema complete +2. Liquidity Kernel complete +3. Strategy Backtest quality gate passed +4. Active Trading Plan present + +## 6. 不变边界 + +本阶段仍不改变: + +- `execution_allowed` +- `decision_support_ready` +- `real_live_ready` +- risk governor +- trust bucket +- operator confirmation protocol +- live order submission + +## 7. 下一步 + +D4 应进入 PD Array / Reaction Kernel: + +1. 把 FVG / Order Block / Breaker / Mitigation / Rebalance / Void 从“画框标签”重建为反应位质量排序。 +2. 明确 PD Array 是 reaction location,不是方向来源。 +3. 将 playbook 的 `pd_array_rule` 细化成可回测字段。 +4. 让 Strategy Backtest 能按 PD Array 质量分层比较绩效。 diff --git a/src/api/backtest.py b/src/api/backtest.py index d2f62c0..336edb5 100644 --- a/src/api/backtest.py +++ b/src/api/backtest.py @@ -7,11 +7,14 @@ from src.db.session import SessionLocal from src.services.backtest import evaluate_backtest_quality from src.services.ict_mainline import ( build_doctrine_backtest_cohort, + build_liquidity_backtest_features, build_backtest_quality_gate, build_ict_rulebook_mapping, evaluate_doctrine_research_status, + evaluate_liquidity_kernel_status, summarize_doctrine_backtest_cohorts, summarize_doctrine_rule_gaps, + summarize_liquidity_kernel_statuses, ) @@ -33,6 +36,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in ) doctrine_rule_gap_summary = summarize_doctrine_rule_gaps(items) doctrine_cohort_summary = summarize_doctrine_backtest_cohorts(items) + liquidity_kernel_summary = summarize_liquidity_kernel_statuses(items) return { "instrument_id": instrument_id, "item_count": len(items), @@ -42,6 +46,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in "cockpit_promotion_gate": build_backtest_quality_gate(quality_gate), "doctrine_rule_gap_summary": doctrine_rule_gap_summary, "doctrine_cohort_summary": doctrine_cohort_summary, + "liquidity_kernel_summary": liquidity_kernel_summary, "ict_rulebook_mapping": ict_rulebook_mapping, "items": items, } @@ -105,6 +110,27 @@ def normalize_backtest_summary_row(row: dict) -> dict: }, require_setup_playbook=True, ) + liquidity_backtest_features = build_liquidity_backtest_features( + { + "subject_type": row.get("subject_type"), + "model_code": row.get("model_code"), + "candidate_model_code": row.get("candidate_model_code"), + "timeframe_set": timeframe_set, + "config": config, + "summary": summary, + } + ) + liquidity_kernel_status = evaluate_liquidity_kernel_status( + { + "subject_type": row.get("subject_type"), + "model_code": row.get("model_code"), + "candidate_model_code": row.get("candidate_model_code"), + "timeframe_set": timeframe_set, + "config": config, + "summary": summary, + "liquidity_backtest_features": liquidity_backtest_features, + } + ) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { "subject_type": row.get("subject_type"), @@ -113,6 +139,7 @@ def normalize_backtest_summary_row(row: dict) -> dict: "timeframe_set": timeframe_set, "config": config, "summary": summary, + "liquidity_backtest_features": liquidity_backtest_features, }, quality_gate=dict(summary.get("quality_gate") or {}), ) @@ -123,6 +150,8 @@ def normalize_backtest_summary_row(row: dict) -> dict: "summary": summary, "ict_rulebook_mapping": ict_rulebook_mapping, "doctrine_research_status": doctrine_research_status, + "liquidity_backtest_features": liquidity_backtest_features, + "liquidity_kernel_status": liquidity_kernel_status, "doctrine_backtest_cohort": doctrine_backtest_cohort, } @@ -153,6 +182,13 @@ def _build_operator_summary(latest: Optional[dict]) -> dict: status = "doctrine_research_only" decision = "do_not_promote_strategy" reason = str(doctrine_status.get("research_only_reason") or reason) + liquidity_status = dict(latest.get("liquidity_kernel_status") or {}) + if not liquidity_status.get("passed"): + liquidity_reasons = list(liquidity_status.get("reasons") or []) + if liquidity_reasons: + status = "liquidity_kernel_blocked" + decision = "do_not_promote_strategy" + reason = str(liquidity_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 40fff72..5e9fa8d 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -122,7 +122,7 @@ from src.api.training import ( parse_quality_sort_query, submit_training_attempt, ) -from src.services.ict_mainline import build_doctrine_playbook_registry, build_doctrine_rule_matrix +from src.services.ict_mainline import build_doctrine_playbook_registry, build_doctrine_rule_matrix, build_liquidity_kernel_registry from src.services.execution import ExecutionService from src.services.market_state import start_market_truth_runtime from src.services.runtime import start_trading_runtime_service @@ -224,6 +224,7 @@ def build_api_dependencies() -> dict: "trading_backtests": get_strategy_backtest_surface, "trading_doctrine_rules": build_doctrine_rule_matrix, "trading_doctrine_playbooks": build_doctrine_playbook_registry, + "trading_liquidity_kernel": build_liquidity_kernel_registry, "training_cases": get_training_cases, "training_case": get_training_case, "training_export_cases": export_training_case_package, @@ -849,6 +850,8 @@ def route_request( return 200, deps["trading_doctrine_rules"]() if method == "GET" and path == "/trading/doctrine-playbooks": return 200, deps["trading_doctrine_playbooks"]() + if method == "GET" and path == "/trading/liquidity-kernel": + return 200, deps["trading_liquidity_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 b24f722..1ed26d6 100644 --- a/src/services/backtest/backtest_service.py +++ b/src/services/backtest/backtest_service.py @@ -4,7 +4,12 @@ from typing import Optional from src.repositories.backtest_repository import BacktestRepository from src.services.backtest.replay import ReplaySwing, iter_historical_replay -from src.services.ict_mainline import build_doctrine_backtest_cohort, extract_setup_playbook_id +from src.services.ict_mainline import ( + build_doctrine_backtest_cohort, + build_liquidity_backtest_features, + evaluate_liquidity_kernel_status, + extract_setup_playbook_id, +) SAME_CANDLE_PRIORITY = "stop_first" @@ -127,6 +132,16 @@ class BacktestService: setup_playbook_id = extract_setup_playbook_id(active_trading_plan) if setup_playbook_id: run_metadata['setup_playbook_id'] = setup_playbook_id + liquidity_backtest_features = build_liquidity_backtest_features( + { + 'subject_type': subject_type, + 'model_code': 'SWEEP_MSS_FVG', + 'candidate_model_code': candidate_model_code, + 'timeframe_set': timeframe_set, + 'run_metadata': run_metadata, + 'active_trading_plan': active_trading_plan, + } + ) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -134,6 +149,7 @@ class BacktestService: 'candidate_model_code': candidate_model_code, 'timeframe_set': timeframe_set, 'run_metadata': run_metadata, + 'liquidity_backtest_features': liquidity_backtest_features, } ) run_id = self.repository.insert_backtest_run( @@ -156,6 +172,8 @@ class BacktestService: 'replay': replay_config.__dict__ if mode == BACKTEST_MODE_REPLAY_NO_LOOKAHEAD else None, 'session_codes': list(session_codes or []), 'setup_playbook_id': setup_playbook_id, + 'liquidity_backtest_features': liquidity_backtest_features, + 'liquidity_kernel_status': evaluate_liquidity_kernel_status(liquidity_backtest_features), 'doctrine_backtest_cohort': doctrine_backtest_cohort, 'run_metadata': run_metadata, }, @@ -175,6 +193,9 @@ class BacktestService: summary = summarize_backtest_outcomes(outcomes, subject_type=subject_type) summary['quality_gate'] = evaluate_backtest_quality(summary, gate=quality_gate, sample_basis='executed') outcome_setup_playbook_id = setup_playbook_id or _first_outcome_meta_value(outcomes, 'setup_playbook_id') + outcome_liquidity_features = _first_outcome_meta_value(outcomes, 'liquidity_backtest_features') or liquidity_backtest_features + summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {}) + summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features']) summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -183,6 +204,7 @@ class BacktestService: 'timeframe_set': timeframe_set, 'run_metadata': {**run_metadata, 'setup_playbook_id': outcome_setup_playbook_id}, 'summary': summary, + 'liquidity_backtest_features': summary['liquidity_backtest_features'], }, quality_gate=summary['quality_gate'], ) @@ -213,6 +235,13 @@ class BacktestService: target_plan = extract_target_plan_from_signal_row(signal) management_rules = extract_management_rules_from_signal_row(signal) session_codes = resolve_signal_session_codes(signal=signal, sessions=sessions) + signal_liquidity_features = build_liquidity_backtest_features( + { + 'signal': signal, + 'target_plan': target_plan, + 'run_metadata': run_metadata, + } + ) error_tags = ['no_future_candles'] if len(sessions) == 0: error_tags.append('outside_session_window') @@ -249,6 +278,8 @@ class BacktestService: 'rr_tp2': float(signal.get('rr_tp2') or 0.0), 'target_plan': target_plan, 'management_rules': management_rules, + 'liquidity_backtest_features': signal_liquidity_features, + 'liquidity_kernel_status': evaluate_liquidity_kernel_status(signal_liquidity_features), 'cost_r': 0.0, 'cost_model': cost_config.__dict__, 'backtest_mode': mode, @@ -325,9 +356,17 @@ class BacktestService: 'rr_tp2': float(signal.get('rr_tp2') or 0.0), 'target_plan': extract_target_plan_from_signal_row(signal), 'management_rules': extract_management_rules_from_signal_row(signal), + 'liquidity_backtest_features': build_liquidity_backtest_features( + { + 'signal': signal, + 'target_plan': extract_target_plan_from_signal_row(signal), + 'run_metadata': run_metadata, + } + ), 'weekday': _extract_weekday(signal['created_ts']), } ) + record.meta['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(record.meta['liquidity_backtest_features']) outcomes.append(record) self.repository.insert_backtest_result( backtest_run_id=run_id, @@ -346,6 +385,9 @@ class BacktestService: ) summary = summarize_backtest_outcomes(outcomes, subject_type=subject_type) summary['quality_gate'] = evaluate_backtest_quality(summary, gate=quality_gate) + outcome_liquidity_features = _first_outcome_meta_value(outcomes, 'liquidity_backtest_features') or liquidity_backtest_features + summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {}) + summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features']) summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -354,6 +396,7 @@ class BacktestService: 'timeframe_set': timeframe_set, 'run_metadata': run_metadata, 'summary': summary, + 'liquidity_backtest_features': summary['liquidity_backtest_features'], }, quality_gate=summary['quality_gate'], ) @@ -909,12 +952,36 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: }, } ) + liquidity_backtest_features = build_liquidity_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, + } + ) + liquidity_kernel_status = evaluate_liquidity_kernel_status(liquidity_backtest_features) + doctrine_backtest_cohort = build_doctrine_backtest_cohort( + { + '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'), + }, + 'liquidity_backtest_features': liquidity_backtest_features, + } + ) units.append( { 'trading_plan_id': first.get('trading_plan_id'), 'candidate_id': (confirmed_row or first).get('candidate_id'), 'setup_playbook_id': setup_playbook_id, 'doctrine_backtest_cohort': doctrine_backtest_cohort, + 'liquidity_backtest_features': liquidity_backtest_features, + 'liquidity_kernel_status': liquidity_kernel_status, 'candidate_model_code': first.get('candidate_model_code'), 'side': first.get('side'), 'session_code': first.get('session_code'), @@ -1006,6 +1073,8 @@ def build_plan_replay_meta( 'doctrine_backtest_cohort': dict(unit.get('doctrine_backtest_cohort') or {}), 'doctrine_cohort_key': (unit.get('doctrine_backtest_cohort') or {}).get('cohort_key'), 'doctrine_completeness': (unit.get('doctrine_backtest_cohort') or {}).get('doctrine_completeness'), + 'liquidity_backtest_features': dict(unit.get('liquidity_backtest_features') or {}), + 'liquidity_kernel_status': dict(unit.get('liquidity_kernel_status') or {}), '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'), @@ -1192,6 +1261,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s grouped_by_symbol: dict[str, dict] = {} grouped_by_timeframe: dict[str, dict] = {} grouped_by_doctrine_cohort: dict[str, dict] = {} + grouped_by_liquidity_target_side: dict[str, dict] = {} r_multiples: list[float] = [] cost_values: list[float] = [] mfe_r_values: list[float] = [] @@ -1202,6 +1272,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s compliant_candidates = 0 draw_target_hits = 0 failure_reason_counts: dict[str, int] = {} + liquidity_kernel_reason_counts: dict[str, int] = {} + liquidity_kernel_status_counts: dict[str, int] = {} plan_total_r: dict[int, float] = {} plan_seen: set[int] = set() @@ -1237,6 +1309,13 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s symbol = _extract_symbol_from_outcome(outcome) timeframe_bucket = str(outcome.meta.get('signal_timeframe_bucket') or 'unknown') doctrine_cohort_key = str(outcome.meta.get('doctrine_cohort_key') or 'missing_playbook::incomplete') + liquidity_features = dict(outcome.meta.get('liquidity_backtest_features') or {}) + liquidity_target_side = str(liquidity_features.get('liquidity_target_side') or 'missing_liquidity_target') + liquidity_status = dict(outcome.meta.get('liquidity_kernel_status') or evaluate_liquidity_kernel_status(liquidity_features)) + liquidity_status_key = str(liquidity_status.get('status') or 'unknown') + liquidity_kernel_status_counts[liquidity_status_key] = liquidity_kernel_status_counts.get(liquidity_status_key, 0) + 1 + for reason in list(liquidity_status.get('reasons') or []): + liquidity_kernel_reason_counts[str(reason)] = liquidity_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)) @@ -1247,6 +1326,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s _accumulate_bucket(grouped_by_symbol, symbol, outcome) _accumulate_bucket(grouped_by_timeframe, timeframe_bucket, outcome) _accumulate_bucket(grouped_by_doctrine_cohort, doctrine_cohort_key, outcome) + _accumulate_bucket(grouped_by_liquidity_target_side, liquidity_target_side, outcome) for session_code in session_codes: _accumulate_bucket(grouped_by_session, str(session_code), outcome) @@ -1277,6 +1357,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s 'plan_wins': plan_wins, 'plan_win_rate': _ratio(plan_wins, plan_count), 'failure_reasons': failure_reason_counts, + 'liquidity_kernel_status_counts': liquidity_kernel_status_counts, + 'liquidity_kernel_reason_counts': liquidity_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), @@ -1295,6 +1377,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s 'by_symbol': grouped_by_symbol, 'by_timeframe': grouped_by_timeframe, 'by_doctrine_cohort': grouped_by_doctrine_cohort, + 'by_liquidity_target_side': grouped_by_liquidity_target_side, } 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 8dbc7a1..162b33e 100644 --- a/src/services/ict_mainline/__init__.py +++ b/src/services/ict_mainline/__init__.py @@ -19,28 +19,37 @@ from src.services.ict_mainline.feedback import ( ) from src.services.ict_mainline.doctrine import ( DOCTRINE_KERNEL_PHASE, + DOCTRINE_LIQUIDITY_PHASE, DOCTRINE_PLAYBOOK_PHASE, build_doctrine_backtest_cohort, + build_liquidity_backtest_features, build_doctrine_rule_matrix, build_doctrine_playbook_gate, build_doctrine_playbook_registry, + build_liquidity_kernel_registry, evaluate_doctrine_research_status, + evaluate_liquidity_kernel_status, extract_setup_playbook_id, get_doctrine_setup_playbook, list_doctrine_setup_playbooks, list_doctrine_rules, + list_liquidity_rules, summarize_doctrine_backtest_cohorts, summarize_doctrine_rule_gaps, + summarize_liquidity_kernel_statuses, ) __all__ = [ "DOCTRINE_KERNEL_PHASE", + "DOCTRINE_LIQUIDITY_PHASE", "DOCTRINE_PLAYBOOK_PHASE", "ICT_MAINLINE_CHAIN", "build_doctrine_backtest_cohort", + "build_liquidity_backtest_features", "build_doctrine_rule_matrix", "build_doctrine_playbook_gate", "build_doctrine_playbook_registry", + "build_liquidity_kernel_registry", "build_ict_rulebook_mapping", "build_ict_rulebook_summary", "build_backtest_quality_gate", @@ -51,13 +60,16 @@ __all__ = [ "classify_book_rule_error", "classify_ict_concepts", "evaluate_doctrine_research_status", + "evaluate_liquidity_kernel_status", "extract_setup_playbook_id", "get_ict_concept", "get_doctrine_setup_playbook", "list_doctrine_setup_playbooks", "list_doctrine_rules", + "list_liquidity_rules", "list_ict_concepts", "summarize_doctrine_backtest_cohorts", "summarize_doctrine_rule_gaps", + "summarize_liquidity_kernel_statuses", "summarize_book_rule_errors", ] diff --git a/src/services/ict_mainline/doctrine.py b/src/services/ict_mainline/doctrine.py index f81db0b..e813023 100644 --- a/src/services/ict_mainline/doctrine.py +++ b/src/services/ict_mainline/doctrine.py @@ -8,6 +8,7 @@ from typing import Iterable DOCTRINE_KERNEL_PHASE = "D1_MARKET_LANGUAGE_KERNEL" DOCTRINE_PLAYBOOK_PHASE = "D2_PLAYBOOK_BACKTEST_COHORT" +DOCTRINE_LIQUIDITY_PHASE = "D3_LIQUIDITY_KERNEL" @dataclass(frozen=True) @@ -76,6 +77,34 @@ class DoctrineSetupPlaybook: return payload +@dataclass(frozen=True) +class LiquidityTargetRule: + rule_id: str + stage: str + book_anchor: str + target_type: 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", @@ -204,8 +233,23 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( "operator_rule": "Long ideas need discount or an explicit exception; short ideas need premium or an explicit exception.", }, liquidity_event_rule={ - "required_fields": ("liquidity_event", "swept_level", "sweep_confirmed"), - "operator_rule": "Sweep/raid is an evidence component, not the setup by itself.", + "required_fields": ( + "liquidity_target_side", + "liquidity_pool_id", + "liquidity_pool_rank", + "liquidity_event", + "swept_level", + "sweep_confirmed", + "raid_stop_run_kind", + "manipulation_window_state", + "post_take_reaction", + "reorder_trigger_type", + ), + "ranking_rule_id": "working_draw_target_priority", + "operator_rule": ( + "Sweep/raid is an evidence component only; it must point to a ranked BSL/SSL pool, " + "show post-take reaction, and preserve or objectively reorder the Working Draw." + ), }, displacement_structure_rule={ "required_fields": ("mss_state", "displacement_state", "structure_shift_ts"), @@ -236,6 +280,10 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( "doctrine_missing_range", "doctrine_missing_draw", "doctrine_missing_location", + "missing_liquidity_target", + "missing_liquidity_pool", + "sweep_is_not_setup", + "manipulation_window_wait", "doctrine_missing_time_path", "doctrine_missing_invalidation", "doctrine_missing_exit_architecture", @@ -269,6 +317,9 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( "traded_without_range", "traded_without_draw", "poor_trade_location", + "traded_sweep_as_setup", + "traded_without_liquidity_target_rank", + "traded_during_manipulation_phase", "traded_without_time_or_path", "traded_without_invalidation", "no_exit_architecture", @@ -287,6 +338,145 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( ) +D3_LIQUIDITY_TARGET_RULES: tuple[LiquidityTargetRule, ...] = ( + LiquidityTargetRule( + rule_id="bsl_ssl_object_not_direction", + stage="Target Side", + book_anchor="ICT交易全书 chapter 7: BSL / SSL are liquidity objects, not direction conclusions", + target_type="side_selection", + ranking_weight=100, + required_inputs=("htf_bias", "working_draw", "liquidity_target_side", "target_layer"), + measurable_fields=("liquidity_target_side", "working_draw", "target_layer", "target_rank"), + no_trade_when=("missing_liquidity_target", "bsl_ssl_used_as_direction", "liquidity_targets_tied_wait"), + wait_when=("target_side_defined_but_not_processed", "post_take_reaction_missing"), + backtest_observable="working_draw_side_missing_or_ambiguous", + operator_rule="Declare the Working Draw side first; BSL/SSL alone never means buy or sell.", + aliases=( + "bsl", + "ssl", + "buy_side_liquidity", + "sell_side_liquidity", + "liquidity_target_side", + "working_draw", + "draw_side", + ), + ), + LiquidityTargetRule( + rule_id="liquidity_pool_quality_ranked", + stage="Pool Ranking", + book_anchor="ICT交易全书 chapter 8: liquidity pool quality is ranking plus evidence plus switching", + target_type="pool_quality", + ranking_weight=90, + required_inputs=("liquidity_pool_id", "liquidity_pool_rank", "pool_visibility", "pool_freshness"), + measurable_fields=("liquidity_pool_id", "liquidity_pool_rank", "pool_quality_score", "pool_role"), + no_trade_when=("missing_liquidity_pool", "unranked_liquidity_pool", "noise_pool_promoted"), + wait_when=("primary_pool_not_processed", "pool_quality_conflict_unresolved"), + backtest_observable="setup_without_ranked_liquidity_pool", + operator_rule="Track the main pool, secondary pool, and noise pool separately before any execution review.", + aliases=( + "liquidity_pool", + "pool_rank", + "liquidity_pool_rank", + "pool_quality_score", + "old_high", + "old_low", + "equal_high", + "equal_low", + "session_high", + "session_low", + ), + ), + LiquidityTargetRule( + rule_id="stop_run_sweep_raid_validated", + stage="Action Validation", + book_anchor="ICT交易全书 chapter 10: Stop Run / Sweep / Raid require relevant pool and follow-through", + target_type="liquidity_action", + ranking_weight=80, + required_inputs=("liquidity_event", "swept_level", "sweep_confirmed", "raid_stop_run_kind", "post_take_reaction"), + measurable_fields=("liquidity_event", "swept_level", "sweep_confirmed", "raid_stop_run_kind", "post_take_reaction", "action_layer"), + no_trade_when=("sweep_is_not_setup", "sweep_without_relevant_pool", "any_wick_called_sweep"), + wait_when=("awaiting_post_sweep_displacement", "awaiting_reclaim_or_acceptance", "awaiting_entry_anchor"), + backtest_observable="sweep_without_post_take_confirmation", + operator_rule="A sweep/raid is only the trigger event; wait for displacement, reclaim/acceptance, and an entry anchor.", + aliases=( + "stop_run", + "liquidity_sweep", + "sweep", + "raid", + "stop_hunt", + "sweep_confirmed", + "swept_level", + "post_take_reaction", + ), + ), + LiquidityTargetRule( + rule_id="judas_manipulation_two_stage", + stage="Manipulation / Timing", + book_anchor="ICT交易全书 chapter 11: Judas / Manipulation has observation stage then confirmation stage", + target_type="manipulation_protocol", + ranking_weight=70, + required_inputs=("manipulation_window_state", "session_code", "inducement_side", "served_side"), + measurable_fields=("manipulation_window_state", "session_code", "inducement_side", "used_side", "served_side", "reclaim_state"), + no_trade_when=("trade_during_inducement", "missing_manipulation_confirmation", "session_timing_unresolved"), + wait_when=("manipulation_window_wait", "inducement_phase_active", "market_conditioning_unresolved"), + backtest_observable="entry_during_unconfirmed_manipulation_phase", + operator_rule="During Judas/Manipulation, observe the inducement side; only the confirmed stage can be reviewed.", + aliases=( + "judas", + "judas_swing", + "manipulation", + "market_conditioning", + "inducement", + "manipulation_window_state", + "used_side", + "served_side", + ), + ), + LiquidityTargetRule( + rule_id="void_path_requires_trigger_pool", + stage="Void / Path", + book_anchor="ICT交易全书 chapter 9: Liquidity Void is path speed after a trigger pool, not a standalone direction", + target_type="path_speed", + ranking_weight=60, + required_inputs=("trigger_pool_id", "liquidity_void_id", "void_displacement_state", "rebalancing_depth"), + measurable_fields=("trigger_pool_id", "liquidity_void_id", "void_displacement_state", "rebalancing_depth", "void_role"), + no_trade_when=("void_without_trigger_pool", "chasing_void_tail"), + wait_when=("waiting_void_rebalance", "waiting_void_continuation_or_takeover"), + backtest_observable="void_without_trigger_pool_or_rebalance_plan", + operator_rule="Use Void to read speed and rebalance; do not chase the tail without trigger-pool context.", + aliases=( + "liquidity_void", + "void", + "trigger_pool", + "rebalancing_depth", + "void_displacement_state", + ), + ), + LiquidityTargetRule( + rule_id="working_draw_target_priority", + stage="Target Priority", + book_anchor="ICT交易全书 chapter 12: target ranking requires main draw, secondary draw, observation draw, and reorder triggers", + target_type="target_ranking", + ranking_weight=110, + required_inputs=("working_draw", "secondary_draw", "observation_draw", "reorder_trigger_type"), + measurable_fields=("working_draw", "secondary_draw", "observation_draw", "reorder_trigger_type", "reorder_evidence"), + no_trade_when=("missing_working_draw", "emotional_reorder_without_trigger", "entry_without_served_draw"), + wait_when=("reorder_trigger_pending", "target_priority_conflict_unresolved"), + backtest_observable="entry_without_working_draw_or_reorder_evidence", + operator_rule="Predefine main/secondary/observation draws; only structure/time/narrative triggers can reorder them.", + aliases=( + "working_draw", + "current_working_draw", + "secondary_draw", + "observation_draw", + "reorder_trigger", + "target_priority", + "served_draw", + ), + ), +) + + def list_doctrine_rules() -> list[dict]: return [rule.to_public_dict() for rule in D1_DOCTRINE_RULES] @@ -325,6 +515,33 @@ def build_doctrine_playbook_registry() -> dict: } +def list_liquidity_rules() -> list[dict]: + return [rule.to_public_dict() for rule in D3_LIQUIDITY_TARGET_RULES] + + +def build_liquidity_kernel_registry() -> dict: + return { + "phase": DOCTRINE_LIQUIDITY_PHASE, + "source": "docs/ICT交易全书_出版终校版.md chapters 7-12", + "status": "active", + "rule_count": len(D3_LIQUIDITY_TARGET_RULES), + "hard_rules": [ + "BSL / SSL are liquidity objects, not direction conclusions.", + "Liquidity Pool must be ranked before it can guide a setup.", + "Stop Run / Sweep / Raid is an evidence component only, never a setup by itself.", + "Judas / Manipulation has an observation stage and a confirmation stage; trading the inducement stage is blocked.", + "Working Draw can be reordered only by structure, time, or narrative triggers.", + ], + "required_backtest_feature_fields": _dedupe( + field + for rule in D3_LIQUIDITY_TARGET_RULES + for field in rule.measurable_fields + ), + "rules": list_liquidity_rules(), + "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: @@ -408,6 +625,7 @@ def build_doctrine_playbook_gate(payload: object) -> dict: def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None = None) -> dict: setup_playbook_id = extract_setup_playbook_id(payload) playbook_gate = build_doctrine_playbook_gate(payload) + liquidity_status = evaluate_liquidity_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")) @@ -420,6 +638,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None cohort_key = f"{resolved_playbook_id}::{doctrine_completeness}" promotion_reasons = [ *[str(item) for item in list(playbook_gate.get("reasons") or []) if item], + *([] if liquidity_status.get("passed") else [str(item) for item in list(liquidity_status.get("reasons") or []) if item]), *([] if quality_passed else quality_reasons), ] promotion_reasons = _dedupe(promotion_reasons) @@ -433,10 +652,13 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None "cohort_key_fields": ["setup_playbook_id", "doctrine_completeness"], "quality_gate_passed": quality_passed, "playbook_gate": playbook_gate, + "liquidity_kernel_status": liquidity_status, + "liquidity_backtest_features": dict(liquidity_status.get("features") or {}), "promotion_gate": { "passed": not promotion_reasons, "reasons": promotion_reasons, "requires_playbook_schema_complete": True, + "requires_liquidity_kernel_complete": True, "requires_backtest_quality_gate": True, "requires_active_trading_plan": True, "execution_boundary": "read_only_analysis_no_order_submission", @@ -465,6 +687,7 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: "quality_gate_passed_count": 0, "promotable_count": 0, "promotion_blocker_counts": {}, + "liquidity_status_counts": {}, }, ) bucket["run_count"] += 1 @@ -472,6 +695,8 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: bucket["quality_gate_passed_count"] += 1 if (cohort.get("promotion_gate") or {}).get("passed"): bucket["promotable_count"] += 1 + liquidity_status = str((cohort.get("liquidity_kernel_status") or {}).get("status") or "unknown") + bucket["liquidity_status_counts"][liquidity_status] = int(bucket["liquidity_status_counts"].get(liquidity_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 @@ -487,6 +712,146 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: } +def build_liquidity_backtest_features(payload: object) -> dict: + features = { + "liquidity_target_side": _string_or_empty( + _extract_first_present_value( + payload, + keys=("liquidity_target_side", "target_side", "primary_draw_side", "draw_side"), + ) + ), + "working_draw": _string_or_empty( + _extract_first_present_value( + payload, + keys=("working_draw", "current_working_draw", "primary_draw", "primary_draw_id"), + ) + ), + "secondary_draw": _string_or_empty(_extract_first_present_value(payload, keys=("secondary_draw", "secondary_draw_id"))), + "observation_draw": _string_or_empty(_extract_first_present_value(payload, keys=("observation_draw", "observation_draw_id"))), + "target_layer": _string_or_empty(_extract_first_present_value(payload, keys=("target_layer", "draw_layer", "irl_erl_layer"))), + "liquidity_pool_id": _string_or_empty(_extract_first_present_value(payload, keys=("liquidity_pool_id", "pool_id", "primary_pool_id"))), + "liquidity_pool_rank": _string_or_empty(_extract_first_present_value(payload, keys=("liquidity_pool_rank", "pool_rank", "target_rank"))), + "pool_quality_score": _extract_first_present_value(payload, keys=("pool_quality_score", "liquidity_pool_score")), + "liquidity_event": _string_or_empty(_extract_first_present_value(payload, keys=("liquidity_event", "liquidity_action", "event_type"))), + "swept_level": _extract_first_present_value(payload, keys=("swept_level", "sweep_level", "taken_level")), + "sweep_confirmed": _extract_first_present_value(payload, keys=("sweep_confirmed", "liquidity_sweep_confirmed")), + "raid_stop_run_kind": _string_or_empty(_extract_first_present_value(payload, keys=("raid_stop_run_kind", "stop_run_kind", "sweep_kind", "raid_kind"))), + "post_take_reaction": _string_or_empty( + _extract_first_present_value( + payload, + keys=("post_take_reaction", "post_sweep_reaction", "reclaim_state", "acceptance_state", "displacement_state"), + ) + ), + "manipulation_window_state": _string_or_empty( + _extract_first_present_value(payload, keys=("manipulation_window_state", "judas_state", "inducement_state")) + ), + "session_code": _string_or_empty(_extract_first_present_value(payload, keys=("session_code", "session"))), + "inducement_side": _string_or_empty(_extract_first_present_value(payload, keys=("inducement_side", "used_side"))), + "served_side": _string_or_empty(_extract_first_present_value(payload, keys=("served_side", "served_draw_side"))), + "trigger_pool_id": _string_or_empty(_extract_first_present_value(payload, keys=("trigger_pool_id", "trigger_liquidity_pool_id"))), + "liquidity_void_id": _string_or_empty(_extract_first_present_value(payload, keys=("liquidity_void_id", "void_id"))), + "void_displacement_state": _string_or_empty(_extract_first_present_value(payload, keys=("void_displacement_state", "void_state"))), + "rebalancing_depth": _extract_first_present_value(payload, keys=("rebalancing_depth", "void_rebalance_depth")), + "reorder_trigger_type": _string_or_empty(_extract_first_present_value(payload, keys=("reorder_trigger_type", "reorder_trigger"))), + "reorder_evidence": _string_or_empty(_extract_first_present_value(payload, keys=("reorder_evidence", "reorder_reason"))), + } + has_action = _has_liquidity_action(features) + features["has_liquidity_action"] = has_action + features["has_ranked_target"] = bool(features["liquidity_target_side"] and features["liquidity_pool_id"]) + features["has_post_take_reaction"] = bool(features["post_take_reaction"]) + return features + + +def evaluate_liquidity_kernel_status(payload: object) -> dict: + features = build_liquidity_backtest_features(payload) + no_trade_reasons: list[str] = [] + wait_reasons: list[str] = [] + if not features["liquidity_target_side"] and not features["working_draw"]: + no_trade_reasons.append("missing_liquidity_target") + if not features["liquidity_pool_id"]: + no_trade_reasons.append("missing_liquidity_pool") + elif not features["liquidity_pool_rank"]: + wait_reasons.append("unranked_liquidity_pool") + if features["has_liquidity_action"] and (not features["has_ranked_target"] or not features["has_post_take_reaction"]): + no_trade_reasons.append("sweep_is_not_setup") + if features["has_liquidity_action"] and not features["has_post_take_reaction"]: + wait_reasons.append("awaiting_post_sweep_displacement") + if not features["has_liquidity_action"]: + wait_reasons.append("awaiting_liquidity_event") + manipulation_state = str(features["manipulation_window_state"] or "").lower() + if manipulation_state in {"active", "inducement", "judas", "manipulation", "conditioning", "pre_confirmation"}: + wait_reasons.append("manipulation_window_wait") + if not features["working_draw"]: + no_trade_reasons.append("missing_working_draw") + if _looks_like_equal_priority(features): + wait_reasons.append("liquidity_targets_tied_wait") + if features["liquidity_void_id"] and not features["trigger_pool_id"]: + no_trade_reasons.append("void_without_trigger_pool") + if features["reorder_trigger_type"] and not features["reorder_evidence"]: + wait_reasons.append("reorder_trigger_pending") + + reasons = _dedupe([*no_trade_reasons, *wait_reasons]) + passed = not reasons + if passed: + status = "liquidity_mapped" + elif no_trade_reasons: + status = "study_only" + else: + status = "wait" + return { + "phase": DOCTRINE_LIQUIDITY_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": ( + "Liquidity target, pool rank, action, post-take reaction, and Working Draw are mapped." + if passed + else "Treat the liquidity event as study-only/wait until target side, ranked pool, post-take reaction, and Working Draw are explicit." + ), + "execution_boundary": "read_only_analysis_no_order_submission", + } + + +def summarize_liquidity_kernel_statuses(items: Iterable[dict]) -> dict: + reason_counts: dict[str, int] = {} + status_counts: dict[str, int] = {} + target_side_counts: dict[str, int] = {} + total = 0 + passed_count = 0 + for item in items: + total += 1 + status = dict(item.get("liquidity_kernel_status") or evaluate_liquidity_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 {}) + target_side = str(features.get("liquidity_target_side") or "missing_liquidity_target") + target_side_counts[target_side] = int(target_side_counts.get(target_side) 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_LIQUIDITY_PHASE, + "item_count": total, + "passed_count": passed_count, + "blocked_count": max(0, total - passed_count), + "status_counts": status_counts, + "reason_counts": reason_counts, + "target_side_counts": target_side_counts, + "operator_summary": ( + "No backtest items available for Liquidity Kernel review." + if not total + else "Review Liquidity Kernel blockers before treating any sweep/raid as usable evidence." + ), + } + + def evaluate_doctrine_research_status( payload: object, *, @@ -618,6 +983,23 @@ def _extract_first_value(payload: object, *, keys: tuple[str, ...]): return None +def _extract_first_present_value(payload: object, *, keys: tuple[str, ...]): + if isinstance(payload, dict): + for key in keys: + if key in payload and payload.get(key) is not None and payload.get(key) != "": + return payload.get(key) + for value in payload.values(): + found = _extract_first_present_value(value, keys=keys) + if found is not None and found != "": + return found + if isinstance(payload, list): + for value in payload: + found = _extract_first_present_value(value, keys=keys) + if found is not None and found != "": + return found + return None + + def _missing_playbook_sections(playbook: dict) -> list[str]: required_sections = ( "range_definition", @@ -672,3 +1054,31 @@ def _dedupe(values: Iterable[str]) -> list[str]: seen.add(key) result.append(key) return result + + +def _string_or_empty(value: object) -> str: + if value is None: + return "" + if isinstance(value, (dict, list, tuple)): + try: + return json.dumps(value, ensure_ascii=False, sort_keys=True, default=str) + except TypeError: + return str(value) + return str(value) + + +def _has_liquidity_action(features: dict) -> bool: + if features.get("sweep_confirmed") is True: + return True + if features.get("swept_level") is not None: + return True + text = " ".join( + str(features.get(key) or "").lower() + for key in ("liquidity_event", "raid_stop_run_kind", "post_take_reaction") + ) + return any(token in text for token in ("sweep", "raid", "stop_run", "stop run", "stop_hunt", "liquidity take")) + + +def _looks_like_equal_priority(features: dict) -> bool: + text = " ".join(str(features.get(key) or "").lower() for key in ("working_draw", "liquidity_pool_rank", "reorder_trigger_type")) + return any(token in text for token in ("equal_priority", "two_sided_equal", "tied", "both_sides_equal")) diff --git a/src/web/app.js b/src/web/app.js index bb3d4f7..6f7269e 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -5528,6 +5528,17 @@ function renderStrategyBacktests(payload) { const operatorSummary = payload?.operator_summary || {}; const doctrineCohort = latest.doctrine_backtest_cohort || summary.doctrine_backtest_cohort || {}; const doctrinePromotionGate = doctrineCohort.promotion_gate || {}; + const liquidityStatus = + latest.liquidity_kernel_status || + summary.liquidity_kernel_status || + doctrineCohort.liquidity_kernel_status || + {}; + const liquidityFeatures = + latest.liquidity_backtest_features || + summary.liquidity_backtest_features || + doctrineCohort.liquidity_backtest_features || + liquidityStatus.features || + {}; strategyBacktestPanel.appendChild( buildInfoGrid([ @@ -5538,6 +5549,10 @@ function renderStrategyBacktests(payload) { ['Setup Playbook', doctrineCohort.setup_playbook_id || 'missing_playbook'], ['Doctrine Cohort', doctrineCohort.cohort_key || 'missing_playbook::incomplete'], ['Doctrine Completeness', doctrineCohort.doctrine_completeness || 'incomplete'], + ['Liquidity Kernel', formatBacktestDecision(liquidityStatus.status || 'study_only')], + ['Liquidity Reason', formatBacktestDecision(liquidityStatus.reason || 'missing_liquidity_target')], + ['Liquidity Target', liquidityFeatures.liquidity_target_side || 'missing_liquidity_target'], + ['Liquidity Pool', liquidityFeatures.liquidity_pool_id || 'missing_liquidity_pool'], ['Promotion Gate', doctrinePromotionGate.passed ? 'Passed' : 'Blocked'], ['Instrument ID', latest.instrument_id ?? 'n/a'], ['Timeframe', formatJsonInline(latest.timeframe_set || {})], @@ -5566,6 +5581,13 @@ function renderStrategyBacktests(payload) { cards.appendChild( buildMetricCard('Quality Gate', passed ? 'Passed' : 'Blocked', formatList((qualityGate.reasons || []).map(formatBacktestDecision), 'No gate blockers')) ); + cards.appendChild( + buildMetricCard( + 'Liquidity Kernel', + liquidityStatus.passed ? 'Mapped' : 'Blocked', + formatList((liquidityStatus.reasons || []).map(formatBacktestDecision), 'Liquidity chain mapped') + ) + ); strategyBacktestPanel.appendChild(cards); strategyBacktestPanel.appendChild(buildBacktestCurve(summary.cumulative_r_curve || [])); @@ -5598,6 +5620,15 @@ function renderStrategyBacktests(payload) { ]) ) ); + detailGrid.appendChild( + buildItemCard( + 'Liquidity Kernel', + `${payload?.liquidity_kernel_summary?.passed_count ?? 0} mapped / ${payload?.liquidity_kernel_summary?.blocked_count ?? 0} blocked`, + topCountEntries(payload?.liquidity_kernel_summary?.reason_counts || summary.liquidity_kernel_reason_counts || {}) + .slice(0, 5) + .map((item) => [formatBacktestDecision(item.key), String(item.value)]) + ) + ); detailGrid.appendChild( buildItemCard( 'Recent Backtest Runs', diff --git a/src/web/i18n.js b/src/web/i18n.js index fca4caf..7e8ca8e 100644 --- a/src/web/i18n.js +++ b/src/web/i18n.js @@ -77,6 +77,13 @@ 'Top Sessions': '优势时段', 'Failure Reasons': '失败原因', 'Recent Backtest Runs': '最近回测运行', + 'Liquidity Kernel': '流动性内核', + 'Liquidity Reason': '流动性原因', + 'Liquidity Target': '流动性目标', + 'Liquidity Pool': '流动性池', + 'Liquidity chain mapped': '流动性链路已映射', + 'Mapped': '已映射', + 'mapped': '已映射', 'Eligible For Trading Plan Review': '可进入交易计划复核', 'Do Not Promote Strategy': '不要晋级策略', 'Do Not Trust Strategy Yet': '暂不信任策略', @@ -741,11 +748,30 @@ ['missing_doctrine_setup_playbook', '缺少 Doctrine setup playbook'], ['unknown_doctrine_setup_playbook', '未知 Doctrine setup playbook'], ['doctrine_setup_playbook_ready', 'Doctrine setup playbook 已就绪'], + ['liquidity_mapped', '流动性链路已映射'], + ['liquidity_kernel_blocked', '流动性内核阻塞'], + ['study_only', '仅研究'], + ['missing_liquidity_target', '缺少流动性目标'], + ['missing_liquidity_pool', '缺少流动性池'], + ['missing_working_draw', '缺少当前 Working Draw'], + ['sweep_is_not_setup', '扫流动性不是完整 setup'], + ['manipulation_window_wait', '诱导窗口等待'], + ['awaiting_post_sweep_displacement', '等待扫后位移/接管'], + ['awaiting_liquidity_event', '等待流动性事件'], + ['unranked_liquidity_pool', '流动性池未排序'], + ['liquidity_targets_tied_wait', '两侧流动性同权等待'], + ['void_without_trigger_pool', 'Void 缺少触发池'], + ['reorder_trigger_pending', '等待目标重排证据'], ['missing_playbook', '缺少 playbook'], ['Doctrine Cohort', 'Doctrine 回测分组'], ['Doctrine Cohorts', 'Doctrine 回测分组'], ['Setup Playbook', 'Setup Playbook'], ['Doctrine Completeness', 'Doctrine 完整度'], + ['Liquidity Kernel', '流动性内核'], + ['Liquidity Reason', '流动性原因'], + ['Liquidity Target', '流动性目标'], + ['Liquidity Pool', '流动性池'], + ['Liquidity chain mapped', '流动性链路已映射'], ['Promotion Gate', '晋级门'], ['complete', '完整'], ['incomplete', '不完整'], diff --git a/tests/unit/test_api_and_main.py b/tests/unit/test_api_and_main.py index 6f50bce..d5522d9 100644 --- a/tests/unit/test_api_and_main.py +++ b/tests/unit/test_api_and_main.py @@ -93,6 +93,8 @@ class ApiTests(unittest.TestCase): self.assertIn("backtest_evidence", result[0]["ict_rulebook_mapping"]["present_concepts"]) self.assertTrue(result[0]["doctrine_research_status"]["research_only"]) self.assertEqual(result[0]["doctrine_research_status"]["research_only_reason"], "missing_doctrine_setup_playbook") + self.assertEqual(result[0]["liquidity_kernel_status"]["status"], "study_only") + self.assertIn("missing_liquidity_target", result[0]["liquidity_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}}}} @@ -122,6 +124,8 @@ class ApiTests(unittest.TestCase): self.assertIn("sample_size_too_small", result["cockpit_promotion_gate"]["reasons"]) self.assertEqual(result["doctrine_rule_gap_summary"]["item_count"], 1) self.assertEqual(result["doctrine_rule_gap_summary"]["research_only_count"], 1) + self.assertEqual(result["liquidity_kernel_summary"]["item_count"], 1) + self.assertIn("missing_liquidity_target", result["liquidity_kernel_summary"]["reason_counts"]) def test_route_request_trading_doctrine_rules_returns_d1_matrix(self) -> None: status, payload = route_request( @@ -150,6 +154,22 @@ class ApiTests(unittest.TestCase): self.assertEqual(payload["phase"], "D2_PLAYBOOK_BACKTEST_COHORT") self.assertEqual(payload["playbooks"][0]["setup_playbook_id"], "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL") + def test_route_request_trading_liquidity_kernel_returns_d3_registry(self) -> None: + status, payload = route_request( + "/trading/liquidity-kernel", + {}, + dependencies={ + "trading_liquidity_kernel": lambda: { + "phase": "D3_LIQUIDITY_KERNEL", + "rules": [{"rule_id": "stop_run_sweep_raid_validated"}], + } + }, + ) + + self.assertEqual(status, 200) + self.assertEqual(payload["phase"], "D3_LIQUIDITY_KERNEL") + self.assertEqual(payload["rules"][0]["rule_id"], "stop_run_sweep_raid_validated") + 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 1f263a7..6de4b44 100644 --- a/tests/unit/test_api_server.py +++ b/tests/unit/test_api_server.py @@ -118,6 +118,22 @@ class ApiServerRoutingTests(unittest.TestCase): self.assertEqual(payload["phase"], "D2_PLAYBOOK_BACKTEST_COHORT") self.assertEqual(payload["playbook_count"], 1) + def test_routes_trading_liquidity_kernel(self) -> None: + status, payload = route_request( + "/trading/liquidity-kernel", + {}, + dependencies={ + "trading_liquidity_kernel": lambda: { + "phase": "D3_LIQUIDITY_KERNEL", + "rule_count": 6, + } + }, + ) + + self.assertEqual(status, 200) + self.assertEqual(payload["phase"], "D3_LIQUIDITY_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 5ff5a84..508f589 100644 --- a/tests/unit/test_backtest_service.py +++ b/tests/unit/test_backtest_service.py @@ -427,6 +427,14 @@ class BacktestOutcomeTests(unittest.TestCase): "expires_ts": datetime(2026, 5, 6, 4, 0, tzinfo=timezone.utc), "confirmation_state": {"executable": True}, "missing_confirmations": [], + "liquidity_target_side": "buy_side_liquidity", + "working_draw": "upper_external_pool", + "liquidity_pool_id": "bsl-old-high", + "liquidity_pool_rank": "primary", + "liquidity_event": "sweep", + "swept_level": 103.0, + "sweep_confirmed": True, + "post_take_reaction": "acceptance_displacement", "entry_low": 100.0, "entry_high": 100.2, "stop_loss": 99.0, @@ -458,6 +466,8 @@ class BacktestOutcomeTests(unittest.TestCase): self.assertEqual(units[0]["confirmed_ts"], rows[1]["created_ts"]) self.assertAlmostEqual(units[0]["rr_tp1"], 0.8181818181) self.assertEqual(units[0]["target_plan"]["tp2"]["objective_layer"], "ERL") + self.assertEqual(units[0]["liquidity_backtest_features"]["liquidity_target_side"], "buy_side_liquidity") + self.assertEqual(units[0]["liquidity_kernel_status"]["status"], "liquidity_mapped") class BacktestServiceRunTests(unittest.TestCase): diff --git a/tests/unit/test_documentation_consistency.py b/tests/unit/test_documentation_consistency.py index 4a9ddb8..5547b59 100644 --- a/tests/unit/test_documentation_consistency.py +++ b/tests/unit/test_documentation_consistency.py @@ -20,6 +20,7 @@ NONEXISTENT_VISUAL_PLAN_NAME = "AI_ICT_" + "视觉盯盘与主动告警增强方 ACTIVE_TOP_LEVEL_DOCS = { "AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md", "AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md", + "AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_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 e395308..8bb4fbc 100644 --- a/tests/unit/test_ict_doctrine.py +++ b/tests/unit/test_ict_doctrine.py @@ -2,12 +2,17 @@ import unittest from src.services.ict_mainline import ( DOCTRINE_KERNEL_PHASE, + DOCTRINE_LIQUIDITY_PHASE, DOCTRINE_PLAYBOOK_PHASE, build_doctrine_backtest_cohort, + build_liquidity_backtest_features, build_doctrine_playbook_gate, build_doctrine_playbook_registry, build_doctrine_rule_matrix, + build_liquidity_kernel_registry, evaluate_doctrine_research_status, + evaluate_liquidity_kernel_status, + summarize_liquidity_kernel_statuses, summarize_doctrine_rule_gaps, ) @@ -34,8 +39,79 @@ class IctDoctrineKernelTests(unittest.TestCase): self.assertEqual(playbook["setup_playbook_id"], "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL") self.assertIn("range_definition", playbook) self.assertIn("target_rule", playbook) + self.assertIn("liquidity_target_side", playbook["liquidity_event_rule"]["required_fields"]) + self.assertIn("post_take_reaction", playbook["liquidity_event_rule"]["required_fields"]) self.assertTrue(playbook["promotion_gate"]["requires_backtest_quality_gate"]) + def test_d3_liquidity_kernel_registry_exposes_target_ranking_rules(self) -> None: + registry = build_liquidity_kernel_registry() + + self.assertEqual(registry["phase"], DOCTRINE_LIQUIDITY_PHASE) + self.assertEqual(registry["status"], "active") + self.assertGreaterEqual(registry["rule_count"], 6) + self.assertIn("liquidity_target_side", registry["required_backtest_feature_fields"]) + self.assertIn("Stop Run / Sweep / Raid is an evidence component only, never a setup by itself.", registry["hard_rules"]) + + def test_sweep_without_ranked_target_stays_study_only(self) -> None: + status = evaluate_liquidity_kernel_status( + { + "liquidity_event": "sweep", + "swept_level": 100.5, + "sweep_confirmed": True, + } + ) + + self.assertFalse(status["passed"]) + self.assertEqual(status["phase"], DOCTRINE_LIQUIDITY_PHASE) + self.assertEqual(status["status"], "study_only") + self.assertIn("missing_liquidity_target", status["reasons"]) + self.assertIn("missing_liquidity_pool", status["reasons"]) + self.assertIn("sweep_is_not_setup", status["reasons"]) + + def test_complete_liquidity_kernel_payload_maps_without_execution_permission(self) -> None: + status = evaluate_liquidity_kernel_status( + { + "liquidity_target_side": "sell_side_liquidity", + "working_draw": "lower_external_pool", + "secondary_draw": "range_midpoint", + "observation_draw": "upper_bsl", + "liquidity_pool_id": "ssl-ny-low", + "liquidity_pool_rank": "primary", + "liquidity_event": "liquidity_sweep", + "swept_level": 99.5, + "sweep_confirmed": True, + "raid_stop_run_kind": "sweep", + "post_take_reaction": "reclaim_displacement", + "manipulation_window_state": "confirmed", + "reorder_trigger_type": "", + } + ) + + self.assertTrue(status["passed"]) + self.assertEqual(status["status"], "liquidity_mapped") + self.assertEqual(status["execution_boundary"], "read_only_analysis_no_order_submission") + + def test_liquidity_summary_counts_blockers(self) -> None: + summary = summarize_liquidity_kernel_statuses( + [ + {"liquidity_event": "sweep", "sweep_confirmed": True}, + { + "liquidity_target_side": "buy_side_liquidity", + "working_draw": "upper_external_pool", + "liquidity_pool_id": "bsl-old-high", + "liquidity_pool_rank": "primary", + "liquidity_event": "raid", + "swept_level": 105.0, + "post_take_reaction": "acceptance_displacement", + }, + ] + ) + + self.assertEqual(summary["item_count"], 2) + self.assertEqual(summary["passed_count"], 1) + self.assertGreater(summary["reason_counts"]["sweep_is_not_setup"], 0) + self.assertEqual(summary["target_side_counts"]["buy_side_liquidity"], 1) + def test_playbook_gate_blocks_missing_and_passes_registered_schema(self) -> None: missing = build_doctrine_playbook_gate({"model_code": "SWEEP_MSS_FVG"}) @@ -115,6 +191,14 @@ class IctDoctrineKernelTests(unittest.TestCase): cohort = build_doctrine_backtest_cohort( { "setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL", + "liquidity_target_side": "sell_side_liquidity", + "working_draw": "lower_external_pool", + "liquidity_pool_id": "ssl-ny-low", + "liquidity_pool_rank": "primary", + "liquidity_event": "sweep", + "swept_level": 99.5, + "sweep_confirmed": True, + "post_take_reaction": "reclaim_displacement", "summary": {"quality_gate": {"passed": True, "reasons": []}}, } ) @@ -122,11 +206,33 @@ class IctDoctrineKernelTests(unittest.TestCase): self.assertEqual(cohort["cohort_key"], "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL::complete") self.assertEqual(cohort["doctrine_completeness"], "complete") self.assertTrue(cohort["promotion_gate"]["passed"]) + self.assertEqual(cohort["liquidity_kernel_status"]["status"], "liquidity_mapped") missing = build_doctrine_backtest_cohort({"model_code": "SWEEP_MSS_FVG"}) self.assertEqual(missing["cohort_key"], "missing_playbook::incomplete") self.assertIn("missing_doctrine_setup_playbook", missing["promotion_gate"]["reasons"]) + self.assertIn("missing_liquidity_target", missing["promotion_gate"]["reasons"]) + + def test_liquidity_backtest_features_extract_nested_candidate_fields(self) -> None: + features = build_liquidity_backtest_features( + { + "candidate": { + "entry": { + "liquidity_target_side": "buy_side_liquidity", + "liquidity_pool_id": "bsl-old-high", + }, + "confirmation_state": {"post_take_reaction": "acceptance_displacement"}, + }, + "sweep_confirmed": False, + "liquidity_pool_rank": "primary", + } + ) + + self.assertEqual(features["liquidity_target_side"], "buy_side_liquidity") + self.assertEqual(features["liquidity_pool_id"], "bsl-old-high") + self.assertEqual(features["liquidity_pool_rank"], "primary") + self.assertFalse(features["sweep_confirmed"]) if __name__ == "__main__":