Files
ai-exchange/tests/unit/test_launch_readiness_service.py
T
2026-05-20 13:25:16 +08:00

355 lines
17 KiB
Python

import os
import unittest
from unittest.mock import patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.services.readiness import LaunchReadinessService
class LaunchReadinessServiceTests(unittest.TestCase):
def test_build_summary_reports_assisted_not_ready_without_real_trade_sample(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(),
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=_RuntimeService()),
patch("src.services.readiness.launch_readiness_service.settings.launch_require_real_trade_sample", True),
):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["analysis_only_ready"])
self.assertTrue(payload["live_analysis_assistant_ready"])
self.assertEqual(payload["live_analysis_assistant_summary"]["status"], "ready_for_readonly_live_analysis")
self.assertTrue(payload["live_analysis_assistant_summary"]["must_not_trade"])
self.assertFalse(payload["live_analysis_assistant_summary"]["execution_allowed"])
self.assertEqual(payload["readonly_live_health_summary"]["broker_account_context"], "available")
self.assertTrue(payload["assisted_trading_ready"])
self.assertTrue(payload["operator_soft_rollout_ready"])
self.assertEqual(payload["operator_soft_rollout_mode"], "execution_assist")
self.assertTrue(payload["real_trade_sample_gate_required"])
self.assertEqual(payload["real_trade_sample_gate_status"], "waiting_for_real_fill_sample")
self.assertIn("wait_for_and_verify_real_fill_sample", payload["next_actions"])
self.assertFalse(payload["real_trade_loop_ready"])
self.assertIn("real_trade_sample_verified", payload["warning_reasons"])
self.assertIn("real_trade_sample_verified", payload["external_waiting_items"])
self.assertFalse(payload["real_live_ready"])
self.assertEqual(payload["decision_support_boundary_summary"]["status"], "execution_assist_available")
self.assertIn("review_market_context", payload["decision_support_boundary_summary"]["allowed_operator_actions"])
self.assertIn("submit_live_orders", payload["decision_support_boundary_summary"]["blocked_operator_actions"])
self.assertEqual(
payload["decision_support_promotion_summary"]["status"],
"ready_for_decision_support_non_execution",
)
self.assertTrue(payload["decision_support_promotion_summary"]["strict_decision_support_ready"])
self.assertEqual(
payload["decision_support_promotion_summary"]["execution_boundary_after_promotion"],
"execution_still_blocked",
)
self.assertEqual(payload["shadow_paper_validation_summary"]["status"], "collecting_paper_samples")
self.assertEqual(payload["shadow_paper_validation_summary"]["next_validation_gate"], "paper_ticket_sample_gate")
def test_build_summary_defers_real_trade_sample_gate_for_analysis_decision_phase(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(),
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=_RuntimeService()),
patch("src.services.readiness.launch_readiness_service.settings.launch_require_real_trade_sample", False),
):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["analysis_only_ready"])
self.assertFalse(payload["real_trade_sample_gate_required"])
self.assertEqual(payload["real_trade_sample_gate_status"], "deferred_analysis_decision_only")
self.assertEqual(payload["real_trade_sample_gate_phase"], "deferred_analysis_decision_only")
self.assertNotIn("real_trade_sample_verified", payload["warning_reasons"])
self.assertNotIn("real_trade_sample_verified", payload["external_waiting_items"])
self.assertNotIn("wait_for_and_verify_real_fill_sample", payload["next_actions"])
self.assertEqual(payload["decision_support_boundary_summary"]["live_execution_boundary"], "live_submit_blocked")
self.assertIn("decision_support_promotion_summary", payload)
self.assertIn("shadow_paper_validation_summary", payload)
def test_live_analysis_assistant_ready_when_account_context_unavailable(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
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=_RuntimeService()):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertFalse(payload["analysis_only_ready"])
self.assertTrue(payload["live_analysis_assistant_ready"])
self.assertEqual(payload["product_operating_mode"], "readonly_live_analysis")
self.assertEqual(payload["readonly_account_context_status"], "unavailable")
self.assertIn("broker_state_ready", payload["blocking_reasons"])
self.assertIn("account_context_unavailable", payload["live_analysis_assistant_summary"]["warnings"])
self.assertEqual(payload["live_analysis_assistant_summary"]["blockers"], [])
self.assertEqual(payload["decision_support_boundary_summary"]["status"], "readonly_live_analysis_available")
self.assertIn("review_market_context", payload["decision_support_boundary_summary"]["allowed_operator_actions"])
self.assertIn("submit_live_orders", payload["live_analysis_assistant_summary"]["blocked_actions"])
self.assertFalse(payload["decision_support_ready"])
self.assertFalse(payload["execution_assist_ready"])
self.assertFalse(payload["real_live_ready"])
def test_live_analysis_allows_rest_fresh_market_when_stream_and_news_are_degraded(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=_RuntimeServiceWithStreamBlocker()):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertFalse(payload["analysis_only_ready"])
self.assertTrue(payload["live_analysis_assistant_ready"])
self.assertEqual(payload["product_operating_mode"], "readonly_live_analysis")
self.assertIn("market_truth_ready", payload["blocking_reasons"])
self.assertIn("news_provider_usable", payload["blocking_reasons"])
self.assertIn("runtime_ready", payload["blocking_reasons"])
self.assertIn("market_stream_not_connected_rest_fallback", payload["live_analysis_assistant_summary"]["warnings"])
self.assertIn("news_provider_unavailable_context_warning", payload["live_analysis_assistant_summary"]["warnings"])
self.assertIn("runtime_stream_degraded_rest_fallback", payload["live_analysis_assistant_summary"]["warnings"])
self.assertEqual(payload["live_analysis_assistant_summary"]["blockers"], [])
self.assertFalse(payload["decision_support_ready"])
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(),
broker_read_model_service=_BrokerReadService(),
weekly_review_service=_WeeklyReviewService(),
trust_service=_BlacklistOnlyTrustService(),
live_readiness_service=_LiveReadinessService(),
shadow_paper_validation_service=_ShadowPaperValidationService(),
)
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.assertTrue(payload["restricted_decision_support_ready"])
self.assertTrue(payload["operator_soft_rollout_ready"])
self.assertEqual(payload["operator_soft_rollout_mode"], "restricted_decision_support")
self.assertIn("start_restricted_decision_support_soft_rollout", payload["next_actions"])
self.assertIn("execution_assist_remains_disabled", payload["operator_soft_rollout_boundary_notes"])
self.assertNotIn("trust_model_available", payload["warning_reasons"])
self.assertEqual(payload["decision_support_boundary_summary"]["status"], "restricted_assisted_analysis_available")
self.assertEqual(payload["decision_support_boundary_summary"]["current_model_boundary"], "observe_only_current_model")
self.assertEqual(
payload["decision_support_boundary_summary"]["next_boundary_gate"],
"promote_model_out_of_observe_only_or_blacklist",
)
self.assertEqual(payload["decision_support_promotion_summary"]["status"], "needs_trust_promotion")
self.assertIn(
"decision_support_trust_candidate",
payload["decision_support_promotion_summary"]["failed_criteria"],
)
class _MarketState:
freshness = type("Freshness", (), {"code": "market_data_fresh"})()
stream_state = type("Stream", (), {"code": "market_stream_connected"})()
data_gap_state = type("Gap", (), {"code": "market_data_gap_clear"})()
news_window = type("News", (), {"provider_name": "cached", "freshness_code": "news_provider_cached", "error": None})()
def to_dict(self):
return {
"freshness": {"code": "market_data_fresh"},
"stream_state": {"code": "market_stream_connected"},
"data_gap_state": {"code": "market_data_gap_clear"},
}
class _MarketStateService:
def build_market_state(self, **kwargs):
return _MarketState()
class _RestFreshMarketState:
freshness = type("Freshness", (), {"code": "market_data_fresh"})()
stream_state = type("Stream", (), {"code": "market_stream_connecting"})()
data_gap_state = type("Gap", (), {"code": "market_data_gap_clear"})()
news_window = type(
"News",
(),
{"provider_name": "news_provider_chain", "freshness_code": "news_provider_stale", "error": "snapshot_missing"},
)()
def to_dict(self):
return {
"freshness": {"code": "market_data_fresh"},
"stream_state": {"code": "market_stream_connecting"},
"data_gap_state": {"code": "market_data_gap_clear"},
}
class _RestFreshMarketStateService:
def build_market_state(self, **kwargs):
return _RestFreshMarketState()
class _BrokerState:
def __init__(self, status: str = "broker_state_ok") -> None:
self.status = status
def to_dict(self):
return {"status": self.status}
class _BrokerReadService:
def __init__(self, status: str = "broker_state_ok") -> None:
self.status = status
def fetch_snapshot(self, **kwargs):
return _BrokerState(self.status)
class _WeeklyReviewService:
def build_summary(self, **kwargs):
return {
"real_trade_count": 0,
"entry_count": 5,
"framework_execution_rate": 0.8,
"risk_violation_count": 0,
}
class _TrustService:
def build_summary(self, **kwargs):
return {
"models": [
{
"model_code": "OSOK",
"classification": "whitelist",
"sample_size": 6,
"quality_gate_passed": True,
"expectancy_r": 0.4,
"framework_execution_rate": 0.8,
"operator_mode": "trade_inside_framework",
"reasons": ["backtest_quality_gate_passed"],
}
],
"whitelist": ["OSOK"],
"observe_list": [],
"blacklist": [],
"analysis_only_list": ["OSOK"],
"restricted_decision_support_list": ["OSOK"],
"decision_support_list": ["OSOK"],
"execution_candidate_list": ["OSOK"],
}
class _BlacklistOnlyTrustService:
def build_summary(self, **kwargs):
return {
"models": [
{
"model_code": "SWEEP_MSS_FVG",
"classification": "blacklist",
"sample_size": 1,
"quality_gate_passed": False,
"expectancy_r": 0.0,
"framework_execution_rate": 0.2,
"operator_mode": "observe_only",
"reasons": ["backtest_quality_gate_failed"],
}
],
"whitelist": [],
"observe_list": [],
"blacklist": ["SWEEP_MSS_FVG"],
"analysis_only_list": ["SWEEP_MSS_FVG"],
"restricted_decision_support_list": ["SWEEP_MSS_FVG"],
"decision_support_list": [],
"execution_candidate_list": [],
}
class _LiveReadinessService:
def build_summary(self, **kwargs):
return {"ready_for_real_live": False, "blocking_reasons": ["adapter_real_live_not_ready"]}
class _RuntimeService:
def status(self):
return {"scheduler_state": "running", "current_blockers": []}
class _RuntimeServiceWithStreamBlocker:
def status(self):
return {
"scheduler_state": "running",
"current_blockers": ["market_stream_degraded:BTC-USDT:market_stream_connecting"],
}
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 {
"status": "collecting_paper_samples",
"next_validation_gate": "paper_ticket_sample_gate",
"execution_boundary": "real_live_still_blocked",
"candidate_count": 3,
"recent_samples": {
"candidates": [{"created_ts": "2026-05-18T01:00:00+00:00"}],
"shadow_observations": [{"observed_ts": "2026-05-18T01:05:00+00:00"}],
},
}
if __name__ == "__main__":
unittest.main()