Add ICT cockpit feedback loop

This commit is contained in:
Codex
2026-05-21 12:24:52 +08:00
parent 3c8613632f
commit 03f0839ceb
14 changed files with 619 additions and 7 deletions
+3 -2
View File
@@ -833,11 +833,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- Session Focus Chinese dynamic-label follow-up passed on 2026-05-20 without adding product features: fixed the existing i18n layer for dynamic session-focus cards so session names and lower-case backend labels now render in Chinese (`ASIA`, `LONDON`, `NY_AM`, `NY_LUNCH`, `NY_PM`, `status`, `window`, `candidates`, `executable`, `open executions`, `alerts`, `recommendation`, `action hint`, `incomplete`, `closed`, `observe`, `avoid`, `review_alerts`, `avoid_new_risk`). Remote browser audit on `/trader-mode?i18n=20260520-session#session-focus` confirmed `lang=zh-CN`, `active=session-focus`, no English leftovers from the reported list, and visible Chinese replacements such as `亚洲盘`, `纽约下午盘`, `状态`, `时间窗口`, `候选数`, `可执行数`, `未平仓执行`, `提醒数`, `建议`, `动作提示`, `观察 @ 无`, `回避 @ 30.0`, `回避新增风险`, and `复核提醒`; console errors were empty. Validation passed `node --check src/web/i18n.js`, focused static/operator/read-only tests, secret static checks, script static checks, full unit discovery (`790` tests), deploy smoke over 11 endpoints, `run_remote_ops_smoke.py`, and `run_readonly_live_analysis_smoke.py`. Evidence screenshot saved at `output/playwright/i18n-session-focus-zh-1366x768.png`. No change was made to `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, or operator confirmation protocol.
- S2 ICT canonical taxonomy/rulebook registry is implemented locally: added `src/services/ict_mainline/taxonomy.py`, `docs/AI_ICT_ICT主线概念字典_S2_2026-05-21_v1.md`, and `ict_rulebook_mapping` on Trader Cockpit, Strategy Backtest, Trading Plan, and Journal outputs. The registry binds trader-facing analysis to the book chain `Range / Draw / Location -> Liquidity / Sweep / Inducement / Path -> Displacement / MSS / FVG / Retest / Session -> backtest evidence -> pre-market plan -> intraday unique focus or no-trade reason -> journal / study-notes feedback`, while preserving read-only/no-order boundaries.
- S3/S4 Review Now mainline gate is implemented locally: added `src/services/ict_mainline/gates.py`, `docs/AI_ICT_ReviewNow主线Gate_S3S4_2026-05-21_v1.md`, `mainline_focus_gate` on Decision Assistant / Trader Cockpit, and `cockpit_promotion_gate` on Strategy Backtest. `Review Now` now requires active Trading Plan + primary draw + invalidation + no blocking plan condition + passed Strategy Backtest quality gate + candidate link to the active plan; otherwise candidates are downgraded to watch / observe and Trader Cockpit remains `Observe Only` or lower.
- Current assisted-analysis next gate: proceed to S5/S6 only: compress Trader Cockpit language into the ICT chain sentence and map operator feedback / Journal / Study Notes to book-rule error classes. Do not restart external-context/archive/digest long-tail work, do not physically delete S1 deprecated surfaces yet, and keep R2 sample collection honest.
- S5/S6 Cockpit chain sentence and feedback loop is implemented locally: added `src/services/ict_mainline/feedback.py`, `docs/AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md`, `trader_mode_summary.trader_readout.ict_chain_sentence`, `ict_chain_steps`, and book-rule feedback classifications for operator feedback, Journal, weekly review, and study-note drafts. Observation review, paper review, and execution-derived review are now counted separately so observation samples cannot be mistaken for execution evidence.
- Current assisted-analysis next gate: proceed to S7/S8 only: solidify read-only live reliability / System Ops trader-safe health wording, then write one-page intraday SOP, backtest SOP, and review SOP. Do not restart external-context/archive/digest long-tail work, do not physically delete S1 deprecated surfaces yet, and keep R2 sample collection honest.
## Exact Next Step
Use `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_ICT主线概念字典_S2_2026-05-21_v1.md`, and `docs/AI_ICT_ReviewNow主线Gate_S3S4_2026-05-21_v1.md` as the active anchors. Continue with S5/S6: compress Trader Cockpit language into the ICT chain sentence, then map operator feedback / Journal / Study Notes to book-rule error classes. 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. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
Use `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_ICT主线概念字典_S2_2026-05-21_v1.md`, `docs/AI_ICT_ReviewNow主线Gate_S3S4_2026-05-21_v1.md`, and `docs/AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md` as the active anchors. Continue with S7/S8: solidify read-only live reliability / System Ops trader-safe health wording, then write one-page intraday SOP, backtest SOP, and review SOP. 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. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
## Validation Commands Used Recently
+2 -1
View File
@@ -937,7 +937,8 @@
- [x] S1 mainline deletion readiness audit under the new plan: created `docs/AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md`; current top navigation is compliant, Research Only and Deprecated/Deferred Execution surfaces are documented, and physical deletion remains blocked until dependency/API/test/remote-smoke impact is proven safe
- [x] S2 ICT canonical taxonomy/rulebook registry: require every Trader Cockpit focus, Strategy Backtest result, Trading Plan item, and Journal note to map to the book chain
- [x] S3/S4 Review Now mainline gate: Trading Plan is now the hard upstream gate, Strategy Backtest quality-gate failure blocks weak setups from live focus, and Strategy Backtest exposes `cockpit_promotion_gate`
- [ ] S5/S6 next gate: compress Trader Cockpit language into the ICT chain sentence, then map operator feedback / Journal / Study Notes to book-rule error classes
- [x] S5/S6 Cockpit chain sentence and feedback loop: Trader Cockpit now exposes `ict_chain_sentence` / `ict_chain_steps`, operator feedback / Journal / weekly review map to ICT book-rule error classes, and observation review remains separated from paper / execution-derived review
- [ ] S7/S8 next gate: solidify read-only live reliability / System Ops trader-safe health wording, then write one-page intraday SOP, backtest SOP, and review SOP
- [ ] R2 paper-ticket sample gate is waiting on a real executable candidate; do not fabricate paper tickets from blocked candidates
- [ ] R2 operator feedback sample gate still needs 2 more real feedback samples (`operator_feedback_count=3/5`)
- [ ] R2 weekly review visible gate still needs a real execution-derived journal/weekly review entry (`weekly_review_entry_count=0`)
@@ -0,0 +1,73 @@
# AI_ICT Cockpit 链路句与反馈错误分类 S5/S6 2026-05-21 v1
## 1. 目标
本阶段只压缩既有交易员主线,不扩展新功能。
- S5:Trader Cockpit 首屏 focus 必须变成一条固定 ICT 链路句。
- S6:Agree / Disagree / No Trade / Need More Evidence / Quick Note、Journal、Study Notes 必须映射到《ICT交易全书》第 46 章的研究闭环。
## 2. Trader Cockpit 链路句
`trader_mode_summary.trader_readout` 现在输出:
- `ict_chain_sentence`
- `ict_chain_steps`
- `focus_reason` 指向同一条链路句
句式固定为:
```text
Current chain: Range/Location ...; main Draw ...; Liquidity event ...; wait for ...; Backtest ...; therefore Review Now / Observe Only / Do Not Use.
```
中文界面通过现有 i18n phrase map 翻译固定短语。链路句只解释“为什么复核 / 为什么观察 / 为什么不能用”,不生成买卖方向,也不生成下单动作。
## 3. 反馈错误分类
新增 `src/services/ict_mainline/feedback.py`,统一分类以下来源:
- operator feedback
- Trader Mode intraday action
- Journal
- weekly review
- study note draft
书本错误分类为:
| class | 含义 |
| --- | --- |
| `model_definition_gap` | Study Notes 层:模型定义、边界、触发链路、失效标准不清 |
| `plan_alignment_gap` | Trading Plan 层:Draw、invalidation、session、候选计划链接不一致 |
| `confirmation_gap` | 确认链路层:Sweep 后缺 Displacement / MSS / FVG / Retest |
| `backtest_evidence_gap` | Backtesting 层:样本、expectancy、profit factor、drawdown 或质量门不足 |
| `execution_process_gap` | Journal 层:执行流程偏差,只能来自 paper/execution-derived review |
| `risk_boundary_gap` | 边界层:stale、Do Not Use、risk governor、news / data blocker |
| `no_trade_discipline` | 决策日志层:正确放弃、观察、等待,不得伪造成执行样本 |
## 4. Weekly Review 分层
Weekly review 现在明确输出:
- `review_type_counts.observation_review`
- `review_type_counts.paper_review`
- `review_type_counts.execution_derived_review`
- `book_rule_error_class_counts`
- `study_note_draft`
- `execution_evidence_policy`
这保证 observation review 与 paper / execution-derived review 分开统计。当前系统仍不做实盘交易,不允许把 observation review 伪造成 execution evidence。
## 5. 验收口径
- Trader Cockpit 首屏 focus 解释必须能读成 ICT 链路,而不是内部原因列表。
- 反馈与 Journal 必须能落到书本错误分类。
- R2 样本仍保持诚实:paper ticket / paper execution 只能来自真实 executable candidate 的纸面流程。
- `execution_allowed=false`、`real_live_ready=false`、operator confirmation protocol 不变。
## 6. 下一步
下一步只能继续 S7/S8:
- 固化只读实盘可靠性与 System Ops 极简健康结论。
- 写一页式盘中 SOP、回测 SOP、复盘 SOP。
+8
View File
@@ -12,6 +12,11 @@ from src.services.ict_mainline.gates import (
build_trading_plan_gate,
candidate_mainline_blockers,
)
from src.services.ict_mainline.feedback import (
build_study_note_draft,
classify_book_rule_error,
summarize_book_rule_errors,
)
__all__ = [
"ICT_MAINLINE_CHAIN",
@@ -19,9 +24,12 @@ __all__ = [
"build_ict_rulebook_summary",
"build_backtest_quality_gate",
"build_review_now_mainline_gate",
"build_study_note_draft",
"build_trading_plan_gate",
"candidate_mainline_blockers",
"classify_book_rule_error",
"classify_ict_concepts",
"get_ict_concept",
"list_ict_concepts",
"summarize_book_rule_errors",
]
+202
View File
@@ -0,0 +1,202 @@
from __future__ import annotations
from collections import Counter
import re
from typing import Iterable
BOOK_RULE_ERROR_CLASSES = {
"model_definition_gap": {
"label": "Model Definition Gap",
"book_anchor": "ICT full book chapter 46: Study Notes define model terms, boundaries, trigger chain, and failure standards.",
"study_note_prompt": "Clarify the model definition before changing live focus rules.",
"next_action": "write_study_note_model_definition",
},
"plan_alignment_gap": {
"label": "Plan Alignment Gap",
"book_anchor": "ICT full book: pre-market Trading Plan must define draw, invalidation, allowed session, and no-trade conditions.",
"study_note_prompt": "Review whether the candidate followed the active Trading Plan.",
"next_action": "tighten_plan_and_candidate_validation",
},
"confirmation_gap": {
"label": "Confirmation Gap",
"book_anchor": "ICT full book: after liquidity, wait for displacement, MSS, FVG, and retest confirmation instead of chasing.",
"study_note_prompt": "Record which confirmation link was missing.",
"next_action": "review_confirmation_chain",
},
"backtest_evidence_gap": {
"label": "Backtest Evidence Gap",
"book_anchor": "ICT full book chapter 46: Backtesting identifies model boundary, timing, drawdown, and sample sufficiency.",
"study_note_prompt": "Add or review no-lookahead samples before promoting this setup.",
"next_action": "review_strategy_backtest_quality_gate",
},
"execution_process_gap": {
"label": "Execution Process Gap",
"book_anchor": "ICT full book chapter 46: Trading Journal records what was done, why, rule adherence, and execution deviation.",
"study_note_prompt": "Record the execution deviation separately from model definition.",
"next_action": "review_execution_process_and_confirmation_path",
},
"risk_boundary_gap": {
"label": "Risk Boundary Gap",
"book_anchor": "ICT full book: no-trade conditions and risk boundaries must stop action when the chain is unsafe.",
"study_note_prompt": "Keep the decision as observe-only until the risk or data boundary clears.",
"next_action": "review_no_trade_boundary",
},
"no_trade_discipline": {
"label": "No-Trade Discipline",
"book_anchor": "ICT full book chapter 46: mature decision logs include why attractive trades were avoided.",
"study_note_prompt": "Keep this as an observation sample; do not convert it into execution evidence.",
"next_action": "retain_no_trade_decision_sample",
},
}
ACTION_DEFAULT_CLASSES = {
"agree": "no_trade_discipline",
"disagree": "plan_alignment_gap",
"no_trade": "no_trade_discipline",
"need_more_evidence": "backtest_evidence_gap",
"quick_note": "model_definition_gap",
"operator_feedback_recorded": "no_trade_discipline",
"candidate_observed": "no_trade_discipline",
"pending": "no_trade_discipline",
}
KEYWORD_CLASS_RULES = (
("risk_boundary_gap", ("risk", "stale", "do_not_use", "blocked", "governor", "news_window", "kill_switch")),
("backtest_evidence_gap", ("backtest", "sample", "expectancy", "profit_factor", "drawdown", "quality_gate")),
("execution_process_gap", ("execution", "ticket", "fill", "early_entry", "reentry", "confirmation_metadata")),
("plan_alignment_gap", ("plan", "draw", "invalidation", "session_mismatch", "candidate_plan", "bias")),
("confirmation_gap", ("mss", "fvg", "retest", "sweep", "liquidity", "displacement", "confirmation")),
("no_trade_discipline", ("no_trade", "stand_down", "observe_only", "wait", "avoid")),
)
def classify_book_rule_error(payload: dict | None = None, *, source: str = "operator_feedback") -> dict:
event = dict(payload or {})
action = _normalize(
event.get("action")
or event.get("feedback_type")
or event.get("operator_agreement")
or event.get("observation_status")
or event.get("status")
)
tokens = _payload_tokens({**event, "source": source, "action": action})
error_class = _resolve_error_class(tokens=tokens, action=action)
definition = BOOK_RULE_ERROR_CLASSES[error_class]
review_type = _resolve_review_type(event=event, source=source, tokens=tokens)
return {
"source": source,
"action": action or "unclassified",
"review_type": review_type,
"error_class": error_class,
"label": definition["label"],
"book_anchor": definition["book_anchor"],
"study_note_prompt": definition["study_note_prompt"],
"next_action": definition["next_action"],
"execution_evidence_allowed": review_type == "execution_derived_review",
"paper_evidence_allowed": review_type == "paper_review",
}
def summarize_book_rule_errors(classifications: Iterable[dict]) -> dict:
rows = [dict(item) for item in classifications if item]
class_counts = Counter(str(item.get("error_class") or "unclassified") for item in rows)
review_type_counts = Counter(str(item.get("review_type") or "observation_review") for item in rows)
next_actions = _dedupe([str(item.get("next_action") or "") for item in rows])
return {
"status": "mapped" if rows else "collecting_feedback",
"classification_count": len(rows),
"book_rule_error_class_counts": dict(class_counts),
"review_type_counts": dict(review_type_counts),
"next_actions": next_actions[:5],
"study_note_draft": build_study_note_draft(rows),
"execution_evidence_policy": "observation_and_paper_reviews_must_not_be_counted_as_execution_evidence",
}
def build_study_note_draft(classifications: Iterable[dict]) -> dict:
rows = [dict(item) for item in classifications if item]
if not rows:
return {
"status": "collecting_feedback",
"title": "Study note draft pending",
"bullets": ["Record Agree, Disagree, No Trade, Need More Evidence, or Quick Note first."],
}
class_counts = Counter(str(item.get("error_class") or "unclassified") for item in rows)
top_class, top_count = class_counts.most_common(1)[0]
definition = BOOK_RULE_ERROR_CLASSES.get(top_class, BOOK_RULE_ERROR_CLASSES["model_definition_gap"])
prompts = _dedupe([str(item.get("study_note_prompt") or "") for item in rows])
return {
"status": "draft_ready",
"title": f"Study note draft: {definition['label']}",
"dominant_error_class": top_class,
"dominant_error_count": top_count,
"bullets": prompts[:3],
"book_anchor": definition["book_anchor"],
}
def _resolve_error_class(*, tokens: set[str], action: str) -> str:
for error_class, keywords in KEYWORD_CLASS_RULES:
if any(
_normalize(keyword) in token
for keyword in keywords
for token in tokens
):
return error_class
return ACTION_DEFAULT_CLASSES.get(action, "model_definition_gap")
def _resolve_review_type(*, event: dict, source: str, tokens: set[str]) -> str:
source_key = _normalize(source)
if event.get("real_trade_detected") or event.get("execution_id") or "execution_derived_review" in tokens:
return "execution_derived_review"
execution_mode = _normalize(event.get("execution_mode"))
if execution_mode == "paper" or event.get("paper_ticket_id") or event.get("paper_execution_id") or "paper_review" in tokens:
return "paper_review"
if source_key in {"study_note", "study_notes"}:
return "study_note_review"
return "observation_review"
def _payload_tokens(payload: object) -> set[str]:
text = " ".join(_payload_text_fragments(payload)).lower().replace("-", "_")
return {_normalize(item) for item in re.split(r"[^a-z0-9_]+", text) if _normalize(item)}
def _payload_text_fragments(payload: object) -> list[str]:
if payload is None or payload is False:
return []
if isinstance(payload, dict):
fragments: list[str] = []
for key, value in payload.items():
if value in (None, False, "", [], {}):
continue
fragments.append(str(key))
if value is not True:
fragments.extend(_payload_text_fragments(value))
return fragments
if isinstance(payload, (list, tuple, set)):
fragments: list[str] = []
for item in payload:
fragments.extend(_payload_text_fragments(item))
return fragments
return [str(payload)]
def _normalize(value) -> str:
return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
def _dedupe(values: Iterable[str]) -> list[str]:
seen = set()
result = []
for value in values:
text = str(value or "").strip()
if not text or text in seen:
continue
seen.add(text)
result.append(text)
return result
@@ -4,7 +4,7 @@ from typing import Optional
from src.repositories.trading_journal_repository import TradingJournalRepository
from src.services.backtest import classify_backtest_lifecycle_state_from_result
from src.services.ict_mainline import build_ict_rulebook_mapping
from src.services.ict_mainline import build_ict_rulebook_mapping, classify_book_rule_error
from src.services.reports import PostTradeReviewService
@@ -135,6 +135,22 @@ class TradingJournalService:
"ticket": _build_ticket_payload(bundle),
"real_trade_mapping": execution_reconciliation,
}
summary["ict_feedback_classification"] = classify_book_rule_error(
{
"source": "journal",
"execution_id": bundle.get("execution_id"),
"execution_mode": bundle.get("execution_mode"),
"real_trade_detected": real_trade_detected,
"review_tags": review_tags,
"next_actions": next_actions,
"error_types": error_types,
"lifecycle_state": lifecycle_state,
"reconciliation_status": execution_reconciliation.get("status"),
"trading_plan_id": bundle.get("trading_plan_id"),
"candidate_id": bundle.get("candidate_id"),
},
source="journal",
)
summary["ict_rulebook_mapping"] = build_ict_rulebook_mapping(
"journal",
{
@@ -3,6 +3,7 @@ from datetime import date
from typing import Optional
from src.repositories.trading_journal_repository import TradingJournalRepository, default_week_range
from src.services.ict_mainline import classify_book_rule_error, summarize_book_rule_errors
class WeeklyReviewService:
@@ -35,6 +36,25 @@ class WeeklyReviewService:
session_stats = build_session_stats(entries)
high_quality_session, low_quality_session = rank_sessions(session_stats)
next_actions = recommend_next_actions(entries, low_quality_session=low_quality_session)
feedback_classifications = [
classify_book_rule_error(
{
"source": "weekly_review",
"execution_id": entry.get("execution_id"),
"execution_mode": entry.get("execution_mode"),
"real_trade_detected": entry.get("real_trade_detected"),
"review_tags": entry.get("review_tags") or [],
"next_actions": entry.get("next_actions") or [],
"summary": entry.get("summary") or {},
"risk_violation": entry.get("risk_violation"),
"model_compliance": entry.get("model_compliance"),
"reconciliation_status": entry.get("reconciliation_status"),
},
source="journal",
)
for entry in entries
]
feedback_loop_summary = summarize_book_rule_errors(feedback_classifications)
return {
"week_start": week_start,
"week_end": week_end,
@@ -49,6 +69,11 @@ class WeeklyReviewService:
"low_quality_session": low_quality_session,
"session_performance": session_stats,
"next_week_actions": next_actions,
"feedback_rule_classifications": feedback_classifications[:20],
"book_rule_error_class_counts": feedback_loop_summary["book_rule_error_class_counts"],
"review_type_counts": feedback_loop_summary["review_type_counts"],
"study_note_draft": feedback_loop_summary["study_note_draft"],
"execution_evidence_policy": feedback_loop_summary["execution_evidence_policy"],
}
@@ -18,7 +18,9 @@ from src.services.execution import ExecutionService
from src.services.ict_mainline import (
build_ict_rulebook_mapping,
build_review_now_mainline_gate,
classify_book_rule_error,
candidate_mainline_blockers,
summarize_book_rule_errors,
)
from src.services.journal import WeeklyReviewService
from src.services.alerts import AlertQualitySummaryService, OperatorDigestService
@@ -2938,6 +2940,7 @@ def _build_trader_mode_summary(
status_tone=status_tone,
data_confidence=data_confidence,
focus=focus,
mainline_focus_gate=mainline_focus_gate,
visible_reasons=visible_reasons,
plain_language_reasons=plain_language_reasons,
next_action=next_action,
@@ -3117,6 +3120,22 @@ def _build_trader_mode_post_market_review_draft(
operator_feedback_count = int(shadow_paper_validation_summary.get("operator_feedback_count") or 0)
weekly_entry_count = int(weekly_review.get("entry_count") or 0)
next_actions = list(weekly_review.get("next_week_actions") or [])
feedback_classifications = [
classify_book_rule_error(
{
"operator_agreement": item.get("operator_agreement"),
"observation_status": item.get("observation_status"),
"system_recommendation": item.get("system_recommendation"),
"reason_codes": item.get("reason_codes") or [],
"paper_ticket_id": item.get("paper_ticket_id"),
"paper_execution_id": item.get("paper_execution_id"),
},
source="operator_feedback",
)
for item in observations
]
feedback_loop_summary = summarize_book_rule_errors(feedback_classifications)
weekly_review_type_counts = dict(weekly_review.get("review_type_counts") or {})
draft_sections = [
{
"title": "Intraday Actions",
@@ -3130,6 +3149,10 @@ def _build_trader_mode_post_market_review_draft(
"title": "Weekly Carry Forward",
"body": "; ".join(str(item) for item in next_actions[:3]) or "Collect more Trader Mode actions before the weekly draft is strong.",
},
{
"title": "Study Notes",
"body": "; ".join((feedback_loop_summary.get("study_note_draft") or {}).get("bullets") or ["Keep observation samples separate from execution evidence."]),
},
]
return {
"status": "draft_ready" if observation_count or weekly_entry_count else "collecting_intraday_actions",
@@ -3140,6 +3163,16 @@ def _build_trader_mode_post_market_review_draft(
"operator_feedback_count": operator_feedback_count,
"weekly_review_entry_count": weekly_entry_count,
"agreement_counts": agreement_counts,
"feedback_rule_classifications": feedback_classifications[:10],
"feedback_loop_summary": feedback_loop_summary,
"study_note_draft": feedback_loop_summary.get("study_note_draft"),
"review_type_counts": {
"observation_review": feedback_loop_summary.get("review_type_counts", {}).get("observation_review", 0),
"paper_review": feedback_loop_summary.get("review_type_counts", {}).get("paper_review", 0)
+ weekly_review_type_counts.get("paper_review", 0),
"execution_derived_review": weekly_review_type_counts.get("execution_derived_review", 0),
},
"execution_evidence_policy": "do_not_count_observation_reviews_as_execution_evidence",
"draft_sections": draft_sections,
"save_note_tags": ["trader-mode", "post-market-draft", "weekly-review-draft"],
"operator_summary": (
@@ -3430,6 +3463,7 @@ def _build_trader_mode_live_readout(
status_tone: str,
data_confidence: str,
focus: dict,
mainline_focus_gate: dict,
visible_reasons: list[str],
plain_language_reasons: list[str],
next_action: str,
@@ -3455,6 +3489,24 @@ def _build_trader_mode_live_readout(
market_posture=market_posture,
)
focus_reason = _build_trader_readout_focus_reason(focus=focus, visible_reasons=visible_reasons)
ict_chain_sentence = _build_trader_readout_ict_chain_sentence(
status_label=status_label,
status_tone=status_tone,
focus=focus,
mainline_focus_gate=mainline_focus_gate,
visible_reasons=visible_reasons,
current_session=current_session,
market_posture=market_posture,
freshness_summary=freshness_summary,
)
ict_chain_steps = _build_trader_readout_ict_chain_steps(
focus=focus,
mainline_focus_gate=mainline_focus_gate,
visible_reasons=visible_reasons,
current_session=current_session,
market_posture=market_posture,
freshness_summary=freshness_summary,
)
data_state = _build_trader_readout_data_state(
data_confidence=data_confidence,
freshness_summary=freshness_summary,
@@ -3475,7 +3527,7 @@ def _build_trader_mode_live_readout(
cards = [
{"label": "Why No Trade", "value": why_no_trade, "tone": "caution"},
{"label": "Watch Next", "value": watch_next, "tone": "focus"},
{"label": "Focus Reason", "value": focus_reason, "tone": "focus" if focus else "muted"},
{"label": "ICT Chain", "value": ict_chain_sentence, "tone": "focus" if focus else "muted"},
{"label": "Data State", "value": data_state, "tone": "blocked" if status_tone == "do_not_use" else "neutral"},
{"label": "No Candidate", "value": no_candidate_state, "tone": "muted" if focus else "caution"},
]
@@ -3484,7 +3536,10 @@ def _build_trader_mode_live_readout(
"headline": headline,
"why_no_trade": why_no_trade,
"watch_next": watch_next,
"focus_reason": focus_reason,
"focus_reason": ict_chain_sentence,
"legacy_focus_reason": focus_reason,
"ict_chain_sentence": ict_chain_sentence,
"ict_chain_steps": ict_chain_steps,
"data_state": data_state,
"no_candidate_state": no_candidate_state,
"one_screen_cards": cards,
@@ -3556,6 +3611,140 @@ def _build_trader_readout_focus_reason(*, focus: dict, visible_reasons: list[str
return f"Focus: {tier} {session}{quality}; wait {reason}."
def _build_trader_readout_ict_chain_sentence(
*,
status_label: str,
status_tone: str,
focus: dict,
mainline_focus_gate: dict,
visible_reasons: list[str],
current_session: str,
market_posture: str,
freshness_summary: dict,
) -> str:
steps = _build_trader_readout_ict_chain_steps(
focus=focus,
mainline_focus_gate=mainline_focus_gate,
visible_reasons=visible_reasons,
current_session=current_session,
market_posture=market_posture,
freshness_summary=freshness_summary,
)
if status_tone == "do_not_use":
conclusion = "Do Not Use"
else:
conclusion = status_label if status_label in {"Review Now", "Observe Only", "Do Not Use"} else "Observe Only"
return (
f"Current chain: Range/Location {steps['range_location']}; "
f"main Draw {steps['draw']}; "
f"Liquidity event {steps['liquidity_event']}; "
f"wait for {steps['confirmation']}; "
f"Backtest {steps['backtest']}; "
f"therefore {conclusion}."
)
def _build_trader_readout_ict_chain_steps(
*,
focus: dict,
mainline_focus_gate: dict,
visible_reasons: list[str],
current_session: str,
market_posture: str,
freshness_summary: dict,
) -> dict:
plan_gate = dict(mainline_focus_gate.get("trading_plan_gate") or {})
backtest_gate = dict(mainline_focus_gate.get("backtest_quality_gate") or {})
primary_draw = dict(plan_gate.get("primary_draw") or {})
missing = list(focus.get("missing_confirmations") or []) if focus else []
blockers = [
*[str(item) for item in list(mainline_focus_gate.get("blockers") or []) if item],
*[str(item) for item in visible_reasons if item],
]
return {
"range_location": _format_chain_range_location(
focus=focus,
current_session=current_session,
market_posture=market_posture,
),
"draw": _format_chain_draw(primary_draw, blockers=blockers),
"liquidity_event": _format_chain_liquidity_event(focus=focus, missing=missing, blockers=blockers),
"confirmation": _format_chain_confirmation(
missing=missing,
blockers=blockers,
freshness_summary=freshness_summary,
),
"backtest": _format_chain_backtest(backtest_gate),
"status": "complete" if mainline_focus_gate.get("review_now_allowed") and focus else "incomplete",
}
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("_", " ")
if "premium" in posture:
location = "premium area"
elif "discount" in posture:
location = "discount area"
elif "range" in posture:
location = "dealing range under review"
else:
location = "location unresolved"
return f"{session} / {location}"
def _format_chain_draw(primary_draw: dict, *, blockers: list[str]) -> str:
if primary_draw:
side = _chain_clean(primary_draw.get("side") or "draw")
source = _chain_clean(primary_draw.get("source") or "plan")
return f"{side} from {source}"
if any("missing_active_trading_plan" in str(item) for item in blockers):
return "missing active plan"
if any("missing_primary_draw" in str(item) for item in blockers):
return "missing primary draw"
return "not resolved"
def _format_chain_liquidity_event(*, focus: dict, missing: list, blockers: list[str]) -> str:
text = " ".join(
[
str(focus.get("model_code") or ""),
str(focus.get("title") or ""),
" ".join(str(item) for item in missing),
" ".join(str(item) for item in blockers),
]
).lower()
if "sweep" in text:
return "sweep in model, still needs confirmation" if missing or blockers else "sweep handled"
if "liquidity" in text:
return "liquidity path under review"
return "not confirmed"
def _format_chain_confirmation(*, missing: list, blockers: list[str], freshness_summary: dict) -> str:
if freshness_summary.get("is_stale"):
return "fresh data before any review"
if missing:
return _short_trader_wait_label(missing)
if blockers:
return _short_trader_wait_label(blockers)
return "manual review only, no execution"
def _format_chain_backtest(backtest_gate: dict) -> str:
if backtest_gate.get("passed"):
return "Pass"
reason = str(backtest_gate.get("reason") or "insufficient")
if "missing" in reason or "sample" in reason:
return "Insufficient"
return "Fail"
def _chain_clean(value) -> str:
text = str(value or "unresolved").strip()
return text.replace("_", " ")[:64]
def _build_trader_readout_data_state(*, data_confidence: str, freshness_summary: dict, readonly_live_health_summary: dict) -> str:
market = _short_runtime_state(readonly_live_health_summary.get("market_freshness") or "unknown")
stream = _short_runtime_state(readonly_live_health_summary.get("market_stream") or "unknown")
+21
View File
@@ -20,6 +20,8 @@
'Strategy Backtest': '策略回测',
'Trading Plan': '交易计划',
'Journal': '交易日志',
'Study Notes': '学习笔记',
'ICT Chain': 'ICT 链路',
'System Ops': '系统运维',
'Overview': '总览',
'Decision Assistant': '决策助手',
@@ -490,6 +492,25 @@
Object.assign(zhExact, zhDynamicTerms);
const zhPhrases = [
['Current chain:', '当前链路:'],
['Range/Location', '区间/位置'],
['main Draw', '主 Draw'],
['Liquidity event', '流动性事件'],
['wait for', '等待'],
['Backtest Pass', '回测通过'],
['Backtest Fail', '回测失败'],
['Backtest Insufficient', '回测样本不足'],
['therefore Review Now', '因此人工复核'],
['therefore Observe Only', '因此仅观察'],
['therefore Do Not Use', '因此不要使用'],
['missing active plan', '缺少激活计划'],
['missing primary draw', '缺少主 Draw'],
['sweep in model, still needs confirmation', '模型包含扫流动性,但仍需确认'],
['sweep handled', '已处理扫流动性'],
['liquidity path under review', '流动性路径待复核'],
['not confirmed', '尚未确认'],
['manual review only, no execution', '仅人工复核,不执行'],
['fresh data before any review', '复核前先恢复新鲜数据'],
['No clean execution signal is available yet.', '还没有干净的执行信号。'],
['Trust gate keeps this in observe-only review.', '信任门禁仍要求仅观察复核。'],
['No trade:', '先不交易:'],
+3
View File
@@ -405,6 +405,8 @@ function buildClientReadout(trader, focus, overrides = {}) {
? `Watch ${focusTitle}: ${waitFor}.`
: 'Wait for a fresh sweep/MSS/FVG setup to enter the queue.');
const focusReason = overrides.focus_reason
|| overrides.ict_chain_sentence
|| trader.trader_readout?.ict_chain_sentence
|| trader.trader_readout?.focus_reason
|| trader.focus_quality_explanation
|| (focus?.candidate_id ? `Focus: ${focusTitle}.` : 'No unique focus yet.');
@@ -441,6 +443,7 @@ function buildClientReadout(trader, focus, overrides = {}) {
focus_reason: reason,
data_state: dataState,
no_candidate_state: noCandidateState,
ict_chain_sentence: focusReason,
one_screen_cards: [
{ label: 'Now', value: now, tone: freshness.is_stale ? 'blocked' : 'focus' },
{ label: 'Reason', value: reason, tone: focus?.candidate_id ? 'focus' : 'muted' },
@@ -23,6 +23,7 @@ ACTIVE_TOP_LEVEL_DOCS = {
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
"AI_ICT_ICT主线概念字典_S2_2026-05-21_v1.md",
"AI_ICT_ReviewNow主线Gate_S3S4_2026-05-21_v1.md",
"AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md",
"AI_ICT_主线纠偏与功能裁剪方案_2026-05-20_v1.md",
"AI_ICT_实盘辅助分析决策助手上线必做清单_2026-05-18_v1.md",
"AI_ICT_实盘辅助分析决策助手使用说明_2026-05-18_v1.md",
+34
View File
@@ -5,8 +5,10 @@ from src.services.ict_mainline import (
build_ict_rulebook_mapping,
build_ict_rulebook_summary,
build_review_now_mainline_gate,
classify_book_rule_error,
classify_ict_concepts,
get_ict_concept,
summarize_book_rule_errors,
)
@@ -140,6 +142,38 @@ class IctMainlineTaxonomyTests(unittest.TestCase):
self.assertEqual(passed["status"], "passed")
self.assertTrue(passed["review_now_allowed"])
def test_feedback_actions_map_to_book_rule_error_classes(self) -> None:
no_trade = classify_book_rule_error(
{
"action": "no_trade",
"body": "No trade because the setup is waiting for MSS/FVG confirmation.",
},
source="operator_feedback",
)
self.assertEqual(no_trade["review_type"], "observation_review")
self.assertEqual(no_trade["error_class"], "confirmation_gap")
self.assertFalse(no_trade["execution_evidence_allowed"])
paper = classify_book_rule_error(
{
"action": "need_more_evidence",
"paper_ticket_id": 10,
"body": "Backtest sample size is too small.",
},
source="operator_feedback",
)
self.assertEqual(paper["review_type"], "paper_review")
self.assertEqual(paper["error_class"], "backtest_evidence_gap")
self.assertTrue(paper["paper_evidence_allowed"])
summary = summarize_book_rule_errors([no_trade, paper])
self.assertEqual(summary["classification_count"], 2)
self.assertEqual(summary["review_type_counts"]["observation_review"], 1)
self.assertEqual(summary["review_type_counts"]["paper_review"], 1)
self.assertEqual(summary["study_note_draft"]["status"], "draft_ready")
if __name__ == "__main__":
unittest.main()
+32 -1
View File
@@ -228,7 +228,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertEqual(trader_mode_summary["trader_readout"]["mode"], "trader_one_screen_readout")
self.assertIn("No trade", trader_mode_summary["trader_readout"]["why_no_trade"])
self.assertIn("Watch", trader_mode_summary["trader_readout"]["watch_next"])
self.assertIn("Focus:", trader_mode_summary["trader_readout"]["focus_reason"])
self.assertIn("Current chain:", trader_mode_summary["trader_readout"]["focus_reason"])
self.assertIn("ict_chain_sentence", trader_mode_summary["trader_readout"])
self.assertIn("backtest", trader_mode_summary["trader_readout"]["ict_chain_steps"])
self.assertIn("market", trader_mode_summary["trader_readout"]["data_state"])
self.assertIn("No Candidate", [item["label"] for item in trader_mode_summary["trader_readout"]["one_screen_cards"]])
self.assertEqual(trader_mode_summary["ict_rulebook_mapping"]["surface"], "trader_cockpit")
@@ -244,6 +246,15 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertEqual(trader_mode_summary["chart_context_bridge"]["external_chart_bridge_required"], False)
self.assertEqual(trader_mode_summary["post_market_review_draft"]["mode"], "auto_post_market_review_draft")
self.assertTrue(trader_mode_summary["post_market_review_draft"]["draft_sections"])
self.assertEqual(
trader_mode_summary["post_market_review_draft"]["review_type_counts"]["observation_review"],
2,
)
self.assertIn("feedback_loop_summary", trader_mode_summary["post_market_review_draft"])
self.assertEqual(
trader_mode_summary["post_market_review_draft"]["study_note_draft"]["status"],
"draft_ready",
)
self.assertIn("usable_today", trader_mode_summary["ops_duty_panel"])
self.assertTrue(trader_mode_summary["must_not_trade"])
self.assertFalse(trader_mode_summary["execution_allowed"])
@@ -1177,6 +1188,26 @@ class _LaunchReadinessService:
"execution_boundary": "real_live_still_blocked",
"operator_summary": "Shadow/paper validation is collecting paper samples.",
"daily_coverage": [],
"recent_samples": {
"shadow_observations": [
{
"operator_agreement": "no_trade",
"observation_status": "operator_feedback_recorded",
"system_recommendation": "stand_down_blocked_candidate",
"reason_codes": ["wait_for_mss"],
"paper_ticket_id": None,
"paper_execution_id": None,
},
{
"operator_agreement": "need_more_evidence",
"observation_status": "operator_feedback_recorded",
"system_recommendation": "observe_only",
"reason_codes": ["backtest_quality_gate_failed"],
"paper_ticket_id": None,
"paper_execution_id": None,
},
],
},
},
"market_state": {
"session": {"session_code": "LONDON", "status": "active"},
@@ -34,6 +34,10 @@ class TradingJournalServiceTests(unittest.TestCase):
self.assertIn("journal_feedback", mapping["present_concepts"])
self.assertIn("backtest_evidence", mapping["present_concepts"])
self.assertIn("trading_plan", mapping["present_concepts"])
classification = entry.summary["ict_feedback_classification"]
self.assertEqual(classification["review_type"], "execution_derived_review")
self.assertIn(classification["error_class"], {"risk_boundary_gap", "plan_alignment_gap", "execution_process_gap"})
self.assertTrue(classification["execution_evidence_allowed"])
def test_upsert_entry_persists_journal(self) -> None:
repo = _JournalRepo(_bundle())
@@ -81,6 +85,9 @@ class TradingJournalServiceTests(unittest.TestCase):
self.assertEqual(summary["high_quality_session"]["session_code"], "LONDON")
self.assertEqual(summary["low_quality_session"]["session_code"], "ASIA")
self.assertEqual(len(summary["next_week_actions"]), 3)
self.assertEqual(summary["review_type_counts"]["execution_derived_review"], 2)
self.assertTrue(summary["book_rule_error_class_counts"])
self.assertEqual(summary["study_note_draft"]["status"], "draft_ready")
class TradingJournalScriptTests(unittest.TestCase):