159 lines
5.6 KiB
Python
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()
|