Add trader product promotion gate

This commit is contained in:
Codex
2026-05-22 11:17:04 +08:00
parent 4ecc40b284
commit 3a37036b56
12 changed files with 474 additions and 15 deletions
+2 -1
View File
@@ -851,10 +851,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- D4 PD Array / Reaction Kernel is complete, deployed, and remote-verified: added `DOCTRINE_REACTION_PHASE=D4_PD_ARRAY_REACTION_KERNEL`, `PdArrayReactionRule`, `GET /trading/pd-array-kernel`, Strategy Backtest `pd_array_backtest_features` / `pd_array_reaction_status` / `pd_array_reaction_summary`, and `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`. FVG / OB / Breaker / Mitigation / BPR/Rebalancing now stay reaction-location quality rankings rather than direction tags; `missing_pd_array`, `missing_pd_array_role`, `missing_array_location`, `fvg_is_not_setup`, `order_block_not_authorized`, and `breaker_without_failure_precondition` block promotion until role, rank, location, freshness, origin, reaction/retest, and invalidation are explicit. Validation passed focused D4/API/backtest/docs tests, full unit discovery (`839` tests), script static checks (`79` scripts), JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/pd-array-kernel` field smoke, `/trading/backtests` D4 field smoke, and Playwright screenshot evidence at `output/playwright/d4-pd-array-strategy-backtest-1366x768.png`. The read-only smoke initially exposed the expected external proxy blocker; starting the local v2rayN/sing-box proxy restored `192.168.3.73:10808` and the gate passed. No execution/readiness/risk/trust/operator-confirmation gate was changed.
- D5 Time / Session Kernel is complete, deployed, and remote-verified: added `DOCTRINE_SESSION_PHASE=D5_TIME_SESSION_KERNEL`, `TimeSessionRule`, `GET /trading/time-session-kernel`, Strategy Backtest `time_session_backtest_features` / `time_session_kernel_status` / `time_session_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`. Asia / London / NY AM / NY PM, killzone, Judas/manipulation window, opening range, weekly profile, and news/no-trade time are now measurable session-quality gates; `missing_session_window`, `session_window_unverified`, `outside_session_window`, `session_not_allowed`, `missing_killzone`, `killzone_not_active`, `judas_window_wait`, `opening_range_unresolved`, `weekly_profile_missing`, `news_time_no_trade`, and `session_invalidation_missing` block promotion until timing is explicit. Validation passed focused D5/API/backtest/docs tests, full unit discovery (`846` tests), script static checks (`79` scripts), secret static checks, JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/time-session-kernel` field smoke, `/trading/backtests` D5 field smoke, and Playwright screenshot evidence at `output/playwright/d5-time-session-strategy-backtest-1366x768.png`. Remote latest backtest `#513` is correctly blocked by D5 as `study_only / session_window_unverified` instead of treating `ASIA` plus `mark_outside` as a verified session window. No execution/readiness/risk/trust/operator-confirmation gate was changed.
- D6 Model Playbook Kernel is complete, deployed, and remote-verified: added `DOCTRINE_MODEL_PLAYBOOK_PHASE=D6_MODEL_PLAYBOOK_KERNEL`, `ModelPlaybookRule`, `GET /trading/model-playbook-kernel`, Strategy Backtest `model_playbook_backtest_features` / `model_playbook_kernel_status` / `model_playbook_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md`. Raw `model_code` values such as `SWEEP_MSS_FVG` now stay `study_only` until they resolve to a registered setup playbook with explicit Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target, batch cohort, quality-gate, journal-feedback, and operator-runbook contracts. Validation passed focused D6/API/backtest/docs tests, full unit discovery (`853` tests), script static checks (`79` scripts), secret static checks, JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/model-playbook-kernel` field smoke, `/trading/backtests` D6 field smoke, and Playwright screenshot evidence at `output/playwright/d6-model-playbook-strategy-backtest-fullpage-zh.png`. Remote latest backtest `#513` is correctly blocked by D6 as `study_only / missing_model_playbook` instead of promoting a surface-shape model as a tradeable setup. No execution/readiness/risk/trust/operator-confirmation gate was changed.
- D7 Trader Product Rebuild / Playbook Promotion Gate is complete, deployed, and remote-verified: added `build_trader_product_promotion_gate`, `candidate_watchlist_tiers.*[].trader_product_promotion_gate`, `candidate_watchlist_summary.trader_product_gate_summary`, `trader_mode_summary.trader_product_gate_summary`, and `docs/AI_ICT_DoctrineKernel_TraderProductGate_D7_2026-05-22_v1.md`. Trader Cockpit `Review Now` now requires playbook-mapped + D3/D4/D5/D6 Doctrine-complete + Strategy Backtest-supported + active Trading Plan-approved candidates; otherwise the candidate is routed to study-only / `archive_only`, the focus matrix becomes `study_only`, and Trader Mode refuses to fall back to a decision-packet focus. Remote latest Trader Mode payload correctly shows `allowed_focus_count=0`, `study_only_count=5`, status `Observe Only`, and no primary opportunity, while the latest raw backtest remains D6-blocked. Validation passed focused operator/doctrine/static/docs tests, full unit discovery (`853` tests), script static checks (`79` scripts), secret static checks, JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, direct Trader Mode D7 field smoke, and Playwright screenshot evidence at `output/playwright/d7-trader-product-gate-trader-mode-zh.png`. No execution/readiness/risk/trust/operator-confirmation gate was changed.
## Exact Next Step
Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md`, `docs/ICT交易全书_出版终校版.md`, `docs/AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md`, `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md`, `SESSION.md`, and `TASKS.md` as the active anchors. Start D7 Trader Product Rebuild / Playbook Promotion Gate: only playbook-mapped, backtest-supported, plan-approved setups can re-enter Trader Cockpit focus; keep study-only evidence in Strategy Backtest / Study Notes / System Ops, not trader-facing Review Now. Do not restart external-context/archive/digest long-tail work, do not add order-submission features, do not physically delete S1 deprecated surfaces yet, and do not fabricate R2 paper samples from blocked candidates. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md`, `docs/AI_ICT_DoctrineKernel_TraderProductGate_D7_2026-05-22_v1.md`, `docs/ICT交易全书_出版终校版.md`, `docs/AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md`, `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md`, `SESSION.md`, and `TASKS.md` as the active anchors. Start D8 Playbook-Specific Historical Cohort Gate: run and summarize batch backtests by registered `setup_playbook_id`, session, timeframe set, and model family so Strategy Backtest can distinguish which book-derived playbooks remain study-only versus eligible for manual review. Do not restart external-context/archive/digest long-tail work, do not add order-submission features, do not physically delete S1 deprecated surfaces yet, and do not fabricate R2 paper samples from blocked candidates. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
## Validation Commands Used Recently
+2 -1
View File
@@ -958,7 +958,8 @@
- [x] D4 PD Array / Reaction Kernel: rebuilt FVG / OB / Breaker / Mitigation / Rebalance / Void into reaction-location quality ranking and no-trade/wait rules; PD Array remains location/reaction evidence only, Strategy Backtest exposes PD Array features/status/summary, `/trading/pd-array-kernel` exposes the D4 registry, and remote deploy/ops/read-only smoke passed after the local proxy was restarted
- [x] D5 Time / Session Kernel: rebuilt Asia / London / NY AM / NY PM / killzone / Judas window / opening range / weekly profile / news time into measurable session-quality gates instead of loose time labels; Strategy Backtest now exposes `time_session_backtest_features`, `time_session_kernel_status`, `time_session_kernel_summary`, and `/trading/time-session-kernel` without changing execution/readiness gates
- [x] D6 Model Playbook Kernel: raw model codes now stay study-only until they resolve to registered setup playbooks with Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target, batch-cohort, quality-gate, journal-feedback, and operator-runbook contracts; Strategy Backtest exposes D6 features/status/summary and `/trading/model-playbook-kernel` exposes the D6 registry without changing execution/readiness gates
- [ ] D7 Trader Product Rebuild / Playbook Promotion Gate: only playbook-mapped, backtest-supported, plan-approved setups may re-enter Trader Cockpit focus; keep study-only evidence in Strategy Backtest / Study Notes / System Ops rather than Review Now
- [x] D7 Trader Product Rebuild / Playbook Promotion Gate: only playbook-mapped, backtest-supported, plan-approved setups may re-enter Trader Cockpit focus; study-only candidates are routed out of `Review Now` and remain Strategy Backtest / Study Notes / System Ops evidence
- [ ] D8 Playbook-Specific Historical Cohort Gate: run and summarize batch backtests by registered `setup_playbook_id`, session, timeframe set, and model family so Strategy Backtest can distinguish study-only playbooks from manual-review-eligible playbooks
- [x] R3 OKX sandbox isolated smoke tooling: guarded script and service can call OKX demo submit/query/cancel/query/fills only when `OKX_SANDBOX_SMOKE_EXECUTE=1`; local validation, deploy smoke, and remote ops smoke passed
- [ ] R3 sandbox credential permission gate deferred: current phase is assisted analysis / decision support only, so do not request trade-permission credentials or make submit/cancel smoke the next gate
- [ ] Long-term target: complete `Real-time Assisted Trading V1` without enabling default live auto-trading
@@ -0,0 +1,62 @@
# AI_ICT Doctrine Kernel Trader Product Gate D7 2026-05-22 v1
## Scope
D7 rebuilds the trader-facing product gate after D0-D6. The goal is not to add more candidate features. The goal is to keep Trader Cockpit focused on one thing:
`Only a playbook-mapped, backtest-supported, plan-approved setup may enter Trader Cockpit Review Now.`
Everything else remains study material in Strategy Backtest, Trading Plan, Journal, Study Notes, or System Ops.
## ICT Chain Served
D7 enforces the full 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`.
## Product Rule
Trader Cockpit may show `Review Now` only when all of these are true:
- D2 setup playbook mapping is present.
- D3 Liquidity Kernel passes.
- D4 PD Array / Reaction Kernel passes.
- D5 Time / Session Kernel passes.
- D6 Model Playbook Kernel passes.
- Strategy Backtest quality gate passes.
- The candidate is linked to the active Trading Plan.
- The candidate playbook matches the active Trading Plan playbook.
If any item fails, the candidate must not become the trader-facing unique focus. It may still be visible as study evidence outside `Review Now`.
## Implemented Gate
New gate:
- `build_trader_product_promotion_gate`
New Trader Mode fields:
- `candidate_watchlist_tiers.*[].trader_product_promotion_gate`
- `candidate_watchlist_tiers.*[].trader_product_surface`
- `candidate_watchlist_summary.trader_product_gate_summary`
- `trader_mode_summary.trader_product_gate_summary`
New behavior:
- Study-only candidates are routed to `archive_only` instead of `review_now` / `strong_watch`.
- If all available candidates are study-only, `candidate_watchlist_summary.focus_matrix.status = study_only`.
- Trader Mode does not fall back to a decision-packet focus when the product gate says the candidate is study-only.
- Trader Mode first screen says there is no trader-ready playbook and points the operator back to Strategy Backtest / Study Notes.
## Read-Only Boundary
D7 does not add order submission, does not change `execution_allowed`, does not change `decision_support_ready`, does not change `real_live_ready`, and does not loosen risk governor / trust bucket / operator confirmation protocol.
## Validation Meaning
A D7 pass means the product can safely keep weak or incomplete setup evidence away from the live trader cockpit. It does not mean the strategy is profitable or live-trade ready. Profitability still depends on future playbook-specific historical backtest cohorts and true paper/operator/weekly review evidence.
## Next Slice
D8 should use the D7 gate to rebuild the first registered playbook's historical cohort inputs: run batch backtests by `setup_playbook_id`, session, timeframe set, and model family, then summarize which exact book-derived playbooks remain study-only versus eligible for manual review.
Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

+2
View File
@@ -9,6 +9,7 @@ from src.services.ict_mainline.taxonomy import (
from src.services.ict_mainline.gates import (
build_backtest_quality_gate,
build_review_now_mainline_gate,
build_trader_product_promotion_gate,
build_trading_plan_gate,
candidate_mainline_blockers,
)
@@ -81,6 +82,7 @@ __all__ = [
"build_ict_rulebook_summary",
"build_backtest_quality_gate",
"build_review_now_mainline_gate",
"build_trader_product_promotion_gate",
"build_study_note_draft",
"build_trading_plan_gate",
"candidate_mainline_blockers",
+95 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from src.services.ict_mainline.doctrine import build_doctrine_playbook_gate
from src.services.ict_mainline.doctrine import build_doctrine_backtest_cohort, build_doctrine_playbook_gate
PLAN_BLOCKING_CONDITIONS = {
@@ -120,6 +120,92 @@ def build_review_now_mainline_gate(
}
def build_trader_product_promotion_gate(
*,
candidate: dict | None,
active_trading_plan: dict | None,
backtest_quality_gate: dict | None,
mainline_focus_gate: dict | None = None,
) -> dict:
candidate_payload = dict(candidate or {})
plan_payload = dict(active_trading_plan or {})
mainline_gate = dict(
mainline_focus_gate
or build_review_now_mainline_gate(
active_trading_plan=plan_payload,
backtest_quality_gate=backtest_quality_gate,
)
)
doctrine_cohort = build_doctrine_backtest_cohort(candidate_payload, quality_gate=backtest_quality_gate)
doctrine_promotion_gate = dict(doctrine_cohort.get("promotion_gate") or {})
plan_gate = dict(mainline_gate.get("trading_plan_gate") or {})
backtest_gate = dict(mainline_gate.get("backtest_quality_gate") or {})
model_playbook_status = dict(doctrine_cohort.get("model_playbook_kernel_status") or {})
candidate_blockers = candidate_mainline_blockers(candidate_payload, mainline_gate)
plan_playbook_id = _extract_playbook_id(plan_payload)
candidate_playbook_id = _extract_playbook_id(candidate_payload)
if plan_gate.get("passed") and plan_playbook_id:
if not candidate_playbook_id:
candidate_blockers.append("candidate_playbook_link_missing")
elif str(candidate_playbook_id) != str(plan_playbook_id):
candidate_blockers.append("candidate_playbook_plan_mismatch")
blockers = _dedupe(
[
*candidate_blockers,
*[str(item) for item in list(doctrine_promotion_gate.get("reasons") or []) if item],
]
)
playbook_mapped = bool(model_playbook_status.get("passed") and doctrine_promotion_gate.get("passed"))
backtest_supported = bool(backtest_gate.get("passed"))
plan_approved = bool(plan_gate.get("passed")) and not any(
item in blockers
for item in (
"candidate_plan_link_missing",
"candidate_plan_mismatch",
"candidate_playbook_link_missing",
"candidate_playbook_plan_mismatch",
)
)
review_now_allowed = bool(
mainline_gate.get("review_now_allowed")
and doctrine_promotion_gate.get("passed")
and playbook_mapped
and backtest_supported
and plan_approved
and not blockers
)
product_surface = "trader_cockpit" if review_now_allowed else "strategy_backtest_study_notes_system_ops"
return {
"status": "passed" if review_now_allowed else "blocked",
"allowed_in_trader_cockpit_focus": review_now_allowed,
"review_now_allowed": review_now_allowed,
"reason": "" if review_now_allowed else (blockers[0] if blockers else "trader_product_promotion_gate_blocked"),
"reasons": blockers,
"blockers": blockers,
"playbook_mapped": playbook_mapped,
"backtest_supported": backtest_supported,
"plan_approved": plan_approved,
"study_only": not review_now_allowed,
"product_surface": product_surface,
"candidate_id": candidate_payload.get("id"),
"trading_plan_id": candidate_payload.get("trading_plan_id"),
"active_plan_id": plan_gate.get("plan_id"),
"setup_playbook_id": candidate_playbook_id,
"active_plan_playbook_id": plan_playbook_id,
"model_playbook_status": model_playbook_status,
"doctrine_backtest_cohort": doctrine_cohort,
"mainline_focus_gate": mainline_gate,
"execution_boundary": "read_only_analysis_no_order_submission",
"operator_summary": (
"Trader Cockpit focus is allowed for read-only manual review only; no order submission is enabled."
if review_now_allowed
else "Keep this item out of Trader Cockpit Review Now; use Strategy Backtest, Study Notes, or System Ops until playbook, backtest, and plan gates all pass."
),
}
def candidate_mainline_blockers(candidate: dict, mainline_gate: dict) -> list[str]:
blockers = [str(item) for item in list(mainline_gate.get("blockers") or []) if item]
plan_gate = dict(mainline_gate.get("trading_plan_gate") or {})
@@ -147,3 +233,11 @@ def _dedupe(values: list[str]) -> list[str]:
def _string_or_empty(value) -> str:
return "" if value is None else str(value)
def _extract_playbook_id(payload: dict) -> str:
for key in ("setup_playbook_id", "model_playbook_id", "doctrine_setup_id", "playbook_id"):
value = payload.get(key)
if value:
return str(value)
return ""
@@ -18,6 +18,7 @@ from src.services.execution import ExecutionService
from src.services.ict_mainline import (
build_ict_rulebook_mapping,
build_review_now_mainline_gate,
build_trader_product_promotion_gate,
classify_book_rule_error,
candidate_mainline_blockers,
evaluate_doctrine_research_status,
@@ -1706,6 +1707,8 @@ def build_decision_assistant_summary(
session_focus=session_focus,
current_candidate_id=persisted_focus_candidate.get("id") or decision_packet.get("candidate_id"),
mainline_focus_gate=mainline_focus_gate,
active_trading_plan=active_trading_plan,
backtest_quality_gate=backtest_quality_gate,
)
candidate_watchlist_summary = _build_candidate_watchlist_summary(
candidate_watchlist_tiers,
@@ -2579,6 +2582,8 @@ def _build_candidate_watchlist_tiers(
session_focus: dict,
current_candidate_id,
mainline_focus_gate: dict,
active_trading_plan: dict,
backtest_quality_gate: dict,
) -> dict:
session_items = {
str(item.get("session_code") or ""): dict(item)
@@ -2594,12 +2599,19 @@ def _build_candidate_watchlist_tiers(
for candidate in candidates:
session_item = session_items.get(str(candidate.get("session_code") or ""), {})
doctrine_status = evaluate_doctrine_research_status(candidate, require_setup_playbook=True)
product_promotion_gate = build_trader_product_promotion_gate(
candidate=candidate,
active_trading_plan=active_trading_plan,
backtest_quality_gate=backtest_quality_gate,
mainline_focus_gate=mainline_focus_gate,
)
tier, tier_reason, priority_score, mainline_blockers = _classify_candidate_watchlist_tier(
candidate=candidate,
session_item=session_item,
current_candidate_id=current_candidate_id,
mainline_focus_gate=mainline_focus_gate,
doctrine_status=doctrine_status,
product_promotion_gate=product_promotion_gate,
)
missing_confirmations = _dedupe(
[
@@ -2618,6 +2630,8 @@ def _build_candidate_watchlist_tiers(
"review_now_allowed": tier == "review_now" and not mainline_blockers,
"mainline_gate": mainline_focus_gate,
"mainline_gate_blockers": mainline_blockers,
"trader_product_promotion_gate": product_promotion_gate,
"trader_product_surface": product_promotion_gate.get("product_surface"),
"doctrine_research_status": doctrine_status,
"research_only": bool(doctrine_status.get("research_only")),
"research_only_reason": doctrine_status.get("research_only_reason") or "",
@@ -2664,6 +2678,7 @@ def _build_candidate_watchlist_tiers(
def _build_candidate_watchlist_summary(tiers: dict, *, preferred_candidate_id=None) -> dict:
focus_matrix = _build_candidate_watchlist_focus_matrix(tiers, preferred_candidate_id=preferred_candidate_id)
product_gate_summary = _build_trader_product_gate_summary(tiers)
return {
"review_now_count": len(list(tiers.get("review_now") or [])),
"strong_watch_count": len(list(tiers.get("strong_watch") or [])),
@@ -2673,6 +2688,7 @@ def _build_candidate_watchlist_summary(tiers: dict, *, preferred_candidate_id=No
"focus_matrix": focus_matrix,
"next_watchlist_focus": focus_matrix.get("next_focus"),
"next_watchlist_focus_prompt": focus_matrix.get("next_focus_prompt"),
"trader_product_gate_summary": product_gate_summary,
}
@@ -2699,8 +2715,33 @@ def _build_candidate_watchlist_focus_matrix(tiers: dict, *, preferred_candidate_
session_code = str(item.get("session_code") or "OFF_HOURS")
tier_counts[tier] = int(tier_counts.get(tier) or 0) + 1
session_counts[session_code] = int(session_counts.get(session_code) or 0) + 1
focusable_rows = [
row for row in rows
if str(row.get("tier") or "") != "archive_only"
and dict(row.get("trader_product_promotion_gate") or {}).get("allowed_in_trader_cockpit_focus")
]
if not focusable_rows:
reason_counts: dict[str, int] = {}
for row in rows:
for reason in list((row.get("trader_product_promotion_gate") or {}).get("reasons") or []):
key = str(reason)
if key:
reason_counts[key] = int(reason_counts.get(key) or 0) + 1
first_reason = next(iter(reason_counts), "trader_product_promotion_gate_blocked")
return {
"status": "study_only",
"tier_counts": tier_counts,
"session_counts": session_counts,
"recommended_tier": None,
"recommended_session_code": None,
"next_focus": None,
"next_focus_prompt": (
f"Keep candidate evidence in Strategy Backtest / Study Notes until {first_reason.replace('_', ' ')} clears."
),
"blocked_reason_counts": reason_counts,
}
ranked = sorted(
rows,
focusable_rows,
key=lambda row: (
{"review_now": 1, "strong_watch": 2, "soft_watch": 3, "archive_only": 4}.get(str(row.get("tier") or "archive_only"), 5),
-float(row.get("priority_score") or 0.0),
@@ -2742,6 +2783,7 @@ def _classify_candidate_watchlist_tier(
current_candidate_id,
mainline_focus_gate: dict,
doctrine_status: dict | None = None,
product_promotion_gate: dict | None = None,
) -> tuple[str, str, float, list[str]]:
score = float(candidate.get("quality_score") or 0.0)
status = str(candidate.get("status") or candidate.get("candidate_state") or "")
@@ -2757,8 +2799,15 @@ def _classify_candidate_watchlist_tier(
mainline_blockers = _dedupe([
*candidate_mainline_blockers(candidate, mainline_focus_gate),
*doctrine_blockers,
*[str(item) for item in list((product_promotion_gate or {}).get("blockers") or []) if item],
])
priority_score = score
if not dict(product_promotion_gate or {}).get("allowed_in_trader_cockpit_focus"):
if candidate.get("id") == current_candidate_id:
priority_score += 120.0
if status == "executable" or bool(candidate.get("executable")):
priority_score += 100.0
return "archive_only", mainline_blockers[0] if mainline_blockers else "trader_product_promotion_gate_blocked", priority_score, mainline_blockers
if candidate.get("id") == current_candidate_id:
priority_score += 120.0
if mainline_blockers:
@@ -2780,6 +2829,45 @@ def _classify_candidate_watchlist_tier(
return "archive_only", "low_priority_or_low_context", priority_score, mainline_blockers
def _build_trader_product_gate_summary(tiers: dict) -> dict:
rows = []
for tier in ("review_now", "strong_watch", "soft_watch", "archive_only"):
rows.extend(dict(item) for item in list(tiers.get(tier) or []))
reason_counts: dict[str, int] = {}
surface_counts: dict[str, int] = {}
allowed_count = 0
study_only_count = 0
for row in rows:
gate = dict(row.get("trader_product_promotion_gate") or {})
if gate.get("allowed_in_trader_cockpit_focus"):
allowed_count += 1
if gate.get("study_only"):
study_only_count += 1
surface = str(gate.get("product_surface") or "unknown")
surface_counts[surface] = int(surface_counts.get(surface) or 0) + 1
for reason in list(gate.get("reasons") or []):
key = str(reason)
if key:
reason_counts[key] = int(reason_counts.get(key) or 0) + 1
blocked_count = max(0, len(rows) - allowed_count)
first_reason = next(iter(reason_counts), "")
return {
"status": "passed" if rows and allowed_count else ("study_only" if rows else "idle"),
"candidate_count": len(rows),
"allowed_focus_count": allowed_count,
"blocked_count": blocked_count,
"study_only_count": study_only_count,
"reason_counts": reason_counts,
"surface_counts": surface_counts,
"first_blocker": first_reason,
"operator_summary": (
"Only playbook-mapped, backtest-supported, plan-approved setups may enter Trader Cockpit focus."
if allowed_count
else "No candidate is trader-product ready; keep evidence in Strategy Backtest / Study Notes / System Ops."
),
}
def _candidate_tier_action_label(*, tier: str, candidate_id) -> str:
if tier == "review_now":
return f"run_confirm_check:{candidate_id}"
@@ -2894,6 +2982,7 @@ def _build_trader_mode_summary(
focus=focus,
decision_packet=decision_packet,
mainline_focus_gate=mainline_focus_gate,
candidate_watchlist_summary=candidate_watchlist_summary,
market_freshness=market_freshness,
market_stream=market_stream,
market_gap=market_gap,
@@ -2956,6 +3045,7 @@ def _build_trader_mode_summary(
data_confidence=data_confidence,
focus=focus,
mainline_focus_gate=mainline_focus_gate,
candidate_watchlist_summary=candidate_watchlist_summary,
visible_reasons=visible_reasons,
plain_language_reasons=plain_language_reasons,
next_action=next_action,
@@ -3002,6 +3092,7 @@ def _build_trader_mode_summary(
"next_action": next_action,
"trader_readout": trader_readout,
"mainline_focus_gate": mainline_focus_gate,
"trader_product_gate_summary": candidate_watchlist_summary.get("trader_product_gate_summary") or {},
"ict_rulebook_mapping": ict_rulebook_mapping,
"why_no_trade": trader_readout.get("why_no_trade"),
"watch_next": trader_readout.get("watch_next"),
@@ -3391,7 +3482,10 @@ def _resolve_trader_mode_focus_candidate(
top_opportunities: list[dict],
decision_packet: dict,
) -> dict:
next_focus = dict(((candidate_watchlist_summary.get("focus_matrix") or {}).get("next_focus")) or {})
focus_matrix = dict(candidate_watchlist_summary.get("focus_matrix") or {})
if focus_matrix.get("status") == "study_only":
return {}
next_focus = dict((focus_matrix.get("next_focus")) or {})
candidates = [
*list(candidate_watchlist_tiers.get("review_now") or []),
*list(candidate_watchlist_tiers.get("strong_watch") or []),
@@ -3451,6 +3545,7 @@ def _build_trader_mode_reasons(
focus: dict,
decision_packet: dict,
mainline_focus_gate: dict,
candidate_watchlist_summary: dict,
market_freshness: str,
market_stream: str,
market_gap: str,
@@ -3474,6 +3569,10 @@ def _build_trader_mode_reasons(
)
reasons.extend([str(item) for item in list(mainline_focus_gate.get("blockers") or []) if item])
reasons.extend([str(item) for item in list(focus.get("mainline_gate_blockers") or []) if item])
product_gate_summary = dict(candidate_watchlist_summary.get("trader_product_gate_summary") or {})
if not focus and int(product_gate_summary.get("blocked_count") or 0):
reasons.append(product_gate_summary.get("first_blocker") or "trader_product_promotion_gate_blocked")
reasons.append("study_only_not_trader_focus")
reasons.extend([f"waiting_for_{item}" for item in list(focus.get("missing_confirmations") or [])[:3]])
confirmation_chain = dict(decision_packet.get("confirmation_chain") or {})
reasons.extend([f"missing_{item}" for item in list(confirmation_chain.get("missing") or [])[:3]])
@@ -3540,6 +3639,7 @@ def _build_trader_mode_live_readout(
current_session: str,
market_posture: str,
readonly_live_health_summary: dict,
candidate_watchlist_summary: dict | None = None,
) -> dict:
why_no_trade = _build_trader_readout_why_no_trade(
status_tone=status_tone,
@@ -3585,6 +3685,7 @@ def _build_trader_mode_live_readout(
focus=focus,
current_session=current_session,
market_posture=market_posture,
product_gate_summary=dict((candidate_watchlist_summary or {}).get("trader_product_gate_summary") or {}),
)
now = _build_trader_readout_now(
status_label=status_label,
@@ -3699,6 +3800,8 @@ def _build_trader_readout_wait(*, status_tone: str, focus: dict, visible_reasons
return "Review the focus manually; then record Agree/Disagree/No Trade."
wait_for = _trader_wait_action_label(list(focus.get("missing_confirmations") or []) or visible_reasons)
return f"Wait for {wait_for}; then record Agree/Disagree/No Trade."
if visible_reasons:
return f"Wait for {_trader_wait_action_label(visible_reasons)}; keep evidence out of Review Now."
return "Wait for a complete sweep -> MSS -> FVG chain."
@@ -3953,9 +4056,19 @@ def _build_trader_readout_data_state(*, data_confidence: str, freshness_summary:
return f"{data_confidence}: market {market}, stream {stream}, runtime {runtime}, stale {stale_text}."
def _build_trader_readout_no_candidate_state(*, focus: dict, current_session: str, market_posture: str) -> str:
def _build_trader_readout_no_candidate_state(
*,
focus: dict,
current_session: str,
market_posture: str,
product_gate_summary: dict | None = None,
) -> str:
if focus:
return "Candidate present; drill down only if needed."
summary = dict(product_gate_summary or {})
if int(summary.get("blocked_count") or 0):
reason = str(summary.get("first_blocker") or "product gate blocked").replace("_", " ")
return f"No trader-ready playbook; keep study-only evidence in Strategy Backtest until {reason} clears."
return f"No clean candidate in {current_session}; wait sweep/MSS/FVG alignment."
@@ -4157,6 +4270,10 @@ def _short_trader_wait_label(values: list) -> str:
return "direction alignment"
if "signal_missing" in joined or "signal" in joined:
return "clean signal"
if "playbook" in joined or "product_promotion" in joined:
return "playbook product gate"
if "study_only" in joined:
return "study-only evidence"
if "trust" in joined:
return "trust gate"
return _humanize_trader_reason(tokens[0]).rstrip(".").lower()[:56]
@@ -4179,6 +4296,10 @@ def _trader_wait_gap_sentence(values: list) -> str:
return "direction alignment is missing"
if "signal_missing" in joined or "signal" in joined:
return "clean signal is missing"
if "playbook" in joined or "product_promotion" in joined:
return "playbook product gate is not cleared yet"
if "study_only" in joined:
return "evidence is study-only"
if "trust" in joined:
return "trust gate is not cleared yet"
return f"{_short_trader_wait_label(tokens)} is not confirmed yet"
@@ -4201,6 +4322,14 @@ def _trader_wait_action_label(values: list) -> str:
return "direction alignment"
if "signal_missing" in joined or "signal" in joined:
return "a clean signal"
if "sample_size" in joined or "executed_sample" in joined:
return "more backtest samples"
if "profit_factor" in joined or "expectancy" in joined or "backtest_quality" in joined:
return "historical edge to pass"
if "playbook" in joined or "product_promotion" in joined:
return "the playbook product gate"
if "study_only" in joined:
return "study-only evidence to become trader-ready"
if "trust" in joined:
return "trust gate clearance"
return _short_trader_wait_label(tokens)
@@ -4228,6 +4357,12 @@ def _humanize_trader_reason(reason: str) -> str:
mapping = {
"not_a_buy_sell_instruction": "This is analysis only, not a buy or sell instruction.",
"no_clean_focus_candidate": "No single candidate is clean enough for the first screen.",
"trader_product_promotion_gate_blocked": "No candidate has passed the playbook, backtest, and plan product gate.",
"study_only_not_trader_focus": "Study-only evidence stays in Strategy Backtest or Study Notes, not Trader Cockpit focus.",
"candidate_playbook_link_missing": "Candidate is not linked to a book-derived setup playbook.",
"candidate_playbook_plan_mismatch": "Candidate playbook does not match the active Trading Plan.",
"candidate_plan_link_missing": "Candidate is not linked to the active Trading Plan.",
"candidate_plan_mismatch": "Candidate does not match the active Trading Plan.",
"signal_missing": "No clean execution signal is available yet.",
"trust_model_decision_support_restricted": "Trust gate keeps this in observe-only review.",
"market_timestamp_stale": "Market data is stale; refresh the feed before using the brief.",
+4
View File
@@ -1387,6 +1387,10 @@ function renderDecisionAssistant(summary) {
['focus reason', traderReadout.focus_reason || traderModeSummary.focus_quality_explanation || 'n/a'],
['data state', traderReadout.data_state || `${traderModeSummary.data_confidence || 'unknown'} / ${traderModeSummary.status_color || 'unknown'}`],
['no candidate state', traderReadout.no_candidate_state || traderModeSummary.no_candidate_state || 'n/a'],
[
'product gate',
`${traderModeSummary.trader_product_gate_summary?.allowed_focus_count ?? 0} ready / ${traderModeSummary.trader_product_gate_summary?.study_only_count ?? 0} study-only`,
],
['trade action', traderModeSummary.primary_question_trade_action || 'not_a_buy_sell_instruction'],
],
(traderModeSummary.drilldown_sections || []).slice(0, 6).map((section) => ({
+55
View File
@@ -95,6 +95,13 @@
'Model Playbook': '模型 Playbook',
'Model Family': '模型族',
'Model Playbook chain mapped': '模型 Playbook 链路已映射',
'Product Gate': '产品门',
'product gate': '产品门',
'trader_product_promotion_gate_blocked': '交易员产品晋级门阻塞',
'study_only_not_trader_focus': '仅研究,不进入交易员焦点',
'study-only': '仅研究',
'study only': '仅研究',
'ready': '就绪',
'Killzone': 'Killzone 时段',
'Mapped': '已映射',
'mapped': '已映射',
@@ -276,8 +283,36 @@
'Wait for sweep -> MSS -> FVG alignment.': '等待扫流动性 -> MSS -> FVG 对齐。',
'Risk: forcing a setup before the ICT chain completes.': '风险:ICT 链路未完成就强行看形态。',
'No complete profile-matching ICT chain yet.': '还没有匹配当前偏好模板的完整 ICT 链路。',
'Profile filters excluded the current candidate from the fast-read view.': '当前候选被偏好模板过滤,不能进入速读焦点。',
'The current candidate is outside this trader profile.': '当前候选不属于这个交易员偏好模板。',
'Stay in observe-only mode until a matching setup appears.': '匹配形态出现前保持仅观察。',
'This is analysis only, not a buy or sell instruction.': '这只是分析,不是买入或卖出指令。',
'No clean focus': '没有干净焦点',
'No clean opportunity': '没有干净机会',
'no clean opportunity': '没有干净机会',
'clean opportunity': '干净机会',
'no profile match': '不匹配模板',
'no_profile_match': '不匹配模板',
'draft ready': '草稿就绪',
'agree': '同意',
'pending': '待处理',
'stabilize news event source': '稳定新闻事件来源',
'Watch for the next clean setup; current action is stabilize news event source.': '等待下一个干净形态;当前动作是稳定新闻事件来源。',
'No clean live setup. Continue observation in OFF_HOURS; OFF_HOURS | missing | market_data_fresh | market_stream_connecting | bias=bullish.': '没有干净实盘形态。继续在 OFF_HOURS 观察;OFF_HOURS | 缺失 | 行情新鲜 | 行情流连接中 | 方向=看多。',
'No single setup is clean enough to deserve the trader\'s first glance.': '没有任何形态干净到值得交易员第一眼优先看。',
'Watch Decision packet: clean signal.': '观察决策包:等待干净信号。',
'No trade: wait clean signal.': '先不交易:等待干净信号。',
'Structure is liquidity sweep into market structure shift into FVG retrace; wait for a clean signal before treating it as actionable; do not act because the trust gate still keeps this in observe-only mode.': '结构是扫流动性 -> MSS -> FVG 回撤;等干净信号后才可视为可行动;信任门禁仍要求仅观察,不要行动。',
'Refresh runtime/feed first; ignore setup output until status leaves Do Not Use.': '先刷新运行状态/行情源;状态离开“不要使用”前忽略形态输出。',
'No trade: restore market feed first.': '先不交易:先恢复行情源。',
'Pause live analysis until data, runtime, or risk blockers clear.': '暂停实盘分析,直到数据、运行或风险阻塞清除。',
'This brief is paused because the data surface is not safe enough to read.': '当前速读已暂停,因为数据面还不安全。',
'No matching candidate. Stay observe-only.': '没有匹配候选,保持仅观察。',
'No profile-matching candidate; keep the queue open and do not use another profile\'s setup.': '没有匹配当前模板的候选;保持队列打开,不要套用其他模板的形态。',
'Risk: forcing another profile into this plan.': '风险:把其他模板的形态强行套进当前计划。',
'Historical edge unavailable: check Strategy Backtest before trusting this setup.': '历史优势不可用:信任该形态前先查看策略回测。',
'Keep this as an observation sample; do not convert it into execution evidence.': '这只作为观察样本;不要转换成执行证据。',
'No trader-ready playbook; keep study-only evidence in Strategy Backtest until sample size too small clears.': '没有可进入交易员首屏的 Playbook;样本太少的问题清除前,只把证据放在策略回测里学习。',
'Historical edge unavailable: Strategy Backtest data is not reachable.': '历史优势不可用:策略回测数据暂时不可达。',
'No historical edge yet: run Strategy Backtest before trusting this setup.': '还没有历史优势:信任该形态前先运行策略回测。',
'Read-only analysis': '只读分析',
@@ -850,6 +885,16 @@
['missing_model_quality_gate_contract', '缺少模型质量门合同'],
['missing_model_journal_feedback', '缺少模型复盘反馈合同'],
['missing_model_operator_runbook', '缺少模型操作话术'],
['Product Gate', '产品门'],
['product gate', '产品门'],
['trader_product_promotion_gate_blocked', '交易员产品晋级门阻塞'],
['study_only_not_trader_focus', '仅研究,不进入交易员焦点'],
['candidate_playbook_link_missing', '候选缺少 Playbook 绑定'],
['candidate_playbook_plan_mismatch', '候选 Playbook 与交易计划不一致'],
['candidate_plan_link_missing', '候选缺少交易计划绑定'],
['candidate_plan_mismatch', '候选与当前交易计划不一致'],
['study-only', '仅研究'],
['ready', '就绪'],
['Killzone', 'Killzone 时段'],
['cohorts', '个分组'],
['cohort', '个分组'],
@@ -874,6 +919,7 @@
['active_plan_ready', '盘前计划已就绪'],
['Review Now is downgraded to Observe Only until Trading Plan and Strategy Backtest evidence both pass.', '盘前计划和回测证据同时通过前,Review Now 降级为仅观察。'],
['Review Now is downgraded to Observe Only until Doctrine playbook, Trading Plan, and Strategy Backtest evidence all pass.', 'Doctrine playbook、交易计划和回测证据全部通过前,Review Now 降级为仅观察。'],
['study only', '仅研究'],
['positive_expectancy', '正期望'],
['operator_feedback_sample_gate', '操作员反馈样本门'],
['framework_execution_rate_gate', '框架执行率门'],
@@ -1035,6 +1081,15 @@
];
const zhPatterns = [
[/^No trade: Trust gate keeps this in observe-only review\.$/i, '先不交易:信任门禁仍要求仅观察复核。'],
[/^collect more executed samples; review cross-session trade selection; review risk process$/i, '继续收集真实执行样本;复核跨时段交易选择;复核风险流程'],
[/^collect more executed samples; review weekly trade selection; review risk process$/i, '继续收集真实执行样本;复核周度交易选择;复核风险流程'],
[/^Wait for a (.+) candidate\.$/i, '等待 $1 候选。'],
[/^Wait for a (.+) candidate to enter the queue\.$/i, '等待 $1 候选进入队列。'],
[/^No focus matches (.+); stay observe-only and wait for a matching setup\.$/i, '当前没有匹配 $1 的焦点;保持仅观察,等待匹配形态。'],
[/^Observe Only: no (.+) focus; no trade\.$/i, '仅观察:没有 $1 焦点;不交易。'],
[/^No trade: no candidate matches (.+)\.$/i, '先不交易:没有候选匹配 $1。'],
[/^No trader-ready playbook; keep study-only evidence in Strategy Backtest until (.+) clears\.$/i, '没有可进入交易员首屏的 Playbook;在 $1 清除前,证据只放在策略回测里学习。'],
[/^Observe Only: wait for (.+); no trade\.$/i, '仅观察:等待 $1;不交易。'],
[/^Observe Only: (.+); no trade\.$/i, '仅观察:$1;不交易。'],
[/^Wait for (.+); then record Agree\/Disagree\/No Trade\.$/i, '等待 $1;然后记录同意/不同意/不交易。'],
+6
View File
@@ -814,6 +814,12 @@ function renderTraderMode(payload) {
traderFocusPanel.appendChild(createMetric('Focus Candidate', focus.title || 'no clean opportunity', `id=${focus.candidate_id ?? 'n/a'}`));
traderFocusPanel.appendChild(createMetric('Session / Model', `${humanizeToken(focus.session_code)} / ${humanizeToken(focus.model_code)}`));
traderFocusPanel.appendChild(createMetric('State', `${humanizeToken(focus.tier)} / ${humanizeToken(focus.status)}`));
const productGate = trader.trader_product_gate_summary || {};
traderFocusPanel.appendChild(createMetric(
'Product Gate',
humanizeToken(productGate.status || 'idle'),
`${productGate.allowed_focus_count ?? 0} ready / ${productGate.study_only_count ?? 0} study-only`,
));
traderFocusPanel.appendChild(createMetric('Why This Focus', trader.focus_quality_explanation || 'No single clean focus yet.'));
traderFocusPanel.appendChild(createMetric('Trade Action', humanizeTradeAction(trader.primary_question_trade_action || 'not_a_buy_sell_instruction')));
clearNode(traderReasonList);
@@ -24,6 +24,7 @@ ACTIVE_TOP_LEVEL_DOCS = {
"AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md",
"AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md",
"AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md",
"AI_ICT_DoctrineKernel_TraderProductGate_D7_2026-05-22_v1.md",
"AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md",
"AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md",
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
+107 -9
View File
@@ -250,16 +250,19 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertTrue(any(item["kind"] == "decision_packet" for item in summary["top_priority_items"]))
self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"][0]["candidate_id"], 18)
self.assertTrue(summary["candidate_watchlist_tiers"]["review_now"][0]["review_now_allowed"])
self.assertTrue(summary["candidate_watchlist_tiers"]["review_now"][0]["trader_product_promotion_gate"]["allowed_in_trader_cockpit_focus"])
self.assertFalse(summary["candidate_watchlist_tiers"]["review_now"][0]["research_only"])
self.assertEqual(summary["mainline_focus_gate"]["status"], "passed")
self.assertTrue(summary["mainline_focus_gate"]["review_now_allowed"])
self.assertEqual(summary["candidate_watchlist_tiers"]["strong_watch"][0]["candidate_id"], 21)
self.assertTrue(summary["candidate_watchlist_tiers"]["strong_watch"][0]["research_only"])
self.assertEqual(summary["candidate_watchlist_tiers"]["archive_only"][0]["candidate_id"], 21)
self.assertTrue(summary["candidate_watchlist_tiers"]["archive_only"][0]["research_only"])
self.assertEqual(
summary["candidate_watchlist_tiers"]["strong_watch"][0]["research_only_reason"],
summary["candidate_watchlist_tiers"]["archive_only"][0]["research_only_reason"],
"missing_doctrine_setup_playbook",
)
self.assertEqual(summary["candidate_watchlist_tiers"]["soft_watch"][0]["candidate_id"], 22)
self.assertEqual(summary["candidate_watchlist_tiers"]["archive_only"][1]["candidate_id"], 22)
self.assertEqual(summary["candidate_watchlist_summary"]["trader_product_gate_summary"]["allowed_focus_count"], 1)
self.assertGreaterEqual(summary["candidate_watchlist_summary"]["trader_product_gate_summary"]["study_only_count"], 1)
self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"][0]["action_method"], "POST")
self.assertIn("/trading/candidates/18/confirm-check", summary["candidate_watchlist_tiers"]["review_now"][0]["action_path"])
self.assertIn("trader_focus_reason", summary["candidate_watchlist_tiers"]["review_now"][0])
@@ -283,6 +286,7 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertLessEqual(len(trader_mode_summary["key_reasons"]), 3)
self.assertIn("focus_quality_explanation", trader_mode_summary)
self.assertIn("plain_language_reasons", trader_mode_summary)
self.assertEqual(trader_mode_summary["trader_product_gate_summary"]["allowed_focus_count"], 1)
self.assertLessEqual(len(trader_mode_summary["plain_language_reasons"]), 3)
self.assertEqual(trader_mode_summary["trader_readout"]["mode"], "trader_one_screen_readout")
self.assertIn("now", trader_mode_summary["trader_readout"])
@@ -948,8 +952,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertEqual(summary["mainline_focus_gate"]["status"], "blocked")
self.assertIn("missing_active_trading_plan", summary["mainline_focus_gate"]["blockers"])
self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"], [])
self.assertEqual(summary["candidate_watchlist_tiers"]["strong_watch"][0]["candidate_id"], 18)
self.assertIn("missing_active_trading_plan", summary["candidate_watchlist_tiers"]["strong_watch"][0]["mainline_gate_blockers"])
self.assertEqual(summary["candidate_watchlist_tiers"]["archive_only"][0]["candidate_id"], 18)
self.assertIn("missing_active_trading_plan", summary["candidate_watchlist_tiers"]["archive_only"][0]["mainline_gate_blockers"])
self.assertEqual(summary["candidate_watchlist_summary"]["focus_matrix"]["status"], "study_only")
self.assertEqual(summary["trader_mode_summary"]["status"], "observe_only")
self.assertEqual(summary["trader_mode_summary"]["status_label"], "Observe Only")
@@ -992,12 +997,58 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertEqual(summary["trader_mode_summary"]["status"], "observe_only")
self.assertIn("sample_size_too_small", summary["trader_mode_summary"]["key_reasons"])
readout = summary["trader_mode_summary"]["trader_readout"]
self.assertIn("not cleared", readout["now"])
self.assertIn("only a watch", readout["reason"])
self.assertIn("trust gate clearance", readout["wait"])
self.assertIn("No trader-ready playbook", readout["now"])
self.assertIn("Strategy Backtest", readout["no_candidate_state"])
self.assertIn("more backtest samples", readout["wait"])
self.assertIn("Study only", readout["historical_edge"])
self.assertIn("no historical edge", readout["historical_edge"])
def test_decision_assistant_d7_keeps_study_only_candidates_out_of_trader_focus(self) -> None:
service = DecisionAssistantSummaryService(
launch_readiness_service=_LaunchReadinessService(),
session_focus_service=_SessionFocusSummaryService(),
operator_digest_service=_OperatorDigestSummaryService(),
decision_repository=_PreviousPacketRepo(),
candidate_repository=_StudyOnlyCandidateWatchlistRepo(),
research_council_service=_ResearchCouncilSummaryService(),
exchange_status_provider=_ExchangeStatusProvider(),
announcements_provider=_AnnouncementsProvider(),
external_chart_provider=_ExternalChartProvider(),
decision_assistant_repository=_DecisionAssistantRepo(),
market_state_repository=_BacktestQualityGateRepo(passed=True),
)
summary = service.build_summary(
instrument_id=1,
timeframe="5m",
decision_packet={
"status": "actionable",
"action_recommendation": "confirm_ticket",
"candidate_id": 77,
"packet_ts": datetime.now(timezone.utc).isoformat(),
"setup_type": "SWEEP_MSS_FVG",
"why_allowed": ["candidate_confirmed"],
"why_blocked": [],
},
weekly_review={"next_week_actions": [], "session_performance": {}},
recent_alerts=[],
recent_notes=[],
recent_executions=[],
)
product_summary = summary["candidate_watchlist_summary"]["trader_product_gate_summary"]
self.assertEqual(product_summary["allowed_focus_count"], 0)
self.assertEqual(product_summary["study_only_count"], 1)
self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"], [])
self.assertEqual(summary["candidate_watchlist_summary"]["focus_matrix"]["status"], "study_only")
self.assertIsNone(summary["candidate_watchlist_summary"]["next_watchlist_focus"])
trader_mode_summary = summary["trader_mode_summary"]
self.assertEqual(trader_mode_summary["status_label"], "Observe Only")
self.assertEqual(trader_mode_summary["primary_opportunity"], {})
self.assertIn("Strategy Backtest", trader_mode_summary["trader_readout"]["no_candidate_state"])
self.assertIn("study-only", trader_mode_summary["trader_readout"]["no_candidate_state"])
self.assertTrue(any("Candidate is not linked" in reason for reason in trader_mode_summary["plain_language_reasons"]))
def test_decision_assistant_trader_mode_freezes_stale_payload(self) -> None:
service = DecisionAssistantSummaryService(
launch_readiness_service=_StaleLaunchReadinessService(),
@@ -1407,7 +1458,37 @@ class _CandidateWatchlistRepo:
"allowed_sessions": ["LONDON"],
"path_context": "sweep_then_displacement_then_retrace",
"sequence_validity": "sweep_mss_fvg_retrace",
"liquidity_target_side": "sell_side",
"liquidity_pool_id": "london_ssl_1",
"liquidity_pool_rank": "primary",
"liquidity_event": "sweep",
"swept_level": 94.8,
"sweep_confirmed": True,
"raid_stop_run_kind": "sell_side_sweep",
"manipulation_window_state": "complete",
"post_take_reaction": "displacement",
"pd_array_type": "fvg",
"pd_array_id": "fvg_london_1",
"pd_array_role": "entry_retrace",
"pd_array_rank": "primary",
"array_location": "discount",
"array_timeframe": "5m",
"array_freshness": "fresh",
"formation_event": "post_sweep_displacement",
"displacement_origin": "mss_london_1",
"fvg_role": "entry_retrace",
"fvg_retrace_confirmed": True,
"reaction_quality": "clean_rejection",
"session_window_state": "verified",
"killzone_name": "London Open",
"killzone_state": "active",
"judas_window_state": "complete",
"opening_range_state": "resolved",
"weekly_profile": "weekly_draw_to_external_liquidity",
"news_window_state": "clear",
"session_invalidation": "range_low_lost",
"invalidations": ["range_low_reclaim_failed"],
"invalidation": "range_low_lost",
"stop_loss": 94.5,
"invalid_level": 94.5,
"target_plan": {"tp1": 100.0, "tp2": 104.0, "irl": "near_fvg", "erl": "range_high"},
@@ -1471,6 +1552,23 @@ class _NoActivePlanCandidateWatchlistRepo(_CandidateWatchlistRepo):
return None
class _StudyOnlyCandidateWatchlistRepo(_CandidateWatchlistRepo):
def fetch_recent_candidates(self, *, instrument_id: int, limit: int = 20):
return [
{
"id": 77,
"trading_plan_id": 101,
"session_code": "LONDON",
"status": "executable",
"executable": True,
"quality_score": 91.0,
"model_code": "SWEEP_MSS_FVG",
"ai_explanation": "Raw surface-shape candidate without Doctrine playbook mapping.",
"missing_confirmations": [],
}
]
class _BacktestQualityGateRepo:
def __init__(self, *, passed: bool, reasons=None) -> None:
self.passed = passed