Add trader product promotion gate
This commit is contained in:
@@ -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