Improve Trader Mode usefulness readout
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user