Files
ai-exchange/tests/unit/test_trading_runtime_service.py
T
2026-05-08 14:39:44 +08:00

185 lines
6.9 KiB
Python

from datetime import datetime, timezone
import contextlib
import io
import json
import os
import unittest
from unittest.mock import patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from scripts import run_trading_runtime
from src.api.server import route_request
from src.services.runtime import TradingRuntimeScheduler, TradingRuntimeSupervisor
class FakeInstrumentRepository:
def fetch_instrument_by_symbol(self, symbol):
return {"id": 1 if symbol == "BTC-USDT" else 2, "symbol": symbol}
class FakeTradingPlanService:
def __init__(self):
self.repository = FakeInstrumentRepository()
self.build_calls = 0
def build_plan(self, **kwargs):
self.build_calls += 1
return type("Plan", (), {"status": "active"})()
def persist_plan(self, candidate):
return 101
class FakeMarketState:
def __init__(self, *, freshness="market_data_fresh", stream="market_stream_connected", gap="market_data_gap_clear"):
self.freshness = freshness
self.stream = stream
self.gap = gap
def to_dict(self):
return {
"freshness": {"code": self.freshness},
"stream_state": {"code": self.stream},
"data_gap_state": {"code": self.gap},
}
class FakeMarketStateService:
def __init__(self, market_state=None):
self.market_state = market_state or FakeMarketState()
def build_market_state(self, **kwargs):
return self.market_state
def persist_market_state_snapshot(self, **kwargs):
return 301
class FakeBrokerSnapshot:
status = "broker_state_ok"
error = None
def to_dict(self):
return {
"status": self.status,
"error": self.error,
"account": {"equity": 1000.0},
"positions": [],
"orders": [],
"fills": [],
"alerts": [],
}
class FakeBrokerReadModelService:
def fetch_snapshot(self, **kwargs):
return FakeBrokerSnapshot()
class FakeSetupWatcherService:
def build_candidate(self, **kwargs):
return type("Candidate", (), {"status": "blocked"})()
def persist_candidate(self, candidate):
return 401 if candidate is not None else None
class FakeDecisionPacketService:
def build_persist_and_alert(self, **kwargs):
return {
"id": 501,
"status": "actionable",
"action_recommendation": "confirm_ticket",
"alerts": [{"alert_type": "candidate_actionable"}],
}
class TradingRuntimeSchedulerTests(unittest.TestCase):
def test_run_cycle_records_successful_task_state(self) -> None:
scheduler = TradingRuntimeScheduler(
market_state_service=FakeMarketStateService(),
trading_plan_service=FakeTradingPlanService(),
setup_watcher_service=FakeSetupWatcherService(),
broker_read_model_service=FakeBrokerReadModelService(),
decision_packet_service=FakeDecisionPacketService(),
)
with patch("src.services.runtime.trading_runtime_service.settings.trading_runtime_symbols", "BTC-USDT"):
payload = scheduler.run_cycle()
self.assertEqual(payload["scheduler_state"], "running")
self.assertEqual(payload["current_blockers"], [])
self.assertEqual(payload["tasks"]["plan"]["last_status"], "ok")
self.assertEqual(payload["tasks"]["broker_sync"]["details"]["status"], "broker_state_ok")
self.assertEqual(payload["tasks"]["decision_packet"]["details"]["packet_id"], 501)
self.assertEqual(payload["latest_broker_state_by_symbol"]["BTC-USDT"]["account"]["equity"], 1000.0)
self.assertTrue(payload["tasks"]["watch"]["next_run_ts"])
def test_run_cycle_enters_degraded_mode_when_market_truth_is_bad(self) -> None:
scheduler = TradingRuntimeScheduler(
market_state_service=FakeMarketStateService(FakeMarketState(stream="market_stream_disconnected")),
trading_plan_service=FakeTradingPlanService(),
setup_watcher_service=FakeSetupWatcherService(),
broker_read_model_service=FakeBrokerReadModelService(),
decision_packet_service=FakeDecisionPacketService(),
)
with patch("src.services.runtime.trading_runtime_service.settings.trading_runtime_symbols", "BTC-USDT"):
payload = scheduler.run_cycle()
self.assertTrue(payload["degraded_mode_state"]["is_degraded"])
self.assertIn("market_stream_degraded:BTC-USDT:market_stream_disconnected", payload["current_blockers"])
self.assertIn("watch_skipped_market_degraded:BTC-USDT", payload["current_blockers"])
self.assertGreaterEqual(len(payload["incidents"]), 1)
def test_supervisor_run_once_exposes_supervisor_state(self) -> None:
scheduler = TradingRuntimeScheduler(
market_state_service=FakeMarketStateService(),
trading_plan_service=FakeTradingPlanService(),
setup_watcher_service=FakeSetupWatcherService(),
broker_read_model_service=FakeBrokerReadModelService(),
decision_packet_service=FakeDecisionPacketService(),
)
supervisor = TradingRuntimeSupervisor(scheduler=scheduler)
with patch("src.services.runtime.trading_runtime_service.settings.trading_runtime_symbols", "BTC-USDT"):
payload = supervisor.run_once()
self.assertEqual(payload["supervisor_state"], "stopped")
self.assertEqual(payload["current_blockers"], [])
class TradingRuntimeApiTests(unittest.TestCase):
def test_routes_runtime_status_and_run_once(self) -> None:
status, payload = route_request(
"/trading/runtime-status",
{},
dependencies={"trading_runtime_status": lambda: {"scheduler_state": "running", "current_blockers": []}},
)
run_status, run_payload = route_request(
"/trading/runtime/run-once",
{},
method="POST",
body={},
dependencies={"trading_runtime_run_once": lambda: {"scheduler_state": "running", "tasks": {}}},
)
self.assertEqual(status, 200)
self.assertEqual(payload["scheduler_state"], "running")
self.assertEqual(run_status, 200)
self.assertIn("tasks", run_payload)
class TradingRuntimeScriptTests(unittest.TestCase):
def test_script_outputs_json(self) -> None:
service = type("Service", (), {"run_once": lambda self: {"scheduler_state": "running", "current_blockers": []}})()
output = io.StringIO()
with patch("scripts.run_trading_runtime.get_trading_runtime_service", return_value=service), contextlib.redirect_stdout(output):
exit_code = run_trading_runtime.main(env={"TRADING_RUNTIME_OUTPUT_JSON": "1"})
self.assertEqual(exit_code, 0)
payload = json.loads(output.getvalue())
self.assertEqual(payload["scheduler_state"], "running")
if __name__ == "__main__":
unittest.main()