Add doctrine liquidity kernel
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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 质量分层比较绩效。
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', '不完整'],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user