Files
ai-exchange/tests/unit/test_live_adapter_service.py
T
2026-05-06 16:32:48 +08:00

159 lines
5.6 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.config.settings import settings
from src.services.broker import LiveBrokerService, validate_live_submit_request
class LiveBrokerServiceTests(unittest.TestCase):
def test_submit_confirmed_ticket_rejects_when_live_disabled(self) -> None:
repo = _LiveRepo()
service = LiveBrokerService(repository=repo)
with patch.object(settings, "live_trading_enabled", False):
result = service.submit_confirmed_ticket(
10,
requested_by="alice",
explicit_live_confirmation=True,
)
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result.status, "rejected")
self.assertIn("live_trading_disabled", result.reason_codes)
self.assertEqual(repo.live_audits[0]["operation_type"], "submit_bracket_order")
def test_submit_confirmed_ticket_requires_explicit_confirmation(self) -> None:
repo = _LiveRepo()
service = LiveBrokerService(repository=repo)
with patch.object(settings, "live_trading_enabled", True):
result = service.submit_confirmed_ticket(
10,
requested_by="alice",
explicit_live_confirmation=False,
)
self.assertEqual(result.status, "rejected")
self.assertIn("explicit_live_confirmation_required", result.reason_codes)
def test_submit_confirmed_ticket_calls_adapter_when_enabled_and_confirmed(self) -> None:
repo = _LiveRepo()
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, "placeholder_not_implemented")
self.assertEqual(repo.live_audits[-1]["adapter_name"], "okx")
self.assertEqual(repo.live_audits[-1]["execution_ticket_id"], 10)
def test_reconcile_is_audited(self) -> None:
repo = _LiveRepo()
service = LiveBrokerService(repository=repo)
with patch.object(settings, "live_trading_enabled", True):
result = service.reconcile_orders(requested_by="ops", instrument_id=1)
self.assertEqual(result.status, "placeholder_reconciliation")
self.assertEqual(repo.live_audits[-1]["operation_type"], "reconcile_orders")
def test_cancel_all_requires_explicit_confirmation(self) -> None:
repo = _LiveRepo()
service = LiveBrokerService(repository=repo)
with patch.object(settings, "live_trading_enabled", True):
result = service.cancel_all(requested_by="ops", explicit_live_confirmation=False)
self.assertEqual(result.status, "rejected")
self.assertIn("explicit_live_confirmation_required", result.reason_codes)
def test_kill_switch_is_audited(self) -> None:
repo = _LiveRepo()
service = LiveBrokerService(repository=repo)
with patch.object(settings, "live_trading_enabled", True):
result = service.activate_kill_switch(
requested_by="ops",
reason="manual_test",
explicit_live_confirmation=True,
)
self.assertEqual(result.status, "placeholder_kill_switch_active")
self.assertEqual(repo.live_audits[-1]["operation_type"], "activate_kill_switch")
class LiveValidationTests(unittest.TestCase):
def test_validate_live_submit_request_requires_confirmed_ticket(self) -> None:
reasons = validate_live_submit_request(
ticket={"status": "awaiting_confirmation", "confirmed_by": None, "confirmed_at": None},
live_trading_enabled=False,
explicit_live_confirmation=False,
)
self.assertEqual(
reasons,
[
"live_trading_disabled",
"explicit_live_confirmation_required",
"execution_ticket_not_confirmed",
"execution_ticket_missing_confirmation_metadata",
],
)
class _LiveRepo:
def __init__(self) -> None:
self.live_audits = []
self.tickets = {
10: {
"id": 10,
"signal_id": 1,
"execution_mode": "semi_auto",
"status": "confirmed",
"confirmed_by": "bob",
"confirmed_at": "2026-05-06T00:00:00+00:00",
"execution_id": 20,
"ticket_snapshot": {
"signal": {
"side": "buy",
"signal_id": 1,
},
"decision": {
"entry_price": 100.5,
"size": 1.0,
"meta": {
"stop_loss": 99.0,
"tp1": 102.0,
"tp2": 104.0,
},
},
},
}
}
def fetch_execution_ticket_by_id(self, ticket_id: int):
return dict(self.tickets.get(ticket_id)) if ticket_id in self.tickets else None
def fetch_signal_for_execution(self, signal_id: int):
return {
"id": signal_id,
"instrument_id": 1,
"side": "buy",
}
def insert_live_operation_audit(self, **kwargs):
self.live_audits.append(kwargs)
return len(self.live_audits)
if __name__ == "__main__":
unittest.main()