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
@@ -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