Files
sanguo_vnpy_v2/sanguo_trader/bridge_client.py
T
claude_dev ff84b3d4b0 feat(live): D-3 sanguo实盘分支(影子下单)+D期设计文档
D-3 模式A影子下单(spec §5):
- bridge_client.py: QMT bridge HTTP客户端(urllib, X-Bridge-Token, 失败不抛返回None)
- live_orchestrator: _shadow_trades_to_bridge 当日成交POST bridge(默认enabled=false)
- persistence: paper_shadow_orders幂等表+save_shadow_order/is_trade_shadowed
- config: data_platform.yaml加live段, token走env(BRIDGE_TOKEN)
- to_bridge_code symbol转换与guess_exchange一致(2位前缀)
安全: enabled=false默认关+token走env+幂等防重复+影子失败不阻断live_step

docs: phase3d-live-trading-design.md(D期完整设计)
2026-07-11 00:03:46 +08:00

101 lines
4.1 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.
"""sanguo 端 QMT bridge HTTP 客户端(D-3 影子下单)。
封装跨网调 Windows bridge 的 HTTP 调用(POST /order、GET /account、GET /positions)。
设计原则:失败不抛异常,记 logger.warning 返回 None —— 影子下单是旁路,
绝不能阻断 PaperEngine 的模拟盘撮合主流程(spec §5 模式 A)。
依赖:仅标准库 urllib(不引入 requests/httpx),避免新增第三方依赖。
接口契约见 sanguo_qmt_bridge/README.md。
"""
import json
import logging
import urllib.error
import urllib.request
from typing import Any
logger = logging.getLogger(__name__)
_TIMEOUT = 10 # 秒
def to_bridge_code(symbol: str) -> str:
"""sanguo symbol → bridge codesh600000 / sz000001)。
sanguo 内部 symbol 为纯数字码(如 '600000''000001'),
paper_trades.symbol 即此格式(见 engine._match 的 save_trade 入参)。
bridge 接受 sh/sz 前缀码(见 sanguo_qmt_bridge/README.md「代码格式」)。
规则(与 sanguo_data.datareader.guess_exchange 一致,避免在客户端 import vnpy):
沪市 SSE60/68/51/56/58 开头 → sh
深市 SZSE00/30/15 开头 → sz
默认 → shguess_exchange 兜底亦为 SSE
若 symbol 已含 sh/sz 前缀,直接小写返回。
"""
if symbol[:2].lower() in ("sh", "sz"):
return symbol.lower()
if symbol.startswith(("60", "68", "51", "56", "58")):
return f"sh{symbol}"
if symbol.startswith(("00", "30", "15")):
return f"sz{symbol}"
return f"sh{symbol}"
class BridgeClient:
"""QMT bridge HTTP 客户端(影子下单旁路)。
每个请求带 header X-Bridge-Token。任何网络/解析失败均返回 None 并记 warning
不向调用方抛异常 —— 影子下单失败不得阻断 live_step 主流程。
"""
def __init__(self, url: str, token: str) -> None:
self._url = url.rstrip("/")
self._token = token
def _headers(self) -> dict[str, str]:
return {"X-Bridge-Token": self._token, "Content-Type": "application/json"}
def _post(self, path: str, payload: dict[str, Any]) -> dict | None:
"""POST JSON,返回解析后 dict;失败记 warning 返回 None。"""
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
f"{self._url}{path}", data=data, headers=self._headers(), method="POST"
)
try:
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
return json.loads(resp.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as e:
logger.warning("bridge POST %s 失败: %s", path, e)
return None
def _get(self, path: str) -> dict | None:
"""GET JSON,返回解析后 dict;失败记 warning 返回 None。"""
req = urllib.request.Request(
f"{self._url}{path}", headers=self._headers(), method="GET"
)
try:
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
return json.loads(resp.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as e:
logger.warning("bridge GET %s 失败: %s", path, e)
return None
def place_order(self, code: str, action: str, price: float, volume: int,
price_type: str = "limit", reason: str = "") -> dict | None:
"""POST /order → {ok: bool, order_id: int} 或 {ok: false, error: str};失败返回 None。"""
return self._post("/order", {
"code": code, "action": action, "price": price,
"volume": volume, "price_type": price_type, "reason": reason,
})
def get_account(self) -> dict | None:
"""GET /account → {ok, cash, frozen, market_value, total};失败返回 None。"""
return self._get("/account")
def get_positions(self) -> list | None:
"""GET /positions → positions 列表;失败返回 None。"""
resp = self._get("/positions")
if resp is None:
return None
return resp.get("positions")