Files
sanguo_vnpy_v2/tests/trader/test_d4a_integration.py
T
claude_dev 2393097074 test(live): D-4a集成测试(mock bridge真HTTP, sanguo半边4例绿)
- _MockBridge HTTP server模拟bridge(health/order/account/positions+鉴权)
- bridge_client真发HTTP(urlopen+X-Bridge-Token)+响应解析+错token 401返回None
- _shadow_trades_to_bridge真HTTP链路:注入成交→真POST→mock收到下单+symbol转sh600000→paper_shadow_orders记录
- NAS容器4 passed
- D-4a sanguo半边端到端验证(真HTTP); miniQMT真报单另半边待Windows bridge实跑
2026-07-11 05:41:43 +08:00

153 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D-4a 集成测试:sanguo → bridge 真实 HTTP round-tripmock bridge server)。
不依赖 Windows——本地起 mock HTTP server 模拟 bridgehealth/order/account/positions + 鉴权),
验证 BridgeClient 真发 HTTPurlopen + X-Bridge-Token + JSON 解析)+ _shadow_trades_to_bridge
完整链路在真实 HTTP 环境跑通。
验证范围(D-4a sanguo 半边):
- bridge_client HTTP 层真发请求、带鉴权 header、解析响应
- _shadow_trades_to_bridge → bridge_client → 真 HTTP → mock bridge 收到下单 → paper_shadow_orders
未覆盖(D-4a 另一半,物理依赖 Windows):
- bridge → xtquant → miniQMT 真报单(miniQMT 只 Windows,需 Windows bridge 实跑)
"""
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from types import SimpleNamespace
from urllib.parse import urlparse
from sanguo_trader.bridge_client import BridgeClient
from sanguo_trader.live_orchestrator import _shadow_trades_to_bridge
from sanguo_trader.persistence import init_db, is_trade_shadowed, save_trade
_TOKEN = "integration-test-token"
class _MockBridgeHandler(BaseHTTPRequestHandler):
"""模拟 Windows bridge 的 4 接口 + token 鉴权(契约同 sanguo_qmt_bridge/bridge.py)。"""
def log_message(self, *args): # 静默测试日志
pass
def _has_token(self) -> bool:
return self.headers.get("X-Bridge-Token") == _TOKEN
def _respond(self, code: int, payload: dict) -> None:
body = json.dumps(payload).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = urlparse(self.path).path
if path == "/health":
self._respond(200, {"status": "ok", "miniqmt_connected": True})
elif path in ("/account", "/positions"):
if not self._has_token():
self._respond(401, {"detail": "token 无效"})
return
self._respond(200, {"ok": True, "cash": 1e7, "frozen": 0.0,
"market_value": 0.0, "total": 1e7, "positions": []})
else:
self._respond(404, {"detail": "not found"})
def do_POST(self):
path = urlparse(self.path).path
if path != "/order":
self._respond(404, {"detail": "not found"})
return
if not self._has_token():
self._respond(401, {"detail": "token 无效"})
return
length = int(self.headers.get("Content-Length", 0))
req = json.loads(self.rfile.read(length))
self.server.orders.append(req) # 记录,供测试断言
self._respond(200, {"ok": True, "order_id": 99999})
class _MockBridge:
"""mock bridge HTTP server(后台线程)。"""
def __init__(self):
self.server = HTTPServer(("127.0.0.1", 0), _MockBridgeHandler)
self.server.orders = []
self.port = self.server.server_address[1]
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
def start(self) -> "_MockBridge":
self.thread.start()
return self
def stop(self) -> None:
self.server.shutdown()
@property
def url(self) -> str:
return f"http://127.0.0.1:{self.port}"
@property
def orders(self) -> list:
"""mock bridge 收到的下单请求(handler 写入 server.orders)。"""
return self.server.orders
class TestBridgeClientRealHttp:
"""BridgeClient 真发 HTTP round-trip(验证 HTTP 层 + 鉴权 + 解析)。"""
def test_get_account_with_token(self):
b = _MockBridge().start()
try:
r = BridgeClient(b.url, _TOKEN).get_account()
assert r is not None and r["ok"] is True and r["cash"] == 1e7
finally:
b.stop()
def test_order_real_http_payload(self):
b = _MockBridge().start()
try:
r = BridgeClient(b.url, _TOKEN).place_order("sh600000", "buy", 10.5, 100)
assert r == {"ok": True, "order_id": 99999}
assert b.orders == [{"code": "sh600000", "action": "buy", "price": 10.5,
"volume": 100, "price_type": "limit", "reason": ""}]
finally:
b.stop()
def test_wrong_token_returns_none_on_401(self):
b = _MockBridge().start()
try:
# 错 token → mock 返回 401 → urlopen 抛 HTTPError → bridge_client 返回 None
r = BridgeClient(b.url, "wrong-token").get_account()
assert r is None
finally:
b.stop()
def _cfg(url: str) -> SimpleNamespace:
return SimpleNamespace(live={"enabled": True, "bridge_url": url, "shadow": True})
class TestShadowTradesRealHttp:
"""_shadow_trades_to_bridge 真发 HTTP 到 mock bridgeD-4a sanguo 半边端到端)。"""
def test_full_shadow_roundtrip(self, tmp_path, monkeypatch):
db = str(tmp_path / "t.db")
init_db(db)
trade_id = save_trade(db, 1, {"symbol": "600000", "direction": "buy", "price": 10.5,
"volume": 100, "bar_date": "2026-07-11",
"strategy_id": "s1"})
monkeypatch.setenv("BRIDGE_TOKEN", _TOKEN)
b = _MockBridge().start()
try:
_shadow_trades_to_bridge(db, 1, "2026-07-11", _cfg(b.url))
# mock bridge 真收到下单(symbol 已转 sh600000
assert len(b.orders) == 1
assert b.orders[0]["code"] == "sh600000"
assert b.orders[0]["action"] == "buy"
# paper_shadow_orders 真记录
assert is_trade_shadowed(db, 1, trade_id)
finally:
b.stop()