diff --git a/SESSION.md b/SESSION.md index e738d32..b8fe290 100644 --- a/SESSION.md +++ b/SESSION.md @@ -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 diff --git a/TASKS.md b/TASKS.md index b4617c7..176dd1b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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`) diff --git a/docs/AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md b/docs/AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md new file mode 100644 index 0000000..da1e337 --- /dev/null +++ b/docs/AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md @@ -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。 diff --git a/src/services/ict_mainline/__init__.py b/src/services/ict_mainline/__init__.py index 602cd64..abfa8c5 100644 --- a/src/services/ict_mainline/__init__.py +++ b/src/services/ict_mainline/__init__.py @@ -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", ] diff --git a/src/services/ict_mainline/feedback.py b/src/services/ict_mainline/feedback.py new file mode 100644 index 0000000..9422690 --- /dev/null +++ b/src/services/ict_mainline/feedback.py @@ -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 diff --git a/src/services/journal/trading_journal_service.py b/src/services/journal/trading_journal_service.py index 89d361d..f635084 100644 --- a/src/services/journal/trading_journal_service.py +++ b/src/services/journal/trading_journal_service.py @@ -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", { diff --git a/src/services/journal/weekly_review_service.py b/src/services/journal/weekly_review_service.py index 40a776b..1269bf4 100644 --- a/src/services/journal/weekly_review_service.py +++ b/src/services/journal/weekly_review_service.py @@ -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"], } diff --git a/src/services/operator/operator_workspace_service.py b/src/services/operator/operator_workspace_service.py index 67290f7..545d38f 100644 --- a/src/services/operator/operator_workspace_service.py +++ b/src/services/operator/operator_workspace_service.py @@ -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") diff --git a/src/web/i18n.js b/src/web/i18n.js index aa408eb..06d692f 100644 --- a/src/web/i18n.js +++ b/src/web/i18n.js @@ -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:', '先不交易:'], diff --git a/src/web/trader_mode.js b/src/web/trader_mode.js index d8f92d0..02e4222 100644 --- a/src/web/trader_mode.js +++ b/src/web/trader_mode.js @@ -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' }, diff --git a/tests/unit/test_documentation_consistency.py b/tests/unit/test_documentation_consistency.py index 0935d52..d024f0c 100644 --- a/tests/unit/test_documentation_consistency.py +++ b/tests/unit/test_documentation_consistency.py @@ -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", diff --git a/tests/unit/test_ict_mainline_taxonomy.py b/tests/unit/test_ict_mainline_taxonomy.py index cfd0b97..cf289db 100644 --- a/tests/unit/test_ict_mainline_taxonomy.py +++ b/tests/unit/test_ict_mainline_taxonomy.py @@ -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() diff --git a/tests/unit/test_operator_workspace_service.py b/tests/unit/test_operator_workspace_service.py index ab761b6..b245739 100644 --- a/tests/unit/test_operator_workspace_service.py +++ b/tests/unit/test_operator_workspace_service.py @@ -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"}, diff --git a/tests/unit/test_trading_journal_service.py b/tests/unit/test_trading_journal_service.py index 0bd6cba..b2ff2e3 100644 --- a/tests/unit/test_trading_journal_service.py +++ b/tests/unit/test_trading_journal_service.py @@ -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):