"""D-2 gateway 稳定性测试:自动重连 + 探活(mock xtquant,Mac 无 xtquant)。 注入 mock xtquant 模块到 sys.modules,验证: - connect 成功/失败 - query 失败 → reconnect → 重试一次(query_account / query_positions / place_order) - place_order broker 拒单(order_id<=0) 不触发重连重试 - is_alive(query_stock_asset 非空=True,None/异常/未连接=False) - reconnect 显式调用 stop 旧 trader """ import sys import types from unittest.mock import MagicMock import pytest from sanguo_qmt_bridge.xt_gateway import XtGateway # ===== mock xtquant 注入/清理工具 ===== def _install_mock_xtquant(*, connect_ret=0, asset=None, positions=None, order_id=1): """注入 mock xtquant 到 sys.modules,返回 (mock_xt, patches)。""" mock_xt = MagicMock() mock_xt.connect.return_value = connect_ret mock_xt.query_stock_asset.return_value = asset mock_xt.query_stock_positions.return_value = positions mock_xt.order_stock.return_value = order_id xttrader_mod = types.ModuleType("xtquant.xttrader") xttrader_mod.XtQuantTrader = MagicMock(return_value=mock_xt) xttype_mod = types.ModuleType("xtquant.xttype") xttype_mod.StockAccount = MagicMock() xtconstant_mod = types.ModuleType("xtquant.xtconstant") xtconstant_mod.STOCK_BUY = 23 xtconstant_mod.STOCK_SELL = 24 xtconstant_mod.FIX_PRICE = 11 xtconstant_mod.LATEST_PRICE = 5 xtquant_mod = types.ModuleType("xtquant") patches: dict[str, object] = {} for name, mod in [ ("xtquant", xtquant_mod), ("xtquant.xttrader", xttrader_mod), ("xtquant.xttype", xttype_mod), ("xtquant.xtconstant", xtconstant_mod), ]: patches[name] = sys.modules.get(name) sys.modules[name] = mod return mock_xt, patches def _remove_mock_xtquant(patches): """恢复 sys.modules 到注入前状态。""" for name, mod in patches.items(): if mod is None: sys.modules.pop(name, None) else: sys.modules[name] = mod @pytest.fixture def mock_xtquant(): """注入 mock xtquant,yield mock_xt 供测试调整返回值,结束后清理。""" mock_asset = MagicMock( cash=100000.0, frozen_cash=0.0, market_value=50000.0, total_asset=150000.0, ) mock_xt, patches = _install_mock_xtquant( connect_ret=0, asset=mock_asset, positions=[], order_id=12345, ) yield mock_xt _remove_mock_xtquant(patches) def _make_mock_position(code="600000.SH", volume=100, can_use=100, avg_price=10.5): """构造 mock 持仓对象(属性与 xtquant XtPosition 一致)。""" return MagicMock( stock_code=code, volume=volume, can_use_volume=can_use, avg_price=avg_price, ) # ===== connect ===== class TestConnect: def test_connect_success(self, mock_xtquant): gw = XtGateway() assert gw.connect() is True assert gw.connected is True def test_connect_failure_returns_false(self, mock_xtquant): mock_xtquant.connect.return_value = -1 gw = XtGateway() assert gw.connect() is False assert gw.connected is False # ===== reconnect ===== class TestReconnect: def test_reconnect_stops_old_trader(self, mock_xtquant): gw = XtGateway() gw.connect() assert mock_xtquant.stop.call_count == 0 gw.reconnect() assert mock_xtquant.stop.call_count == 1 assert gw.connected is True def test_reconnect_returns_true_on_success(self, mock_xtquant): gw = XtGateway() gw.connect() assert gw.reconnect() is True # ===== query 重连重试 ===== class TestQueryRetry: def test_query_account_reconnects_then_succeeds(self, mock_xtquant): """首次 query_stock_asset 返回 None → 重连 → 重试成功。""" mock_asset = MagicMock( cash=80000.0, frozen_cash=0.0, market_value=40000.0, total_asset=120000.0, ) mock_xtquant.query_stock_asset.side_effect = [None, mock_asset] gw = XtGateway() gw.connect() data = gw.query_account() assert data["cash"] == 80000.0 assert data["total"] == 120000.0 assert gw._last_query_ok is True assert mock_xtquant.stop.call_count == 1 # 确实触发了 reconnect def test_query_account_fails_after_retry(self, mock_xtquant): """重连后仍返回 None → RuntimeError 透出。""" mock_xtquant.query_stock_asset.return_value = None gw = XtGateway() gw.connect() with pytest.raises(RuntimeError, match="query_stock_asset 返回空"): gw.query_account() assert gw._last_query_ok is False def test_query_positions_reconnects_then_succeeds(self, mock_xtquant): """首次 query_stock_positions 返回 None → 重连 → 重试返回持仓列表。""" pos = _make_mock_position() mock_xtquant.query_stock_positions.side_effect = [None, [pos]] gw = XtGateway() gw.connect() positions = gw.query_positions() assert len(positions) == 1 assert positions[0]["code"] == "sh600000" assert positions[0]["volume"] == 100 assert mock_xtquant.stop.call_count == 1 def test_query_positions_empty_list_is_not_disconnection(self, mock_xtquant): """空列表 [](真没持仓)不触发重连。""" mock_xtquant.query_stock_positions.return_value = [] gw = XtGateway() gw.connect() positions = gw.query_positions() assert positions == [] assert mock_xtquant.stop.call_count == 0 # 没重连 assert gw._last_query_ok is True def test_place_order_reconnects_on_exception(self, mock_xtquant): """order_stock 首次抛异常 → 重连 → 重试成功。""" mock_xtquant.order_stock.side_effect = [Exception("连接断开"), 99999] gw = XtGateway() gw.connect() order_id = gw.place_order("sh600000", "buy", 10.5, 100) assert order_id == 99999 assert mock_xtquant.stop.call_count == 1 assert gw._last_query_ok is True def test_place_order_no_retry_on_broker_reject(self, mock_xtquant): """order_id<=0(broker 拒单)不触发重连,直接返回。""" mock_xtquant.order_stock.return_value = -1 gw = XtGateway() gw.connect() order_id = gw.place_order("sh600000", "buy", 10.5, 100) assert order_id == -1 assert mock_xtquant.stop.call_count == 0 # 没重连 # ===== is_alive ===== class TestIsAlive: def test_is_alive_true_when_asset_returned(self, mock_xtquant): gw = XtGateway() gw.connect() assert gw.is_alive() is True assert gw._last_query_ok is True def test_is_alive_false_when_none(self, mock_xtquant): mock_xtquant.query_stock_asset.return_value = None gw = XtGateway() gw.connect() assert gw.is_alive() is False assert gw._last_query_ok is False def test_is_alive_false_when_not_connected(self, mock_xtquant): gw = XtGateway() # 未 connect assert gw.is_alive() is False def test_is_alive_false_on_exception(self, mock_xtquant): mock_xtquant.query_stock_asset.side_effect = RuntimeError("断线") gw = XtGateway() gw.connect() assert gw.is_alive() is False # 不崩溃 assert gw._last_query_ok is False def test_is_alive_does_not_trigger_reconnect(self, mock_xtquant): """is_alive 保持轻量,探活失败不自动重连。""" mock_xtquant.query_stock_asset.return_value = None gw = XtGateway() gw.connect() gw.is_alive() assert mock_xtquant.stop.call_count == 0 # 不重连