Add doctrine liquidity kernel

This commit is contained in:
Codex
2026-05-21 20:51:20 +08:00
parent dfef7b951c
commit 0749d886c4
15 changed files with 880 additions and 6 deletions
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
@@ -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 质量分层比较绩效。
+36
View File
@@ -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:
+4 -1
View File
@@ -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:
+84 -1
View File
@@ -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
+12
View File
@@ -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",
]
+412 -2
View File
@@ -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"))
+31
View File
@@ -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',
+26
View File
@@ -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', '不完整'],
+20
View File
@@ -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",
+16
View File
@@ -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",
+10
View File
@@ -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):
@@ -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",
+106
View File
@@ -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__":