Files
sanguo_vnpy_v2/sanguo_qmt_bridge/bridge.py
T
claude_dev eff9ed2ae9 feat(bridge): D-1 bridge MVP—FastAPI 4接口(xtquant封装+token鉴权+sh/sz代码转换)
- bridge.py: lifespan连miniQMT, health/order/account/positions, 连不上不崩
- xt_gateway.py: xtquant单例封装(延迟import), 照搬check_xtquant验证模式
- auth.py: X-Bridge-Token校验(hmac防时序攻击), 未配token返回503不裸奔
- requirements.txt(fastapi+uvicorn) + README.md(Windows部署步骤)

安全: 无硬编码secret, token/userdata/account均走环境变量(grep验证CLEAN)
2026-07-10 23:48:59 +08:00

116 lines
3.8 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.
"""D-1 实盘 bridge MVPFastAPI 4 接口(health/order/account/positions)。
监听 127.0.0.1:8765,启动时连 miniQMT(连不上不崩溃,/health 报 disconnected)。
xtquant 调用照搬 check_xtquant.py 验证过的模式(封装在 xt_gateway.py)。
token 鉴权(auth.py),/health 豁免。
启动:uvicorn bridge:app --host 127.0.0.1 --port 8765
"""
import logging
from contextlib import asynccontextmanager
from typing import Literal
from fastapi import Depends, FastAPI
from pydantic import BaseModel, Field
from auth import verify_token
from xt_gateway import gateway
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger(__name__)
# ===== 请求模型 =====
class OrderRequest(BaseModel):
"""下单请求(sanguo 格式代码 sh600000/sz000001)。"""
code: str = Field(..., examples=["sh600000"])
action: Literal["buy", "sell"]
price: float = Field(..., gt=0, examples=[10.50])
volume: int = Field(..., gt=0, examples=[100])
price_type: Literal["limit", "market"] = "limit"
reason: str | None = None
# ===== 生命周期 =====
@asynccontextmanager
async def lifespan(_app: FastAPI):
"""启动时连接 miniQMT;连不上不阻塞服务。"""
ok = gateway.connect()
if not ok:
logger.warning("miniQMT 连接失败,服务继续运行(/health 报 disconnected")
yield
# shutdown: 无资源需释放(xtquant.stop 由 miniQMT 管控)
app = FastAPI(title="sanguo QMT bridge", version="0.1.0", lifespan=lifespan)
# ===== 接口 =====
@app.get("/health")
async def health() -> dict:
"""健康检查(无需鉴权,供 frpc/Caddy 探活)。"""
return {"status": "ok", "miniqmt_connected": gateway.connected}
@app.post("/order", dependencies=[Depends(verify_token)])
async def place_order(req: OrderRequest) -> dict:
"""下单 -> {ok, order_id?} 或 {ok:false, error}。"""
if not gateway.connected:
return {"ok": False, "error": "miniQMT 未连接"}
logger.info(
"下单请求 code=%s action=%s price=%s volume=%s type=%s reason=%s",
req.code, req.action, req.price, req.volume, req.price_type, req.reason,
)
try:
order_id = gateway.place_order(
code=req.code,
action=req.action,
price=req.price,
volume=req.volume,
price_type=req.price_type,
)
except (ValueError, RuntimeError) as e:
return {"ok": False, "error": str(e)}
except Exception as e:
logger.error("下单异常: %s", e)
return {"ok": False, "error": f"下单异常: {e}"}
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}
@app.get("/account", dependencies=[Depends(verify_token)])
async def query_account() -> dict:
"""查资金 -> {ok, cash, frozen, market_value, total} 或 {ok:false, error}。"""
if not gateway.connected:
return {"ok": False, "error": "miniQMT 未连接"}
try:
data = gateway.query_account()
except Exception as e:
logger.error("查资金异常: %s", e)
return {"ok": False, "error": f"查询异常: {e}"}
return {"ok": True, **data}
@app.get("/positions", dependencies=[Depends(verify_token)])
async def query_positions() -> dict:
"""查持仓 -> {ok, positions:[...]} 或 {ok:false, error}。"""
if not gateway.connected:
return {"ok": False, "error": "miniQMT 未连接"}
try:
positions = gateway.query_positions()
except Exception as e:
logger.error("查持仓异常: %s", e)
return {"ok": False, "error": f"查询异常: {e}"}
return {"ok": True, "positions": positions}