Add live readiness protocol

This commit is contained in:
Codex
2026-05-08 16:18:12 +08:00
parent e918c11107
commit a1aeaacfc3
16 changed files with 569 additions and 50 deletions
+11
View File
@@ -123,6 +123,17 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertEqual(packet_status, 200)
self.assertEqual(packet_payload["instrument_id"], 1)
def test_routes_trading_live_readiness(self) -> None:
status, payload = route_request(
"/trading/live-readiness",
{"instrument_id": ["1"], "timeframe": ["5m"]},
dependencies={"trading_live_readiness": lambda **kwargs: {"ready_for_real_live": False, "timeframe": kwargs["timeframe"]}},
)
self.assertEqual(status, 200)
self.assertFalse(payload["ready_for_real_live"])
self.assertEqual(payload["timeframe"], "5m")
def test_routes_trading_broker_state(self) -> None:
status, payload = route_request(
"/trading/broker-state",
+43
View File
@@ -69,6 +69,7 @@ class LiveBrokerServiceTests(unittest.TestCase):
self.assertEqual(result.status, "placeholder_not_implemented")
self.assertEqual(repo.live_audits[-1]["adapter_name"], "okx")
self.assertEqual(repo.live_audits[-1]["execution_ticket_id"], 10)
self.assertIn("live_order_envelope", repo.live_audits[-1]["payload"])
def test_submit_confirmed_ticket_rejects_paper_ticket(self) -> None:
repo = _LiveRepo()
@@ -85,6 +86,22 @@ class LiveBrokerServiceTests(unittest.TestCase):
self.assertEqual(result.status, "rejected")
self.assertIn("execution_ticket_mode_not_live", result.reason_codes)
def test_submit_confirmed_ticket_rejects_missing_operator_protocol(self) -> None:
repo = _LiveRepo()
repo.tickets[10]["ticket_snapshot"] = dict(repo.tickets[10]["ticket_snapshot"])
repo.tickets[10]["ticket_snapshot"].pop("confirmation")
service = LiveBrokerService(repository=repo)
with patch.object(settings, "live_trading_enabled", True):
result = service.submit_confirmed_ticket(
10,
requested_by="alice",
explicit_live_confirmation=True,
)
self.assertEqual(result.status, "rejected")
self.assertIn("operator_confirmation_protocol_missing", result.reason_codes)
def test_unsupported_adapter_is_audited(self) -> None:
repo = _LiveRepo()
service = LiveBrokerService(repository=repo)
@@ -151,10 +168,31 @@ class LiveValidationTests(unittest.TestCase):
"execution_ticket_mode_not_live",
"execution_ticket_not_confirmed",
"execution_ticket_missing_confirmation_metadata",
"operator_confirmation_protocol_missing",
"execution_ticket_missing_execution_record",
],
)
def test_validate_live_submit_request_accepts_protocolized_ticket(self) -> None:
reasons = validate_live_submit_request(
ticket={
"status": "confirmed",
"execution_mode": "semi_auto",
"confirmed_by": "bob",
"confirmed_at": "2026-05-06T00:00:00+00:00",
"execution_id": 20,
"ticket_snapshot": {
"confirmation": {
"protocol": {"protocol_version": "live-confirmation-v1"}
}
},
},
live_trading_enabled=True,
explicit_live_confirmation=True,
)
self.assertEqual(reasons, [])
class _LiveRepo:
def __init__(self) -> None:
@@ -173,6 +211,11 @@ class _LiveRepo:
"side": "buy",
"signal_id": 1,
},
"confirmation": {
"confirmed_by": "bob",
"confirmed_at": "2026-05-06T00:00:00+00:00",
"protocol": {"protocol_version": "live-confirmation-v1"},
},
"decision": {
"entry_price": 100.5,
"size": 1.0,
+83
View File
@@ -0,0 +1,83 @@
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.broker.live_readiness_service import LiveReadinessService
class LiveReadinessServiceTests(unittest.TestCase):
def test_build_summary_reports_blocking_reasons(self) -> None:
service = LiveReadinessService(
broker_read_model_service=_BrokerReadService(),
market_state_service=_MarketStateService(),
execution_repository=_ExecutionRepo(),
decision_packet_service=_DecisionPacketService(),
trust_service=_TrustService(),
)
with patch("src.services.broker.live_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertFalse(payload["ready_for_real_live"])
self.assertIn("adapter_real_live_not_ready", payload["blocking_reasons"])
self.assertEqual(payload["operator_confirmation_protocol"]["protocol_version"], "live-confirmation-v1")
self.assertEqual(payload["live_order_schema"]["schema_version"], "live-order-v1")
class _BrokerState:
status = "broker_state_ok"
def to_dict(self):
return {"status": "broker_state_ok"}
class _BrokerReadService:
def fetch_snapshot(self, **kwargs):
return _BrokerState()
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"})()
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 _ExecutionRepo:
def fetch_recent_execution_ticket_rows(self, **kwargs):
return []
def fetch_recent_live_operation_audits(self, **kwargs):
return []
class _DecisionPacketService:
def build_preview(self, **kwargs):
return {"action_recommendation": "confirm_ticket"}
class _TrustService:
def build_summary(self, **kwargs):
return {"models": [{"model_code": "OSOK"}], "whitelist": ["OSOK"], "blacklist": []}
class _RuntimeService:
def status(self):
return {"scheduler_state": "running", "current_blockers": []}
if __name__ == "__main__":
unittest.main()
+14
View File
@@ -25,6 +25,7 @@ from scripts import (
run_decision_packet,
run_review_packet,
run_session_generator,
run_live_readiness,
run_trading_runtime,
run_sweeps,
run_swings,
@@ -170,6 +171,19 @@ class RuntimeScriptJsonTests(unittest.TestCase):
self.assertEqual(exit_code, 0)
self.assertEqual(payload["instrument_id"], 2)
def test_run_live_readiness_outputs_json(self) -> None:
service = type("Service", (), {"build_live_readiness": lambda self, instrument_id, timeframe: {"instrument_id": instrument_id, "timeframe": timeframe, "ready_for_real_live": False, "blocking_reasons": ["adapter_real_live_not_ready"]}})()
output = io.StringIO()
with patch("scripts.run_live_readiness.LiveBrokerService", return_value=service), contextlib.redirect_stdout(output):
exit_code = run_live_readiness.main(
env={"LIVE_READINESS_OUTPUT_JSON": "1", "LIVE_READINESS_INSTRUMENT_ID": "2", "LIVE_READINESS_TIMEFRAME": "5m"}
)
payload = json.loads(output.getvalue())
self.assertEqual(exit_code, 2)
self.assertEqual(payload["instrument_id"], 2)
self.assertEqual(payload["timeframe"], "5m")
if __name__ == "__main__":
unittest.main()