Split launch readiness levels
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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 || [])],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user