feat(bridge): bridge稳定性完善—自动重连miniQMT+health探活+交易日判断

- xt_gateway: _open_session抽离, reconnect(stop旧trader+重建), is_alive(query探活), _retry_with_reconnect(query/order失败重连重试一次)
- query_account/positions/place_order包重试: 断线(异常/None)→reconnect→重试, broker拒单(order_id<=0)不重连
- bridge /health: is_alive真实探活(5s缓存)+断线后台reconnect(不阻塞), 不再假阳性
- trade_calendar: is_trading_day(周一-周五), /order非交易日加warning(120141提示)
- test_gateway15+test_trade_calendar6=NAS21passed, 回归bridge_client/d4a 10绿
- 修复Issue#4运维发现: miniQMT重启后bridge自动重连(无需手动重启)
This commit is contained in:
2026-07-11 07:32:23 +08:00
parent a93d5ed8d8
commit cadc59e6dc
5 changed files with 488 additions and 77 deletions
+41 -3
View File
@@ -7,6 +7,8 @@ token 鉴权(auth.py),/health 豁免。
启动:uvicorn bridge:app --host 127.0.0.1 --port 8765
"""
import logging
import threading
import time
from contextlib import asynccontextmanager
from typing import Literal
@@ -14,6 +16,7 @@ from fastapi import Depends, FastAPI
from pydantic import BaseModel, Field
from auth import verify_token
from trade_calendar import is_trading_day
from xt_gateway import gateway
logging.basicConfig(
@@ -51,12 +54,44 @@ async def lifespan(_app: FastAPI):
app = FastAPI(title="sanguo QMT bridge", version="0.1.0", lifespan=lifespan)
# ===== /health 探活缓存(避免每次 health 都 query_stock_asset=====
_PROBE_CACHE_SECONDS = 5.0
_last_probe_time: float = 0.0
_last_probe_alive: bool = False
def _probe_alive() -> bool:
"""探活 miniQMT 真实连接(5 秒缓存)。
缓存过期时调 gateway.is_alive()query_stock_asset 探活);
探活失败且 gateway 自认已连接 → 后台触发 reconnect(不阻塞 health 响应)。
"""
global _last_probe_time, _last_probe_alive
now = time.monotonic()
if now - _last_probe_time < _PROBE_CACHE_SECONDS:
return _last_probe_alive
alive = gateway.is_alive()
_last_probe_time = now
_last_probe_alive = alive
if not alive and gateway.connected:
# _connected 假阳性(miniQMT 可能重启),后台重连不阻塞响应
threading.Thread(target=gateway.reconnect, daemon=True).start()
return alive
# ===== 接口 =====
@app.get("/health")
async def health() -> dict:
"""健康检查(无需鉴权,供 frpc/Caddy 探活)。"""
return {"status": "ok", "miniqmt_connected": gateway.connected}
"""健康检查(无需鉴权,供 frpc/Caddy 探活)。
miniqmt_connected 调 gateway.is_alive() 真实探活(5 秒缓存),
不再依赖可能假阳性的 _connected 标志。
"""
return {"status": "ok", "miniqmt_connected": _probe_alive()}
@app.post("/order", dependencies=[Depends(verify_token)])
@@ -86,7 +121,10 @@ async def place_order(req: OrderRequest) -> dict:
if order_id <= 0:
return {"ok": False, "error": f"报单失败 order_id={order_id}"}
logger.info("下单成功 order_id=%s code=%s action=%s", order_id, req.code, req.action)
return {"ok": True, "order_id": order_id}
result: dict = {"ok": True, "order_id": order_id}
if not is_trading_day():
result["warning"] = "非交易日,miniQMT 可能拒绝(120141)"
return result
@app.get("/account", dependencies=[Depends(verify_token)])