Improve Trader Mode usefulness readout

This commit is contained in:
Codex
2026-05-20 13:25:16 +08:00
parent 6edf2e39ae
commit 84d6b49be6
16 changed files with 634 additions and 35 deletions
+12 -3
View File
@@ -206,11 +206,13 @@ class ApiStaticServerTests(unittest.TestCase):
for expected in (
"Trader Mode / Live Brief",
"decision now",
"three key reasons",
"data confidence",
"why no trade",
"watch next",
"focus reason",
"data state",
"no candidate state",
"trade action",
"not_a_buy_sell_instruction",
"boundary badges",
"trader-mode-card",
"workflow operations",
"workflow drift",
@@ -453,6 +455,7 @@ class ApiStaticServerTests(unittest.TestCase):
"Status",
"Decision Now",
"Next Action",
"Live Readout",
"Market Last Updated",
"Review Packet Updated",
"Runtime Online",
@@ -501,6 +504,12 @@ class ApiStaticServerTests(unittest.TestCase):
"aria-keyshortcuts",
"pause_or_resume_auto_refresh",
"profile_filter_status",
"trader_readout",
"Why No Trade",
"Watch Next",
"Focus Reason",
"Data State",
"No Candidate",
):
self.assertIn(expected, js_text)
@@ -136,6 +136,28 @@ class LaunchReadinessServiceTests(unittest.TestCase):
self.assertFalse(payload["execution_assist_ready"])
self.assertFalse(payload["real_live_ready"])
def test_live_analysis_treats_broker_sync_degraded_as_readonly_warning(self) -> None:
service = LaunchReadinessService(
market_state_service=_RestFreshMarketStateService(),
broker_read_model_service=_BrokerReadService(status="broker_state_unavailable"),
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
shadow_paper_validation_service=_ShadowPaperValidationService(),
)
with patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeServiceWithBrokerSyncBlocker()):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["live_analysis_assistant_ready"])
self.assertEqual(payload["product_operating_mode"], "readonly_live_analysis")
self.assertIn("runtime_ready", payload["blocking_reasons"])
self.assertIn("runtime_broker_sync_degraded_readonly_warning", payload["live_analysis_assistant_summary"]["warnings"])
self.assertIn("account_context_unavailable", payload["live_analysis_assistant_summary"]["warnings"])
self.assertEqual(payload["live_analysis_assistant_summary"]["blockers"], [])
self.assertFalse(payload["execution_assist_ready"])
self.assertFalse(payload["real_live_ready"])
def test_build_summary_exposes_restricted_soft_rollout_when_only_blacklist_models_exist(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
@@ -306,6 +328,14 @@ class _RuntimeServiceWithStreamBlocker:
}
class _RuntimeServiceWithBrokerSyncBlocker:
def status(self):
return {
"scheduler_state": "running",
"current_blockers": ["broker_sync_degraded:BTC-USDT:okx_read_credentials_missing"],
}
class _ShadowPaperValidationService:
def build_summary(self, **kwargs):
return {
@@ -144,6 +144,36 @@ class MaintenanceSummaryServiceTests(unittest.TestCase):
self.assertEqual(summary["runtime"]["current_blockers"], [])
self.assertEqual(summary["runtime"]["suppressed_blockers"], ["market_stream_degraded:BTC-USDT:market_stream_connecting"])
def test_build_summary_suppresses_broker_sync_blocker_for_readonly(self) -> None:
service = MaintenanceSummaryService(
capture_retention_service=_CaptureRetentionService(
{"status": "completed", "eligible_file_count": 0, "deleted_file_count": 0}
),
alert_quality_service=_AlertQualityService(
{
"delivery_summary": {"failed_count": 0, "retry_exhausted_skip_count": 0, "channel_disabled_count": 0},
"alert_feedback_summary": {"count": 1},
"visual_feedback_summary": {"count": 1},
"quality_flags": [],
}
),
incident_journal_service=_IncidentJournalService(
{"status": "idle", "incident_count": 0, "severity_counts": {}, "recommended_actions": []}
),
runtime_status_provider=lambda: {
"current_blockers": ["broker_sync_degraded:BTC-USDT:okx_read_credentials_missing"],
"current_warnings": [],
"runtime_watchdog": {"stalled_task_count": 0},
"runtime_health": {"level": "blocked"},
},
)
summary = service.build_summary(instrument_id=1)
self.assertEqual(summary["status"], "healthy")
self.assertEqual(summary["runtime"]["current_blockers"], [])
self.assertEqual(summary["runtime"]["suppressed_blockers"], ["broker_sync_degraded:BTC-USDT:okx_read_credentials_missing"])
if __name__ == "__main__":
unittest.main()
@@ -171,6 +171,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertEqual(summary["candidate_watchlist_tiers"]["soft_watch"][0]["candidate_id"], 22)
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])
self.assertIn("trader_no_trade_reason", summary["candidate_watchlist_tiers"]["review_now"][0])
self.assertIn("trader_watch_next", summary["candidate_watchlist_tiers"]["review_now"][0])
trader_mode_summary = summary["trader_mode_summary"]
self.assertEqual(trader_mode_summary["mode"], "trader_mode_live_brief")
self.assertEqual(trader_mode_summary["status"], "review_now")
@@ -189,6 +192,12 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("focus_quality_explanation", trader_mode_summary)
self.assertIn("plain_language_reasons", trader_mode_summary)
self.assertLessEqual(len(trader_mode_summary["plain_language_reasons"]), 3)
self.assertEqual(trader_mode_summary["trader_readout"]["mode"], "trader_one_screen_readout")
self.assertIn("No trade", trader_mode_summary["trader_readout"]["why_no_trade"])
self.assertIn("Watch", trader_mode_summary["trader_readout"]["watch_next"])
self.assertIn("Focus:", trader_mode_summary["trader_readout"]["focus_reason"])
self.assertIn("market", trader_mode_summary["trader_readout"]["data_state"])
self.assertIn("No Candidate", [item["label"] for item in trader_mode_summary["trader_readout"]["one_screen_cards"]])
self.assertIn("recovery_actions", trader_mode_summary)
self.assertIn("keyboard_shortcuts", trader_mode_summary)
self.assertIn({"key": "Space", "action": "pause_or_resume_auto_refresh"}, trader_mode_summary["keyboard_shortcuts"])
@@ -817,6 +826,48 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertTrue(trader_mode_summary["freshness_summary"]["is_stale"])
self.assertTrue(trader_mode_summary["freshness_freeze"])
self.assertEqual(trader_mode_summary["primary_question_can_watch_now"], "no")
self.assertIn("Do Not Use", trader_mode_summary["trader_readout"]["headline"])
self.assertIn("restore", trader_mode_summary["trader_readout"]["why_no_trade"].lower())
def test_trader_mode_uses_readonly_rest_fallback_without_do_not_use(self) -> None:
service = DecisionAssistantSummaryService(
launch_readiness_service=_ReadonlyFallbackLaunchReadinessService(),
session_focus_service=_SessionFocusSummaryService(),
operator_digest_service=_OperatorDigestSummaryService(),
decision_repository=_PreviousPacketRepo(),
candidate_repository=_CandidateWatchlistRepo(),
research_council_service=_ResearchCouncilSummaryService(),
exchange_status_provider=_ExchangeStatusProvider(),
announcements_provider=_AnnouncementsProvider(),
external_chart_provider=_ExternalChartProvider(),
decision_assistant_repository=_DecisionAssistantRepo(),
)
summary = service.build_summary(
instrument_id=1,
timeframe="1m",
decision_packet={
"status": "actionable",
"action_recommendation": "confirm_ticket",
"candidate_id": 18,
"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=[],
)
trader_mode_summary = summary["trader_mode_summary"]
self.assertEqual(trader_mode_summary["status_label"], "Review Now")
self.assertEqual(trader_mode_summary["data_confidence"], "fresh")
self.assertFalse(trader_mode_summary["freshness_summary"]["is_stale"])
self.assertEqual(trader_mode_summary["freshness_summary"]["stream_warning"], "market_stream_connecting")
self.assertIn("stream connecting", trader_mode_summary["trader_readout"]["data_state"])
self.assertIn("No trade", trader_mode_summary["trader_readout"]["why_no_trade"])
class _NoteRepo:
@@ -1035,6 +1086,25 @@ class _StaleLaunchReadinessService(_LaunchReadinessService):
return payload
class _ReadonlyFallbackLaunchReadinessService(_LaunchReadinessService):
def build_summary(self, *, instrument_id: int, timeframe: str = "1m"):
payload = super().build_summary(instrument_id=instrument_id, timeframe=timeframe)
payload["decision_support_ready"] = False
payload["blocking_reasons"] = ["market_truth_ready", "broker_state_ready"]
payload["readonly_live_health_summary"]["latest_candidate_timestamp"] = "2026-01-01T00:00:00+00:00"
payload["readonly_live_health_summary"]["market_stream"] = "market_stream_connecting"
payload["readonly_live_health_summary"]["runtime_health"] = "blocked"
payload["readonly_live_health_summary"]["current_blocker"] = ""
payload["readonly_live_health_summary"]["broker_account_context"] = "unavailable"
payload["live_analysis_assistant_summary"]["ready_for_readonly_live_analysis"] = True
payload["live_analysis_assistant_summary"]["blockers"] = []
payload["live_analysis_assistant_summary"]["warnings"] = [
"market_stream_not_connected_rest_fallback",
"account_context_unavailable",
]
return payload
class _SessionFocusSummaryService:
def build_summary(self, *, instrument_id: int, limit: int = 6):
return {