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:
@@ -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)])
|
||||
|
||||
Reference in New Issue
Block a user