Rebuild ICT doctrine rule kernel
This commit is contained in:
+2
-2
@@ -845,11 +845,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- S15 R2 paper gate live polling is complete with a real no-trade blocker: after S14, remote runtime was triggered again and then polled through three short-window attempts. Runtime stayed usable with `current_blockers=[]` and `watch_market_input_mode=rest_fallback`; R2 validation stayed correctly at `status=collecting_paper_samples`, `next_validation_gate=paper_ticket_sample_gate`, `operator_feedback_count=5`, `paper_ticket_count=0`, `paper_execution_count=0`, and `weekly_review_entry_count=0`. The newest real candidate advanced to Candidate `4615`, but it remained `blocked` / `executable=false` because `fvg_retrace_confirmed` is still missing and `fvg_direction_mismatch` is rejected. No paper ticket, paper execution, or weekly review evidence was created, because blocked candidates must remain observation/no-trade only.
|
||||
- S16 batch historical backtest validation is complete: Strategy Backtest now has `scripts/run_backtest_batch.py` for matrix runs across instrument / subject / timeframe / session / candidate model, and `BacktestService.run_backtest` persists `run_metadata.batch_id` while keeping the read-only boundary `read_only_backtest_no_order_submission`. Local focused backtest tests, `py_compile`, script static checks (`79` scripts), documentation consistency, full unit discovery (`814` tests), and `git diff --check` passed. Local batch smoke correctly blocked on the known local DB privilege issue for `trading_plans`, so the decisive validation was remote: deploy smoke passed 11 endpoints, remote ops smoke and readonly live analysis smoke passed, remote smoke batch `S16-remote-20260521-a` wrote run ids `490-495`, and the larger remote matrix `S16-remote-20260521-b` wrote run ids `496-513`. The larger batch ran `18` historical no-lookahead runs across 3 timeframe sets and 5 session groups with `211` total results and `156` executed signals; `0/18` passed the quality gate. Main blockers were `expectancy_below_threshold=18`, `profit_factor_below_threshold=18`, `executed_sample_too_small=16`, and `sample_size_too_small=15`; failure reasons were dominated by `stopped_out=124`, `outside_session_window=126`, `no_future_candles=54`, and `same_candle_entry_exit_ambiguous=30`. `/trading/backtests?instrument_id=1&limit=18` returned latest id `513` and batch metadata `S16-remote-20260521-b` for ids `496-513`, so the existing Strategy Backtest page can read the batch output. This proves the batch backtest capability is usable, but the current strategy evidence is not good enough to promote into Trader Cockpit `Review Now`.
|
||||
- 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.
|
||||
- Current assisted-analysis next gate: keep R2 executable-candidate waiting in the background; do not collect duplicate operator feedback or fabricate paper samples. The active product work is now D1 `Market Language Kernel`: extract the first Doctrine Rule matrix from 《ICT交易全书》第 1-6 章, build the first `Range / Draw / Location / Time / Invalidation` registry, and mark old candidates research-only whenever those fields are missing. Do not add order-submission features, do not restart external-context/archive/digest long-tail work, do not physically delete S1 deprecated surfaces yet, and keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
|
||||
- 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.
|
||||
|
||||
## Exact Next Step
|
||||
|
||||
Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_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 D1 `Market Language Kernel`: extract Doctrine Rules from chapters 1-6, produce the first `Range / Draw / Location / Time / Invalidation` rule registry, and update the existing candidate/backtest/cockpit language so old sweep/MSS/FVG/retrace candidates are clearly `research_only` unless they map to a full setup playbook and pass historical gates. Keep R2 executable-candidate waiting in the background; do not keep clicking feedback now that `operator_feedback_count=5/5`; do not restart external-context/archive/digest long-tail work, do not create 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/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 D2: define the first real Doctrine setup playbook schema, make Strategy Backtest cohorts use `setup_playbook_id + doctrine completeness` rather than old `model_code` alone, and keep Trader Cockpit `Review Now` blocked unless playbook mapping, historical quality gate, and active Trading Plan all pass. 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
|
||||
|
||||
|
||||
@@ -952,7 +952,8 @@
|
||||
- [ ] R2 paper-ticket sample gate is waiting on a real executable candidate; do not fabricate paper tickets from blocked candidates
|
||||
- [x] R2 operator feedback sample gate satisfied with distinct real samples (`operator_feedback_count=5/5`); do not keep clicking feedback to pad counts
|
||||
- [ ] R2 weekly review visible gate still needs a real execution-derived journal/weekly review entry (`weekly_review_entry_count=0`)
|
||||
- [ ] D1 next product gate: extract 《ICT交易全书》第 1-6 章 into a Doctrine Rule matrix, build the first `Range / Draw / Location / Time / Invalidation` registry, and make old candidates expose `research_only_reason` when any required book-field is missing; no live order submission and no fabricated samples
|
||||
- [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
|
||||
- [ ] D2 next product gate: define the first real Doctrine setup playbook schema, make Strategy Backtest cohorts use `setup_playbook_id + doctrine completeness`, and keep `Review Now` blocked unless playbook mapping, historical quality gate, and active Trading Plan all pass
|
||||
- [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,45 @@
|
||||
# AI_ICT Doctrine Kernel 市场语言规则内核 D1 2026-05-21 v1
|
||||
|
||||
## 1. 本阶段结论
|
||||
|
||||
D1 已开始把《ICT交易全书》第 1-6 章从“表面形态识别”重建为可检查的市场语言规则内核。
|
||||
|
||||
当前系统中的 `sweep / MSS / FVG / retrace` 只能作为 evidence component。除非候选同时映射到完整 Doctrine setup playbook,并具备 `Range / Draw / Location / Context / Time / Invalidation / IRL-ERL` 链路,否则一律标记为 `research_only`,不得进入 Trader Cockpit 的 `Review Now`。
|
||||
|
||||
## 2. D1 Rule Registry
|
||||
|
||||
第一版规则注册表由 `src/services/ict_mainline/doctrine.py` 提供,当前包含:
|
||||
|
||||
| Stage | Rule | 缺失时原因 |
|
||||
| --- | --- | --- |
|
||||
| Range | 必须定义 Dealing Range、key high / key low、equilibrium | `doctrine_missing_range` |
|
||||
| Draw | 必须定义主 Draw、方向、目标流动性 | `doctrine_missing_draw` |
|
||||
| Location | 必须定义 premium / discount / OTE 或等价位置优势 | `doctrine_missing_location` |
|
||||
| Context | 必须定义 context、framework、reference point、setup type | `doctrine_missing_context_reference` |
|
||||
| Path / Time | 必须定义可用 session、路径、序列有效性 | `doctrine_missing_time_path` |
|
||||
| Invalidation | 必须定义失效条件、止损或失效价位 | `doctrine_missing_invalidation` |
|
||||
| IRL / ERL | 必须定义分层目标、管理和退出架构 | `doctrine_missing_exit_architecture` |
|
||||
|
||||
额外硬门槛:候选必须带有 `setup_playbook_id` / `doctrine_setup_id` / `playbook_id` 之一,否则首先返回 `missing_doctrine_setup_playbook`。
|
||||
|
||||
## 3. 接入点
|
||||
|
||||
- `GET /trading/doctrine-rules`:返回当前 D1 Doctrine Rule Matrix。
|
||||
- Strategy Backtest:每条 backtest summary 增加 `doctrine_research_status`,整体 surface 增加 `doctrine_rule_gap_summary`。
|
||||
- Trader Cockpit / Operator Workspace:候选 watchlist 增加 `doctrine_research_status`、`research_only`、`research_only_reason`,并把 Doctrine 缺失项加入 Review Now 阻断条件。
|
||||
- Trader Readout:当 focus 为 research-only 时,首屏语言改成“研究材料 / 不能交易 / 等完整 playbook 映射”。
|
||||
|
||||
## 4. 不变边界
|
||||
|
||||
- 不开启下单。
|
||||
- 不改变 `execution_allowed`、`decision_support_ready`、`real_live_ready`。
|
||||
- 不改变 risk governor、trust bucket、operator confirmation protocol。
|
||||
- 不伪造 R2 paper ticket / paper execution / operator feedback 样本。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
D2 应该把 Trading Plan 与 Backtest Cohort 也改成 Doctrine setup playbook 驱动:
|
||||
|
||||
1. 为真实 playbook 定义最小字段 schema。
|
||||
2. 把 Strategy Backtest 的 cohort key 从旧 `model_code` 迁移到 `setup_playbook_id + doctrine rule completeness`。
|
||||
3. Trader Cockpit 只允许“playbook 完整 + 历史质量门通过 + 计划激活”的候选进入 `Review Now`。
|
||||
+27
-3
@@ -5,7 +5,12 @@ from sqlalchemy import text
|
||||
|
||||
from src.db.session import SessionLocal
|
||||
from src.services.backtest import evaluate_backtest_quality
|
||||
from src.services.ict_mainline import build_backtest_quality_gate, build_ict_rulebook_mapping
|
||||
from src.services.ict_mainline import (
|
||||
build_backtest_quality_gate,
|
||||
build_ict_rulebook_mapping,
|
||||
evaluate_doctrine_research_status,
|
||||
summarize_doctrine_rule_gaps,
|
||||
)
|
||||
|
||||
|
||||
def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: int = 10) -> dict:
|
||||
@@ -24,6 +29,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in
|
||||
"items": items,
|
||||
},
|
||||
)
|
||||
doctrine_rule_gap_summary = summarize_doctrine_rule_gaps(items)
|
||||
return {
|
||||
"instrument_id": instrument_id,
|
||||
"item_count": len(items),
|
||||
@@ -31,6 +37,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in
|
||||
"quality_gate": quality_gate,
|
||||
"operator_summary": _build_operator_summary(latest),
|
||||
"cockpit_promotion_gate": build_backtest_quality_gate(quality_gate),
|
||||
"doctrine_rule_gap_summary": doctrine_rule_gap_summary,
|
||||
"ict_rulebook_mapping": ict_rulebook_mapping,
|
||||
"items": items,
|
||||
}
|
||||
@@ -83,12 +90,24 @@ def normalize_backtest_summary_row(row: dict) -> dict:
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
doctrine_research_status = evaluate_doctrine_research_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,
|
||||
},
|
||||
require_setup_playbook=True,
|
||||
)
|
||||
return {
|
||||
**row,
|
||||
"timeframe_set": timeframe_set,
|
||||
"config": config,
|
||||
"summary": summary,
|
||||
"ict_rulebook_mapping": ict_rulebook_mapping,
|
||||
"doctrine_research_status": doctrine_research_status,
|
||||
}
|
||||
|
||||
|
||||
@@ -105,18 +124,23 @@ def _build_operator_summary(latest: Optional[dict]) -> dict:
|
||||
passed = bool(quality_gate.get("passed"))
|
||||
if passed:
|
||||
status = "quality_gate_passed"
|
||||
decision = "eligible_for_trading_plan_review"
|
||||
decision = "study_only_until_doctrine_playbook"
|
||||
reason = "sample_and_risk_metrics_passed"
|
||||
else:
|
||||
status = "quality_gate_blocked"
|
||||
decision = "do_not_promote_strategy"
|
||||
reasons = quality_gate.get("reasons") or []
|
||||
reason = str(reasons[0]) if reasons else "quality_gate_not_passed"
|
||||
doctrine_status = dict(latest.get("doctrine_research_status") or {})
|
||||
if doctrine_status.get("research_only"):
|
||||
status = "doctrine_research_only"
|
||||
decision = "do_not_promote_strategy"
|
||||
reason = str(doctrine_status.get("research_only_reason") or reason)
|
||||
return {
|
||||
"status": status,
|
||||
"decision": decision,
|
||||
"reason": reason,
|
||||
"next_action": "review_failure_reasons_and_session_edges",
|
||||
"next_action": "map_to_doctrine_setup_playbook_before_promotion",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ from src.api.training import (
|
||||
parse_quality_sort_query,
|
||||
submit_training_attempt,
|
||||
)
|
||||
from src.services.ict_mainline import build_doctrine_rule_matrix
|
||||
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
|
||||
@@ -221,6 +222,7 @@ def build_api_dependencies() -> dict:
|
||||
"trading_weekly_review": get_weekly_review,
|
||||
"trading_plans": get_recent_trading_plans,
|
||||
"trading_backtests": get_strategy_backtest_surface,
|
||||
"trading_doctrine_rules": build_doctrine_rule_matrix,
|
||||
"training_cases": get_training_cases,
|
||||
"training_case": get_training_case,
|
||||
"training_export_cases": export_training_case_package,
|
||||
@@ -842,6 +844,8 @@ def route_request(
|
||||
instrument_id = _get_optional_int(query, "instrument_id")
|
||||
limit = _get_int(query, "limit", 10)
|
||||
return 200, deps["trading_backtests"](instrument_id=instrument_id, limit=limit)
|
||||
if method == "GET" and path == "/trading/doctrine-rules":
|
||||
return 200, deps["trading_doctrine_rules"]()
|
||||
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:
|
||||
|
||||
@@ -17,9 +17,18 @@ from src.services.ict_mainline.feedback import (
|
||||
classify_book_rule_error,
|
||||
summarize_book_rule_errors,
|
||||
)
|
||||
from src.services.ict_mainline.doctrine import (
|
||||
DOCTRINE_KERNEL_PHASE,
|
||||
build_doctrine_rule_matrix,
|
||||
evaluate_doctrine_research_status,
|
||||
list_doctrine_rules,
|
||||
summarize_doctrine_rule_gaps,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DOCTRINE_KERNEL_PHASE",
|
||||
"ICT_MAINLINE_CHAIN",
|
||||
"build_doctrine_rule_matrix",
|
||||
"build_ict_rulebook_mapping",
|
||||
"build_ict_rulebook_summary",
|
||||
"build_backtest_quality_gate",
|
||||
@@ -29,7 +38,10 @@ __all__ = [
|
||||
"candidate_mainline_blockers",
|
||||
"classify_book_rule_error",
|
||||
"classify_ict_concepts",
|
||||
"evaluate_doctrine_research_status",
|
||||
"get_ict_concept",
|
||||
"list_doctrine_rules",
|
||||
"list_ict_concepts",
|
||||
"summarize_doctrine_rule_gaps",
|
||||
"summarize_book_rule_errors",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
import json
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
DOCTRINE_KERNEL_PHASE = "D1_MARKET_LANGUAGE_KERNEL"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DoctrineRule:
|
||||
rule_id: str
|
||||
stage: str
|
||||
book_anchor: str
|
||||
rule_statement: str
|
||||
required_inputs: tuple[str, ...]
|
||||
measurable_fields: tuple[str, ...]
|
||||
invalid_when: tuple[str, ...]
|
||||
no_trade_when: tuple[str, ...]
|
||||
journal_error_class: str
|
||||
backtest_observable: str
|
||||
research_only_reason: str
|
||||
aliases: tuple[str, ...]
|
||||
|
||||
def to_public_dict(self) -> dict:
|
||||
payload = asdict(self)
|
||||
for key in (
|
||||
"required_inputs",
|
||||
"measurable_fields",
|
||||
"invalid_when",
|
||||
"no_trade_when",
|
||||
"aliases",
|
||||
):
|
||||
payload[key] = list(payload[key])
|
||||
return payload
|
||||
|
||||
|
||||
D1_DOCTRINE_RULES: tuple[DoctrineRule, ...] = (
|
||||
DoctrineRule(
|
||||
rule_id="range_defined",
|
||||
stage="Range",
|
||||
book_anchor="ICT交易全书 chapters 1-2: dealing range and market structure context",
|
||||
rule_statement="未定义当前 Dealing Range 时,任何 FVG/MSS 都只能作为研究材料。",
|
||||
required_inputs=("key_high", "key_low", "equilibrium", "range_timeframe"),
|
||||
measurable_fields=("range_id", "range_high", "range_low", "equilibrium"),
|
||||
invalid_when=("range is absent", "range boundary is stale", "range timeframe is unresolved"),
|
||||
no_trade_when=("missing_key_high_low", "missing_equilibrium", "range_conflict_unresolved"),
|
||||
journal_error_class="traded_without_range",
|
||||
backtest_observable="setup_without_range_definition",
|
||||
research_only_reason="doctrine_missing_range",
|
||||
aliases=("dealing_range", "daily_range", "range_id", "range_high", "range_low", "key_high", "key_low", "equilibrium"),
|
||||
),
|
||||
DoctrineRule(
|
||||
rule_id="draw_defined",
|
||||
stage="Draw",
|
||||
book_anchor="ICT交易全书 chapters 1-3: draw on liquidity",
|
||||
rule_statement="未定义主 Draw 时,方向判断不得升级为可交易 setup。",
|
||||
required_inputs=("primary_draw", "draw_side", "objective_price"),
|
||||
measurable_fields=("primary_draw", "secondary_draw", "target_price", "liquidity_pool_id"),
|
||||
invalid_when=("draw target is missing", "draw conflicts with higher timeframe", "draw already fully handled"),
|
||||
no_trade_when=("missing_primary_draw", "draw_conflict_unresolved"),
|
||||
journal_error_class="traded_without_draw",
|
||||
backtest_observable="setup_without_primary_draw",
|
||||
research_only_reason="doctrine_missing_draw",
|
||||
aliases=("draw_on_liquidity", "primary_draw", "secondary_draw", "liquidity_target", "target_price", "objective_price"),
|
||||
),
|
||||
DoctrineRule(
|
||||
rule_id="location_quality_defined",
|
||||
stage="Location",
|
||||
book_anchor="ICT交易全书 chapters 2-4: premium/discount and OTE location",
|
||||
rule_statement="方向正确但位置错误时,候选只能研究,不能进入盘中 Review Now。",
|
||||
required_inputs=("premium_discount_zone", "equilibrium", "location_advantage"),
|
||||
measurable_fields=("premium_discount_zone", "location", "ote", "discount", "premium"),
|
||||
invalid_when=("long in premium without exception", "short in discount without exception", "equilibrium chop"),
|
||||
no_trade_when=("missing_location", "location_disadvantage", "equilibrium_noise"),
|
||||
journal_error_class="poor_trade_location",
|
||||
backtest_observable="setup_without_location_filter",
|
||||
research_only_reason="doctrine_missing_location",
|
||||
aliases=("premium_discount", "premium", "discount", "equilibrium", "ote", "location", "location_advantage"),
|
||||
),
|
||||
DoctrineRule(
|
||||
rule_id="context_reference_match",
|
||||
stage="Context",
|
||||
book_anchor="ICT交易全书 chapters 1-6: context, framework, and reference point",
|
||||
rule_statement="setup 必须由环境与参考点配对产生,不能由单个形态倒推出叙事。",
|
||||
required_inputs=("context", "framework", "reference_point", "setup_type"),
|
||||
measurable_fields=("setup_type", "model_code", "reference_point", "setup_timeframe", "trigger_timeframe"),
|
||||
invalid_when=("tool first narrative", "reference point missing", "context conflicts with setup type"),
|
||||
no_trade_when=("missing_context", "missing_reference_point", "context_reference_mismatch"),
|
||||
journal_error_class="tool_first_setup",
|
||||
backtest_observable="setup_without_context_reference_pair",
|
||||
research_only_reason="doctrine_missing_context_reference",
|
||||
aliases=("context", "framework", "reference_point", "setup_type", "model_code", "setup_timeframe", "trigger_timeframe"),
|
||||
),
|
||||
DoctrineRule(
|
||||
rule_id="path_time_defined",
|
||||
stage="Path / Time",
|
||||
book_anchor="ICT交易全书 chapters 3-6: time, path, and sequence validity",
|
||||
rule_statement="没有路径与时段约束时,局部确认不能说明 setup 有执行价值。",
|
||||
required_inputs=("session_code", "allowed_sessions", "path_context", "sequence_validity"),
|
||||
measurable_fields=("session_code", "allowed_sessions", "created_ts", "expires_ts", "path_context"),
|
||||
invalid_when=("outside allowed session", "sequence is incomplete", "path is unresolved"),
|
||||
no_trade_when=("missing_session_time", "outside_session_window", "path_unresolved"),
|
||||
journal_error_class="traded_without_time_or_path",
|
||||
backtest_observable="setup_without_session_or_path_filter",
|
||||
research_only_reason="doctrine_missing_time_path",
|
||||
aliases=("session_code", "allowed_sessions", "killzone", "created_ts", "expires_ts", "expiry_ts", "path_context", "sequence_validity"),
|
||||
),
|
||||
DoctrineRule(
|
||||
rule_id="invalidation_defined",
|
||||
stage="Invalidation",
|
||||
book_anchor="ICT交易全书 chapters 4-6: pre-trade invalidation checklist",
|
||||
rule_statement="没有明确失效条件时,候选不得被解释成可交易 setup。",
|
||||
required_inputs=("invalidations", "stop_loss", "invalid_level"),
|
||||
measurable_fields=("invalidations", "stop_loss", "management_rules", "broken_level"),
|
||||
invalid_when=("stop is absent", "invalidations are generic", "invalid level conflicts with range"),
|
||||
no_trade_when=("missing_invalidation", "generic_invalidation", "invalid_stop_structure"),
|
||||
journal_error_class="traded_without_invalidation",
|
||||
backtest_observable="setup_without_invalidation",
|
||||
research_only_reason="doctrine_missing_invalidation",
|
||||
aliases=("invalidation", "invalidations", "stop_loss", "invalidated", "broken_level", "reclaimed", "invalidate_on_reclaim"),
|
||||
),
|
||||
DoctrineRule(
|
||||
rule_id="exit_architecture_defined",
|
||||
stage="IRL / ERL",
|
||||
book_anchor="ICT交易全书 chapters 4-6: internal range and external range objectives",
|
||||
rule_statement="没有 IRL/ERL 分层退出架构时,setup 不能证明风险回报质量。",
|
||||
required_inputs=("tp1", "tp2", "target_plan", "irl", "erl"),
|
||||
measurable_fields=("tp1", "tp2", "rr_tp1", "rr_tp2", "target_plan", "management_rules"),
|
||||
invalid_when=("target missing", "only vague objective", "partial/full target not separated"),
|
||||
no_trade_when=("missing_target_architecture", "missing_irl_erl", "rr_unresolved"),
|
||||
journal_error_class="no_exit_architecture",
|
||||
backtest_observable="setup_without_irl_erl_targets",
|
||||
research_only_reason="doctrine_missing_exit_architecture",
|
||||
aliases=("tp1", "tp2", "rr_tp1", "rr_tp2", "target_plan", "management_rules", "irl", "erl", "partial_close"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def list_doctrine_rules() -> list[dict]:
|
||||
return [rule.to_public_dict() for rule in D1_DOCTRINE_RULES]
|
||||
|
||||
|
||||
def build_doctrine_rule_matrix() -> dict:
|
||||
return {
|
||||
"phase": DOCTRINE_KERNEL_PHASE,
|
||||
"source": "docs/ICT交易全书_出版终校版.md chapters 1-6",
|
||||
"status": "active",
|
||||
"required_chain": [
|
||||
"Range",
|
||||
"Draw",
|
||||
"Location",
|
||||
"Context",
|
||||
"Path / Time",
|
||||
"Invalidation",
|
||||
"IRL / ERL",
|
||||
],
|
||||
"rules": list_doctrine_rules(),
|
||||
"execution_boundary": "read_only_analysis_no_order_submission",
|
||||
}
|
||||
|
||||
|
||||
def evaluate_doctrine_research_status(
|
||||
payload: object,
|
||||
*,
|
||||
require_setup_playbook: bool = True,
|
||||
) -> dict:
|
||||
text = _payload_to_search_text(payload)
|
||||
rule_results = []
|
||||
missing_rules = []
|
||||
present_rules = []
|
||||
research_only_reasons = []
|
||||
missing_required_fields = []
|
||||
for rule in D1_DOCTRINE_RULES:
|
||||
present = _matches_any(text, rule.aliases)
|
||||
rule_result = {
|
||||
"rule_id": rule.rule_id,
|
||||
"stage": rule.stage,
|
||||
"status": "present" if present else "missing",
|
||||
"book_anchor": rule.book_anchor,
|
||||
"rule_statement": rule.rule_statement,
|
||||
"research_only_reason": rule.research_only_reason,
|
||||
"required_inputs": list(rule.required_inputs),
|
||||
"measurable_fields": list(rule.measurable_fields),
|
||||
"journal_error_class": rule.journal_error_class,
|
||||
"backtest_observable": rule.backtest_observable,
|
||||
}
|
||||
rule_results.append(rule_result)
|
||||
if present:
|
||||
present_rules.append(rule.rule_id)
|
||||
else:
|
||||
missing_rules.append(rule.rule_id)
|
||||
research_only_reasons.append(rule.research_only_reason)
|
||||
missing_required_fields.extend(rule.required_inputs)
|
||||
|
||||
playbook_id = _extract_first_value(
|
||||
payload,
|
||||
keys=("setup_playbook_id", "doctrine_setup_id", "playbook_id"),
|
||||
)
|
||||
if require_setup_playbook and not playbook_id:
|
||||
research_only_reasons.insert(0, "missing_doctrine_setup_playbook")
|
||||
|
||||
research_only = bool(research_only_reasons)
|
||||
return {
|
||||
"phase": DOCTRINE_KERNEL_PHASE,
|
||||
"status": "research_only" if research_only else "doctrine_mapped",
|
||||
"research_only": research_only,
|
||||
"research_only_reason": research_only_reasons[0] if research_only_reasons else "",
|
||||
"research_only_reasons": _dedupe(research_only_reasons),
|
||||
"present_rules": present_rules,
|
||||
"missing_rules": missing_rules,
|
||||
"missing_required_fields": _dedupe(missing_required_fields),
|
||||
"setup_playbook_id": playbook_id,
|
||||
"can_promote_to_cockpit": not research_only,
|
||||
"rule_results": rule_results,
|
||||
"operator_summary": (
|
||||
"Only research material: a full Doctrine setup playbook and complete Range/Draw/Location/Time/Invalidation chain are required."
|
||||
if research_only
|
||||
else "Doctrine market-language chain is mapped; still requires historical and operator gates before any live review."
|
||||
),
|
||||
"execution_boundary": "read_only_analysis_no_order_submission",
|
||||
}
|
||||
|
||||
|
||||
def summarize_doctrine_rule_gaps(items: Iterable[dict]) -> dict:
|
||||
reason_counts: dict[str, int] = {}
|
||||
missing_rule_counts: dict[str, int] = {}
|
||||
research_only_count = 0
|
||||
total = 0
|
||||
for item in items:
|
||||
total += 1
|
||||
status = dict(item.get("doctrine_research_status") or {})
|
||||
if not status:
|
||||
status = evaluate_doctrine_research_status(item)
|
||||
if status.get("research_only"):
|
||||
research_only_count += 1
|
||||
for reason in list(status.get("research_only_reasons") or []):
|
||||
key = str(reason)
|
||||
reason_counts[key] = int(reason_counts.get(key) or 0) + 1
|
||||
for rule_id in list(status.get("missing_rules") or []):
|
||||
key = str(rule_id)
|
||||
missing_rule_counts[key] = int(missing_rule_counts.get(key) or 0) + 1
|
||||
return {
|
||||
"phase": DOCTRINE_KERNEL_PHASE,
|
||||
"item_count": total,
|
||||
"research_only_count": research_only_count,
|
||||
"promotable_count": max(0, total - research_only_count),
|
||||
"research_only_reason_counts": reason_counts,
|
||||
"missing_rule_counts": missing_rule_counts,
|
||||
"operator_summary": (
|
||||
"No stored item is Doctrine-promotable yet; treat current evidence as study material."
|
||||
if total and research_only_count == total
|
||||
else "Review Doctrine gap counts before promoting any setup."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _payload_to_search_text(payload: object) -> str:
|
||||
try:
|
||||
raw = json.dumps(payload, ensure_ascii=False, default=str)
|
||||
except TypeError:
|
||||
raw = str(payload)
|
||||
return raw.lower().replace("-", "_").replace(" ", "_")
|
||||
|
||||
|
||||
def _matches_any(text: str, aliases: Iterable[str]) -> bool:
|
||||
for alias in aliases:
|
||||
token = str(alias or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||
if not token:
|
||||
continue
|
||||
pattern = rf"(?<![a-z0-9]){re.escape(token)}(?![a-z0-9])"
|
||||
if re.search(pattern, text):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _extract_first_value(payload: object, *, keys: tuple[str, ...]):
|
||||
if isinstance(payload, dict):
|
||||
for key in keys:
|
||||
if payload.get(key):
|
||||
return payload.get(key)
|
||||
for value in payload.values():
|
||||
found = _extract_first_value(value, keys=keys)
|
||||
if found:
|
||||
return found
|
||||
if isinstance(payload, list):
|
||||
for value in payload:
|
||||
found = _extract_first_value(value, keys=keys)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def _dedupe(values: Iterable[str]) -> list[str]:
|
||||
result = []
|
||||
seen = set()
|
||||
for value in values:
|
||||
key = str(value or "").strip()
|
||||
if not key or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append(key)
|
||||
return result
|
||||
@@ -20,6 +20,7 @@ from src.services.ict_mainline import (
|
||||
build_review_now_mainline_gate,
|
||||
classify_book_rule_error,
|
||||
candidate_mainline_blockers,
|
||||
evaluate_doctrine_research_status,
|
||||
summarize_book_rule_errors,
|
||||
)
|
||||
from src.services.journal import WeeklyReviewService
|
||||
@@ -2592,11 +2593,13 @@ def _build_candidate_watchlist_tiers(
|
||||
}
|
||||
for candidate in candidates:
|
||||
session_item = session_items.get(str(candidate.get("session_code") or ""), {})
|
||||
doctrine_status = evaluate_doctrine_research_status(candidate, require_setup_playbook=True)
|
||||
tier, tier_reason, priority_score, mainline_blockers = _classify_candidate_watchlist_tier(
|
||||
candidate=candidate,
|
||||
session_item=session_item,
|
||||
current_candidate_id=current_candidate_id,
|
||||
mainline_focus_gate=mainline_focus_gate,
|
||||
doctrine_status=doctrine_status,
|
||||
)
|
||||
missing_confirmations = _dedupe(
|
||||
[
|
||||
@@ -2615,6 +2618,9 @@ def _build_candidate_watchlist_tiers(
|
||||
"review_now_allowed": tier == "review_now" and not mainline_blockers,
|
||||
"mainline_gate": mainline_focus_gate,
|
||||
"mainline_gate_blockers": mainline_blockers,
|
||||
"doctrine_research_status": doctrine_status,
|
||||
"research_only": bool(doctrine_status.get("research_only")),
|
||||
"research_only_reason": doctrine_status.get("research_only_reason") or "",
|
||||
"suggested_action": _candidate_tier_action_label(tier=tier, candidate_id=candidate.get("id")),
|
||||
"action_path": _candidate_tier_action_path(tier=tier, candidate_id=candidate.get("id")),
|
||||
"action_method": _candidate_tier_action_method(tier=tier),
|
||||
@@ -2735,6 +2741,7 @@ def _classify_candidate_watchlist_tier(
|
||||
session_item: dict,
|
||||
current_candidate_id,
|
||||
mainline_focus_gate: dict,
|
||||
doctrine_status: dict | None = None,
|
||||
) -> tuple[str, str, float, list[str]]:
|
||||
score = float(candidate.get("quality_score") or 0.0)
|
||||
status = str(candidate.get("status") or candidate.get("candidate_state") or "")
|
||||
@@ -2742,7 +2749,15 @@ def _classify_candidate_watchlist_tier(
|
||||
recommendation = dict(session_item.get("session_recommendation") or {})
|
||||
session_score = float(recommendation.get("score") or 0.0)
|
||||
missing_count = len(list(candidate.get("missing_confirmations") or []))
|
||||
mainline_blockers = candidate_mainline_blockers(candidate, mainline_focus_gate)
|
||||
doctrine_blockers = [
|
||||
str(item)
|
||||
for item in list((doctrine_status or {}).get("research_only_reasons") or [])
|
||||
if item
|
||||
]
|
||||
mainline_blockers = _dedupe([
|
||||
*candidate_mainline_blockers(candidate, mainline_focus_gate),
|
||||
*doctrine_blockers,
|
||||
])
|
||||
priority_score = score
|
||||
if candidate.get("id") == current_candidate_id:
|
||||
priority_score += 120.0
|
||||
@@ -3422,6 +3437,9 @@ def _resolve_trader_mode_focus_candidate(
|
||||
"review_now_allowed": bool(focus.get("review_now_allowed")),
|
||||
"mainline_gate": dict(focus.get("mainline_gate") or {}),
|
||||
"mainline_gate_blockers": list(focus.get("mainline_gate_blockers") or [])[:5],
|
||||
"doctrine_research_status": dict(focus.get("doctrine_research_status") or {}),
|
||||
"research_only": bool(focus.get("research_only")),
|
||||
"research_only_reason": focus.get("research_only_reason") or "",
|
||||
}
|
||||
|
||||
|
||||
@@ -3636,6 +3654,8 @@ def _build_trader_readout_now(
|
||||
if status_tone == "do_not_use":
|
||||
return "Do Not Use: restore data/runtime before reading."
|
||||
if focus:
|
||||
if focus.get("research_only"):
|
||||
return "Observe Only: research material only; no trade."
|
||||
if status_tone == "review":
|
||||
return f"{status_label}: one focus; manual check only."
|
||||
wait_gap = _trader_wait_gap_sentence(list(focus.get("missing_confirmations") or []) or visible_reasons)
|
||||
@@ -3655,6 +3675,9 @@ def _build_trader_readout_reason(
|
||||
if status_tone == "do_not_use":
|
||||
return "Data/readiness is not safe enough for live reading."
|
||||
if focus:
|
||||
if focus.get("research_only"):
|
||||
reason = _short_trader_wait_label([focus.get("research_only_reason") or "missing Doctrine playbook"])
|
||||
return f"{current_session}: only research material; {reason}."
|
||||
structure = _short_structure_label(str(focus.get("model_code") or ""))
|
||||
session = str(focus.get("session_code") or current_session or "session")
|
||||
if status_tone == "review":
|
||||
@@ -3670,6 +3693,8 @@ def _build_trader_readout_wait(*, status_tone: str, focus: dict, visible_reasons
|
||||
if status_tone == "do_not_use":
|
||||
return "Restore data/runtime, then refresh."
|
||||
if focus:
|
||||
if focus.get("research_only"):
|
||||
return "Wait for full Doctrine playbook mapping before any Review Now."
|
||||
if status_tone == "review":
|
||||
return "Review the focus manually; then record Agree/Disagree/No Trade."
|
||||
wait_for = _trader_wait_action_label(list(focus.get("missing_confirmations") or []) or visible_reasons)
|
||||
@@ -3690,6 +3715,8 @@ def _build_trader_readout_risk(
|
||||
if status_tone == "review":
|
||||
return "Risk: treating Review Now as a trade signal."
|
||||
if focus:
|
||||
if focus.get("research_only"):
|
||||
return "Risk: treating surface shapes as a complete ICT setup."
|
||||
caution = _trader_wait_action_label(visible_reasons[:1]) if visible_reasons else "confirmation"
|
||||
return f"Risk: treating this as a signal before {caution}."
|
||||
return "Risk: forcing a setup before the ICT chain completes."
|
||||
@@ -3718,6 +3745,9 @@ def _build_trader_readout_why_no_trade(
|
||||
freeze_reason = str(freshness_summary.get("freeze_reason") or data_confidence or "readiness_not_safe")
|
||||
return f"No trade: restore {_short_trader_wait_label([freeze_reason])} first."
|
||||
if focus:
|
||||
if focus.get("research_only"):
|
||||
reason = _short_trader_wait_label([focus.get("research_only_reason") or "missing Doctrine playbook"])
|
||||
return f"No trade: {reason}; study only."
|
||||
wait_for = _short_trader_wait_label(
|
||||
list(focus.get("missing_confirmations") or [])
|
||||
or visible_reasons
|
||||
@@ -3742,6 +3772,8 @@ def _build_trader_readout_watch_next(
|
||||
if status_tone == "do_not_use":
|
||||
return "Refresh runtime/feed first; ignore setup output until status leaves Do Not Use."
|
||||
if focus:
|
||||
if focus.get("research_only"):
|
||||
return "Watch for Doctrine-complete setup: Range, Draw, Location, Time, Invalidation."
|
||||
wait_for = _short_trader_wait_label(list(focus.get("missing_confirmations") or []) or visible_reasons)
|
||||
return f"Watch {focus.get('title') or 'current focus'}: {wait_for}."
|
||||
if next_action:
|
||||
@@ -3752,6 +3784,8 @@ def _build_trader_readout_watch_next(
|
||||
def _build_trader_readout_focus_reason(*, focus: dict, visible_reasons: list[str]) -> str:
|
||||
if not focus:
|
||||
return "No unique focus: nothing has enough alignment to deserve the trader's first glance."
|
||||
if focus.get("research_only"):
|
||||
return f"Research only: {_short_trader_wait_label([focus.get('research_only_reason') or 'missing Doctrine playbook'])}."
|
||||
quality = f", q{_format_trader_quality(focus.get('quality_score'))}" if focus.get("quality_score") is not None else ""
|
||||
tier = str(focus.get("tier") or "watch").replace("_", " ")
|
||||
session = str(focus.get("session_code") or "OFF_HOURS")
|
||||
@@ -3780,6 +3814,8 @@ def _build_trader_readout_ict_chain_sentence(
|
||||
)
|
||||
if status_tone == "do_not_use":
|
||||
conclusion = "Do Not Use"
|
||||
elif focus and focus.get("research_only"):
|
||||
conclusion = "Observe Only"
|
||||
else:
|
||||
conclusion = status_label if status_label in {"Review Now", "Observe Only", "Do Not Use"} else "Observe Only"
|
||||
return (
|
||||
@@ -3788,6 +3824,7 @@ def _build_trader_readout_ict_chain_sentence(
|
||||
f"Liquidity event {steps['liquidity_event']}; "
|
||||
f"wait for {steps['confirmation']}; "
|
||||
f"Backtest {steps['backtest']}; "
|
||||
f"Doctrine {steps['doctrine']}; "
|
||||
f"therefore {conclusion}."
|
||||
)
|
||||
|
||||
@@ -3823,10 +3860,21 @@ def _build_trader_readout_ict_chain_steps(
|
||||
freshness_summary=freshness_summary,
|
||||
),
|
||||
"backtest": _format_chain_backtest(backtest_gate),
|
||||
"status": "complete" if mainline_focus_gate.get("review_now_allowed") and focus else "incomplete",
|
||||
"doctrine": _format_chain_doctrine(focus),
|
||||
"status": "complete" if mainline_focus_gate.get("review_now_allowed") and focus and not focus.get("research_only") else "incomplete",
|
||||
}
|
||||
|
||||
|
||||
def _format_chain_doctrine(focus: dict) -> str:
|
||||
if not focus:
|
||||
return "No playbook"
|
||||
doctrine_status = dict(focus.get("doctrine_research_status") or {})
|
||||
if doctrine_status.get("research_only"):
|
||||
reason = str(doctrine_status.get("research_only_reason") or "missing_doctrine_setup_playbook")
|
||||
return f"Research only ({reason.replace('_', ' ')})"
|
||||
return "Mapped"
|
||||
|
||||
|
||||
def _format_chain_range_location(*, focus: dict, current_session: str, market_posture: str) -> str:
|
||||
session = _chain_clean(focus.get("session_code") if focus else current_session)
|
||||
posture = str(market_posture or "").lower().replace("_", " ")
|
||||
|
||||
@@ -91,6 +91,8 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertIn("quality_gate", result[0]["summary"])
|
||||
self.assertEqual(result[0]["ict_rulebook_mapping"]["surface"], "strategy_backtest")
|
||||
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")
|
||||
|
||||
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}}}}
|
||||
@@ -101,6 +103,7 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertEqual(result["summary"]["by_symbol"]["BTC-USDT"]["count"], 3)
|
||||
self.assertEqual(result["ict_rulebook_mapping"]["surface"], "strategy_backtest")
|
||||
self.assertIn("backtest_evidence", result["ict_rulebook_mapping"]["present_concepts"])
|
||||
self.assertEqual(result["doctrine_research_status"]["status"], "research_only")
|
||||
|
||||
def test_strategy_backtest_surface_exposes_cockpit_promotion_gate(self) -> None:
|
||||
rows = [
|
||||
@@ -117,6 +120,19 @@ class ApiTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result["cockpit_promotion_gate"]["status"], "blocked")
|
||||
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)
|
||||
|
||||
def test_route_request_trading_doctrine_rules_returns_d1_matrix(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/doctrine-rules",
|
||||
{},
|
||||
dependencies={"trading_doctrine_rules": lambda: {"phase": "D1_MARKET_LANGUAGE_KERNEL", "rules": [{"rule_id": "range_defined"}]}},
|
||||
)
|
||||
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload["phase"], "D1_MARKET_LANGUAGE_KERNEL")
|
||||
self.assertEqual(payload["rules"][0]["rule_id"], "range_defined")
|
||||
|
||||
def test_route_request_trading_candidates_returns_items_payload(self) -> None:
|
||||
status, payload = route_request(
|
||||
|
||||
@@ -18,6 +18,7 @@ OLD_TRAINING_DOC = ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md
|
||||
NONEXISTENT_VISUAL_PLAN_NAME = "AI_ICT_" + "视觉盯盘与主动告警增强方案_v1.md"
|
||||
|
||||
ACTIVE_TOP_LEVEL_DOCS = {
|
||||
"AI_ICT_DoctrineKernel市场语言规则内核_D1_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",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import unittest
|
||||
|
||||
from src.services.ict_mainline import (
|
||||
DOCTRINE_KERNEL_PHASE,
|
||||
build_doctrine_rule_matrix,
|
||||
evaluate_doctrine_research_status,
|
||||
summarize_doctrine_rule_gaps,
|
||||
)
|
||||
|
||||
|
||||
class IctDoctrineKernelTests(unittest.TestCase):
|
||||
def test_d1_rule_matrix_exposes_required_market_language_chain(self) -> None:
|
||||
matrix = build_doctrine_rule_matrix()
|
||||
|
||||
self.assertEqual(matrix["phase"], DOCTRINE_KERNEL_PHASE)
|
||||
self.assertEqual(matrix["status"], "active")
|
||||
self.assertIn("Range", matrix["required_chain"])
|
||||
self.assertIn("Draw", matrix["required_chain"])
|
||||
self.assertIn("Invalidation", matrix["required_chain"])
|
||||
self.assertIn("read_only_analysis_no_order_submission", matrix["execution_boundary"])
|
||||
self.assertGreaterEqual(len(matrix["rules"]), 7)
|
||||
self.assertEqual(matrix["rules"][0]["rule_id"], "range_defined")
|
||||
|
||||
def test_surface_shape_candidate_without_doctrine_playbook_is_research_only(self) -> None:
|
||||
status = evaluate_doctrine_research_status(
|
||||
{
|
||||
"model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
|
||||
"missing_confirmations": ["fvg_retrace_confirmed"],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(status["research_only"])
|
||||
self.assertFalse(status["can_promote_to_cockpit"])
|
||||
self.assertEqual(status["research_only_reason"], "missing_doctrine_setup_playbook")
|
||||
self.assertIn("doctrine_missing_draw", status["research_only_reasons"])
|
||||
self.assertIn("doctrine_missing_invalidation", status["research_only_reasons"])
|
||||
|
||||
def test_complete_doctrine_payload_can_be_mapped_but_not_executed(self) -> None:
|
||||
status = evaluate_doctrine_research_status(
|
||||
{
|
||||
"setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL",
|
||||
"range_id": "range-1",
|
||||
"range_high": 105.0,
|
||||
"range_low": 95.0,
|
||||
"equilibrium": 100.0,
|
||||
"range_timeframe": "1h",
|
||||
"primary_draw": "sell_side_liquidity",
|
||||
"draw_side": "sell_side",
|
||||
"objective_price": 96.0,
|
||||
"premium_discount_zone": "discount",
|
||||
"location_advantage": "discount_reclaim",
|
||||
"context": "daily range expansion",
|
||||
"framework": "London killzone reversal",
|
||||
"reference_point": "range_low_sweep",
|
||||
"setup_type": "range_sweep_mss_fvg_retrace",
|
||||
"session_code": "LONDON",
|
||||
"allowed_sessions": ["LONDON"],
|
||||
"path_context": "sweep_then_displacement_then_retrace",
|
||||
"sequence_validity": "sweep_mss_fvg_retrace",
|
||||
"invalidations": ["range_low_reclaim_failed"],
|
||||
"stop_loss": 94.5,
|
||||
"invalid_level": 94.5,
|
||||
"target_plan": {"tp1": 100.0, "tp2": 104.0, "irl": "near_fvg", "erl": "range_high"},
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(status["research_only"])
|
||||
self.assertEqual(status["status"], "doctrine_mapped")
|
||||
self.assertTrue(status["can_promote_to_cockpit"])
|
||||
self.assertEqual(status["execution_boundary"], "read_only_analysis_no_order_submission")
|
||||
|
||||
def test_doctrine_gap_summary_counts_missing_book_fields(self) -> None:
|
||||
summary = summarize_doctrine_rule_gaps(
|
||||
[
|
||||
{"model_code": "SWEEP_MSS_FVG"},
|
||||
{"model_code": "SWEEP_MSS_FVG", "range_id": "range-1", "equilibrium": 100.0},
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(summary["item_count"], 2)
|
||||
self.assertEqual(summary["research_only_count"], 2)
|
||||
self.assertEqual(summary["promotable_count"], 0)
|
||||
self.assertEqual(summary["research_only_reason_counts"]["missing_doctrine_setup_playbook"], 2)
|
||||
self.assertGreater(summary["missing_rule_counts"]["draw_defined"], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -6,7 +6,11 @@ from unittest.mock import patch
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
||||
|
||||
from src.services.operator import DecisionAssistantSummaryService, OperatorNoteService, ReviewPacketService, SessionFocusService
|
||||
from src.services.operator.operator_workspace_service import _build_trader_mode_r2_evidence_window
|
||||
from src.services.operator.operator_workspace_service import (
|
||||
_build_trader_mode_live_readout,
|
||||
_build_trader_mode_r2_evidence_window,
|
||||
_resolve_trader_mode_focus_candidate,
|
||||
)
|
||||
|
||||
|
||||
class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
@@ -37,6 +41,55 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
|
||||
self.assertNotIn("operator_feedback_count", aligned_window["missing_evidence"])
|
||||
|
||||
def test_trader_mode_focus_preserves_doctrine_research_only_state(self) -> None:
|
||||
focus = _resolve_trader_mode_focus_candidate(
|
||||
candidate_watchlist_tiers={
|
||||
"review_now": [],
|
||||
"strong_watch": [
|
||||
{
|
||||
"candidate_id": 4693,
|
||||
"title": "Candidate #4693",
|
||||
"tier": "strong_watch",
|
||||
"session_code": "LONDON",
|
||||
"model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
|
||||
"missing_confirmations": ["fvg_retrace_confirmed"],
|
||||
"mainline_gate_blockers": ["missing_doctrine_setup_playbook"],
|
||||
"doctrine_research_status": {
|
||||
"status": "research_only",
|
||||
"research_only": True,
|
||||
"research_only_reason": "missing_doctrine_setup_playbook",
|
||||
},
|
||||
"research_only": True,
|
||||
"research_only_reason": "missing_doctrine_setup_playbook",
|
||||
}
|
||||
],
|
||||
"soft_watch": [],
|
||||
},
|
||||
candidate_watchlist_summary={},
|
||||
top_opportunities=[],
|
||||
decision_packet={},
|
||||
)
|
||||
readout = _build_trader_mode_live_readout(
|
||||
status_label="Observe Only",
|
||||
status_tone="observe",
|
||||
data_confidence="fresh",
|
||||
focus=focus,
|
||||
mainline_focus_gate={"review_now_allowed": False, "blockers": []},
|
||||
visible_reasons=["missing_doctrine_setup_playbook"],
|
||||
plain_language_reasons=["missing_doctrine_setup_playbook"],
|
||||
next_action="wait_for_doctrine_playbook",
|
||||
freshness_summary={"is_stale": False},
|
||||
current_session="LONDON",
|
||||
market_posture="monitor",
|
||||
readonly_live_health_summary={},
|
||||
)
|
||||
|
||||
self.assertTrue(focus["research_only"])
|
||||
self.assertEqual(focus["research_only_reason"], "missing_doctrine_setup_playbook")
|
||||
self.assertIn("Research only", readout["focus_reason"])
|
||||
self.assertIn("Doctrine Research only", readout["ict_chain_sentence"])
|
||||
self.assertIn("full Doctrine playbook", readout["wait"])
|
||||
|
||||
def test_create_note_returns_payload(self) -> None:
|
||||
service = OperatorNoteService(repository=_NoteRepo())
|
||||
|
||||
@@ -197,9 +250,15 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
self.assertTrue(any(item["kind"] == "decision_packet" for item in summary["top_priority_items"]))
|
||||
self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"][0]["candidate_id"], 18)
|
||||
self.assertTrue(summary["candidate_watchlist_tiers"]["review_now"][0]["review_now_allowed"])
|
||||
self.assertFalse(summary["candidate_watchlist_tiers"]["review_now"][0]["research_only"])
|
||||
self.assertEqual(summary["mainline_focus_gate"]["status"], "passed")
|
||||
self.assertTrue(summary["mainline_focus_gate"]["review_now_allowed"])
|
||||
self.assertEqual(summary["candidate_watchlist_tiers"]["strong_watch"][0]["candidate_id"], 21)
|
||||
self.assertTrue(summary["candidate_watchlist_tiers"]["strong_watch"][0]["research_only"])
|
||||
self.assertEqual(
|
||||
summary["candidate_watchlist_tiers"]["strong_watch"][0]["research_only_reason"],
|
||||
"missing_doctrine_setup_playbook",
|
||||
)
|
||||
self.assertEqual(summary["candidate_watchlist_tiers"]["soft_watch"][0]["candidate_id"], 22)
|
||||
self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"][0]["action_method"], "POST")
|
||||
self.assertIn("/trading/candidates/18/confirm-check", summary["candidate_watchlist_tiers"]["review_now"][0]["action_path"])
|
||||
@@ -1330,6 +1389,28 @@ class _CandidateWatchlistRepo:
|
||||
"status": "candidate",
|
||||
"quality_score": 81.0,
|
||||
"model_code": "SWEEP_MSS_FVG",
|
||||
"setup_playbook_id": "ICT-CH1-6-LONDON-DISCOUNT-REVERSAL",
|
||||
"range_id": "london-range-1",
|
||||
"range_high": 105.0,
|
||||
"range_low": 95.0,
|
||||
"range_timeframe": "1h",
|
||||
"equilibrium": 100.0,
|
||||
"primary_draw": "sell_side_liquidity",
|
||||
"draw_side": "sell_side",
|
||||
"objective_price": 96.0,
|
||||
"premium_discount_zone": "discount",
|
||||
"location_advantage": "discount_reclaim",
|
||||
"context": "daily range expansion",
|
||||
"framework": "London killzone reversal",
|
||||
"reference_point": "range_low_sweep",
|
||||
"setup_type": "range_sweep_mss_fvg_retrace",
|
||||
"allowed_sessions": ["LONDON"],
|
||||
"path_context": "sweep_then_displacement_then_retrace",
|
||||
"sequence_validity": "sweep_mss_fvg_retrace",
|
||||
"invalidations": ["range_low_reclaim_failed"],
|
||||
"stop_loss": 94.5,
|
||||
"invalid_level": 94.5,
|
||||
"target_plan": {"tp1": 100.0, "tp2": 104.0, "irl": "near_fvg", "erl": "range_high"},
|
||||
"ai_explanation": "Current focus candidate.",
|
||||
"missing_confirmations": ["trust_soft_block"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user