From 77d64ee79ea6d178ff24e8195180ac1a4dd61ba1 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 8 May 2026 21:01:09 +0800 Subject: [PATCH] Split launch readiness levels --- .../readiness/launch_readiness_service.py | 25 ++++++++--- src/services/trust/model_trust_service.py | 40 ++++++++++++++++- src/web/app.js | 3 ++ tests/unit/test_api_server.py | 6 ++- tests/unit/test_launch_readiness_service.py | 43 +++++++++++++++++-- tests/unit/test_model_trust_service.py | 6 +++ tests/unit/test_runtime_script_json.py | 4 +- 7 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/services/readiness/launch_readiness_service.py b/src/services/readiness/launch_readiness_service.py index 4984b3e..fb08c3a 100644 --- a/src/services/readiness/launch_readiness_service.py +++ b/src/services/readiness/launch_readiness_service.py @@ -85,14 +85,14 @@ class LaunchReadinessService: ), LaunchReadinessCheck( code="trust_model_available", - passed=bool(trust_score.get("whitelist") or trust_score.get("observe_list")), - severity="blocker", - detail=f"whitelist={trust_score.get('whitelist') or []}, observe={trust_score.get('observe_list') or []}, blacklist={trust_score.get('blacklist') or []}", + passed=bool(trust_score.get("decision_support_list")), + severity="warning", + detail=f"analysis_only={trust_score.get('analysis_only_list') or []}, decision_support={trust_score.get('decision_support_list') or []}, blacklist={trust_score.get('blacklist') or []}", ), LaunchReadinessCheck( code="real_trade_sample_verified", passed=(weekly_review.get("real_trade_count") or 0) > 0 if settings.launch_require_real_trade_sample else True, - severity="blocker", + severity="warning", detail=f"real_trade_count={weekly_review.get('real_trade_count') or 0}", ), LaunchReadinessCheck( @@ -111,12 +111,22 @@ class LaunchReadinessService: blockers = [item.code for item in checklist if not item.passed and item.severity == "blocker"] warnings = [item.code for item in checklist if not item.passed and item.severity == "warning"] + analysis_only_ready = not blockers + decision_support_ready = analysis_only_ready and bool(trust_score.get("decision_support_list")) + execution_assist_ready = analysis_only_ready and bool(trust_score.get("execution_candidate_list")) + real_trade_loop_ready = decision_support_ready and ( + (weekly_review.get("real_trade_count") or 0) > 0 if settings.launch_require_real_trade_sample else True + ) next_actions = _build_next_actions(blockers=blockers, warnings=warnings) return { "generated_ts": now.isoformat(), "instrument_id": instrument_id, "timeframe": timeframe, - "assisted_trading_ready": not blockers, + "analysis_only_ready": analysis_only_ready, + "decision_support_ready": decision_support_ready, + "execution_assist_ready": execution_assist_ready, + "real_trade_loop_ready": real_trade_loop_ready, + "assisted_trading_ready": analysis_only_ready, "real_live_ready": bool(live_readiness.get("ready_for_real_live")), "blocking_reasons": blockers, "warning_reasons": warnings, @@ -142,9 +152,10 @@ def _build_next_actions(*, blockers: list[str], warnings: list[str]) -> list[str actions = [] if "news_provider_usable" in blockers: actions.append("stabilize_news_event_source") - if "real_trade_sample_verified" in blockers: + if "real_trade_sample_verified" in warnings: actions.append("wait_for_and_verify_real_fill_sample") - if "trust_model_available" in blockers: + if "trust_model_available" in warnings: + actions.append("promote_blacklist_model_to_observe_only_readiness") actions.append("improve_model_quality_or_collect_more_samples") if "real_live_path_ready" in warnings: actions.append("decide_live_submit_strategy") diff --git a/src/services/trust/model_trust_service.py b/src/services/trust/model_trust_service.py index 2de0172..513c217 100644 --- a/src/services/trust/model_trust_service.py +++ b/src/services/trust/model_trust_service.py @@ -22,6 +22,10 @@ class TrustBucket: expectancy_r: float quality_gate_passed: bool recommendation: str + analysis_only_allowed: bool + decision_support_allowed: bool + execution_allowed: bool + operator_mode: str session_recommendations: list[dict] reasons: list[str] @@ -59,6 +63,9 @@ class ModelTrustService: whitelist = [bucket["model_code"] for bucket in ranked if bucket["classification"] == "whitelist"] observe_list = [bucket["model_code"] for bucket in ranked if bucket["classification"] == "observe"] blacklist = [bucket["model_code"] for bucket in ranked if bucket["classification"] == "blacklist"] + analysis_only_list = [bucket["model_code"] for bucket in ranked if bucket.get("analysis_only_allowed")] + decision_support_list = [bucket["model_code"] for bucket in ranked if bucket.get("decision_support_allowed")] + execution_candidate_list = [bucket["model_code"] for bucket in ranked if bucket.get("execution_allowed")] next_actions = recommend_trust_actions(ranked) return { "instrument_id": instrument_id, @@ -68,6 +75,9 @@ class ModelTrustService: "whitelist": whitelist, "observe_list": observe_list, "blacklist": blacklist, + "analysis_only_list": analysis_only_list, + "decision_support_list": decision_support_list, + "execution_candidate_list": execution_candidate_list, "next_actions": next_actions, } @@ -111,6 +121,7 @@ def build_model_buckets(*, backtest_runs: list[dict], journal_entries: list[dict real_trade_count=real_trade_count, ) classification = classify_model(score=score, quality_gate_passed=bool(quality_gate.get("passed")), sample_size=sample_size) + allowances = build_trust_allowances(classification) result[model_code] = TrustBucket( model_code=model_code, classification=classification, @@ -123,6 +134,10 @@ def build_model_buckets(*, backtest_runs: list[dict], journal_entries: list[dict expectancy_r=expectancy_r, quality_gate_passed=bool(quality_gate.get("passed")), recommendation=recommend_model_action(classification), + analysis_only_allowed=allowances["analysis_only_allowed"], + decision_support_allowed=allowances["decision_support_allowed"], + execution_allowed=allowances["execution_allowed"], + operator_mode=allowances["operator_mode"], session_recommendations=build_session_recommendations(session_performance), reasons=reasons, ) @@ -179,10 +194,33 @@ def recommend_model_action(classification: str) -> str: if classification == "whitelist": return "trade_inside_framework" if classification == "blacklist": - return "avoid_for_now" + return "observe_only" return "observe_only" +def build_trust_allowances(classification: str) -> dict: + if classification == "whitelist": + return { + "analysis_only_allowed": True, + "decision_support_allowed": True, + "execution_allowed": True, + "operator_mode": "trade_inside_framework", + } + if classification == "observe": + return { + "analysis_only_allowed": True, + "decision_support_allowed": True, + "execution_allowed": False, + "operator_mode": "decision_support_only", + } + return { + "analysis_only_allowed": True, + "decision_support_allowed": False, + "execution_allowed": False, + "operator_mode": "observe_only", + } + + def build_session_recommendations(session_performance: dict) -> list[dict]: results = [] for session_code, stats in session_performance.items(): diff --git a/src/web/app.js b/src/web/app.js index 85f63f6..dff3ade 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -447,6 +447,9 @@ function renderLaunchReadiness(payload) { clearNode(launchReadinessPanel); launchReadinessPanel.appendChild( buildInfoGrid([ + ['analysis only ready', String(Boolean(payload?.analysis_only_ready))], + ['decision support ready', String(Boolean(payload?.decision_support_ready))], + ['real trade loop ready', String(Boolean(payload?.real_trade_loop_ready))], ['assisted trading ready', String(Boolean(payload?.assisted_trading_ready))], ['real live ready', String(Boolean(payload?.real_live_ready))], ['blocking reasons', formatList(payload?.blocking_reasons || [])], diff --git a/tests/unit/test_api_server.py b/tests/unit/test_api_server.py index 18fd0e2..9d436e2 100644 --- a/tests/unit/test_api_server.py +++ b/tests/unit/test_api_server.py @@ -138,11 +138,13 @@ class ApiServerRoutingTests(unittest.TestCase): status, payload = route_request( "/trading/launch-readiness", {"instrument_id": ["1"], "timeframe": ["5m"]}, - dependencies={"trading_launch_readiness": lambda **kwargs: {"assisted_trading_ready": False, "timeframe": kwargs["timeframe"]}}, + dependencies={"trading_launch_readiness": lambda **kwargs: {"analysis_only_ready": True, "decision_support_ready": False, "real_trade_loop_ready": False, "assisted_trading_ready": True, "timeframe": kwargs["timeframe"]}}, ) self.assertEqual(status, 200) - self.assertFalse(payload["assisted_trading_ready"]) + self.assertTrue(payload["assisted_trading_ready"]) + self.assertTrue(payload["analysis_only_ready"]) + self.assertFalse(payload["decision_support_ready"]) self.assertEqual(payload["timeframe"], "5m") def test_routes_trading_broker_state(self) -> None: diff --git a/tests/unit/test_launch_readiness_service.py b/tests/unit/test_launch_readiness_service.py index a61a005..a30d075 100644 --- a/tests/unit/test_launch_readiness_service.py +++ b/tests/unit/test_launch_readiness_service.py @@ -20,10 +20,28 @@ class LaunchReadinessServiceTests(unittest.TestCase): with patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()): payload = service.build_summary(instrument_id=1, timeframe="1m") - self.assertFalse(payload["assisted_trading_ready"]) - self.assertIn("real_trade_sample_verified", payload["blocking_reasons"]) + self.assertTrue(payload["analysis_only_ready"]) + self.assertTrue(payload["assisted_trading_ready"]) + self.assertFalse(payload["real_trade_loop_ready"]) + self.assertIn("real_trade_sample_verified", payload["warning_reasons"]) self.assertFalse(payload["real_live_ready"]) + def test_build_summary_keeps_decision_support_false_when_only_blacklist_models_exist(self) -> None: + service = LaunchReadinessService( + market_state_service=_MarketStateService(), + broker_read_model_service=_BrokerReadService(), + weekly_review_service=_WeeklyReviewService(), + trust_service=_BlacklistOnlyTrustService(), + live_readiness_service=_LiveReadinessService(), + ) + + with patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()): + payload = service.build_summary(instrument_id=1, timeframe="1m") + + self.assertTrue(payload["analysis_only_ready"]) + self.assertFalse(payload["decision_support_ready"]) + self.assertIn("trust_model_available", payload["warning_reasons"]) + class _MarketState: freshness = type("Freshness", (), {"code": "market_data_fresh"})() @@ -63,7 +81,26 @@ class _WeeklyReviewService: class _TrustService: def build_summary(self, **kwargs): - return {"whitelist": ["OSOK"], "observe_list": [], "blacklist": []} + return { + "whitelist": ["OSOK"], + "observe_list": [], + "blacklist": [], + "analysis_only_list": ["OSOK"], + "decision_support_list": ["OSOK"], + "execution_candidate_list": ["OSOK"], + } + + +class _BlacklistOnlyTrustService: + def build_summary(self, **kwargs): + return { + "whitelist": [], + "observe_list": [], + "blacklist": ["SWEEP_MSS_FVG"], + "analysis_only_list": ["SWEEP_MSS_FVG"], + "decision_support_list": [], + "execution_candidate_list": [], + } class _LiveReadinessService: diff --git a/tests/unit/test_model_trust_service.py b/tests/unit/test_model_trust_service.py index 87f6632..c1fb5ab 100644 --- a/tests/unit/test_model_trust_service.py +++ b/tests/unit/test_model_trust_service.py @@ -47,6 +47,9 @@ class ModelTrustServiceTests(unittest.TestCase): self.assertIn("OSOK", payload["whitelist"]) self.assertIn("REAL_TRADE_IMPORT", payload["blacklist"]) + self.assertIn("REAL_TRADE_IMPORT", payload["analysis_only_list"]) + self.assertIn("OSOK", payload["decision_support_list"]) + self.assertIn("OSOK", payload["execution_candidate_list"]) self.assertTrue(payload["models"][0]["score"] >= payload["models"][1]["score"]) def test_build_model_buckets_creates_observe_bucket_when_samples_are_small(self) -> None: @@ -57,6 +60,9 @@ class ModelTrustServiceTests(unittest.TestCase): self.assertEqual(buckets["OSOK"].classification, "observe") self.assertIn("sample_size_small", buckets["OSOK"].reasons) + self.assertTrue(buckets["OSOK"].analysis_only_allowed) + self.assertTrue(buckets["OSOK"].decision_support_allowed) + self.assertFalse(buckets["OSOK"].execution_allowed) class _BacktestRepo: diff --git a/tests/unit/test_runtime_script_json.py b/tests/unit/test_runtime_script_json.py index fb98c8a..db30059 100644 --- a/tests/unit/test_runtime_script_json.py +++ b/tests/unit/test_runtime_script_json.py @@ -186,7 +186,7 @@ class RuntimeScriptJsonTests(unittest.TestCase): self.assertEqual(payload["timeframe"], "5m") def test_run_launch_readiness_outputs_json(self) -> None: - service = type("Service", (), {"build_summary": lambda self, instrument_id, timeframe: {"instrument_id": instrument_id, "timeframe": timeframe, "assisted_trading_ready": False, "real_live_ready": False, "blocking_reasons": ["real_trade_sample_verified"]}})() + service = type("Service", (), {"build_summary": lambda self, instrument_id, timeframe: {"instrument_id": instrument_id, "timeframe": timeframe, "analysis_only_ready": True, "decision_support_ready": False, "real_trade_loop_ready": False, "assisted_trading_ready": True, "real_live_ready": False, "blocking_reasons": [], "warning_reasons": ["real_trade_sample_verified"]}})() output = io.StringIO() with patch("scripts.run_launch_readiness.LaunchReadinessService", return_value=service), contextlib.redirect_stdout(output): exit_code = run_launch_readiness.main( @@ -194,7 +194,7 @@ class RuntimeScriptJsonTests(unittest.TestCase): ) payload = json.loads(output.getvalue()) - self.assertEqual(exit_code, 2) + self.assertEqual(exit_code, 0) self.assertEqual(payload["instrument_id"], 2) self.assertEqual(payload["timeframe"], "5m")