Add live readiness protocol
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user