8c06ef1e53
bridge.py: POST /cancel + GET /orders; xt_gateway: query_orders/cancel_order(含xtquant状态映射50挂单/56已成/57拒单/54撤单); bridge_client: get_orders/cancel_order. 已在 VPS 生产验证.
124 lines
5.0 KiB
Python
124 lines
5.0 KiB
Python
"""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 code(sh600000 / 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):
|
||
沪市 SSE:60/68/51/56/58 开头 → sh
|
||
深市 SZSE:00/30/15 开头 → sz
|
||
默认 → sh(guess_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 code(sh600000/sz000001)→ sanguo 纯数字码(to_bridge_code 逆函数)。
|
||
|
||
D-4c 模式 B reconcile:bridge /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)})
|