Files
sanguo_vnpy_v2/sanguo_trader/bridge_client.py
T
claude_dev 8c06ef1e53 feat(bridge): 补 /orders+/cancel 端点与客户端(实盘查委托/撤单)
bridge.py: POST /cancel + GET /orders; xt_gateway: query_orders/cancel_order(含xtquant状态映射50挂单/56已成/57拒单/54撤单); bridge_client: get_orders/cancel_order. 已在 VPS 生产验证.
2026-07-15 07:12:46 +08:00

124 lines
5.0 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}"
def from_bridge_code(code: str) -> str:
"""bridge codesh600000/sz000001)→ sanguo 纯数字码(to_bridge_code 逆函数)。
D-4c 模式 B reconcilebridge /positions 返回 sh/sz 前缀码,
account.positions 以纯数字码为 key(与 PaperEngine trade.symbol 一致)。
已是纯数字则原样返回。
"""
if code[:2].lower() in ("sh", "sz"):
return code[2:]
return code
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")
def get_orders(self) -> list | None:
"""GET /orders → 委托列表;失败返回 None。"""
resp = self._get("/orders")
if resp is None:
return None
return resp.get("orders")
def cancel_order(self, order_id: int) -> dict | None:
"""POST /cancel → {ok, order_id} 或 {ok:false, error};失败返回 None。"""
return self._post("/cancel", {"order_id": int(order_id)})