Add ICT cockpit feedback loop
This commit is contained in:
+3
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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。
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:', '先不交易:'],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user