Add trader product promotion gate
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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;然后记录同意/不同意/不交易。'],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user