Split launch readiness levels

This commit is contained in:
Codex
2026-05-08 21:01:09 +08:00
parent 79aab90a45
commit 77d64ee79e
7 changed files with 112 additions and 15 deletions
@@ -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")
+39 -1
View File
@@ -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():
+3
View File
@@ -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 || [])],
+4 -2
View File
@@ -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:
+40 -3
View File
@@ -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:
+6
View File
@@ -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:
+2 -2
View File
@@ -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")