实盘对接开发文档

A股量化平台 · 支付渠道 / 券商网关 / QMT / 行情源对接指南 正式入口:https://quant.quasarxode.cn


  1. 对接总览
  2. 支付渠道对接
  3. 券商 HTTP 网关协议
  4. QMT 对接
  5. 行情源对接
  6. 自动交易风控接口
  7. 管理后台配置清单
  8. 故障排查

一、对接总览

平台有四类外部对接点,彼此独立,按需启用:

对接点用途必需性配置位置
支付渠道在线售卖套餐可选(关闭时前端隐藏充值入口)管理后台 → 系统设置
券商 HTTP 网关实盘下单(服务器在 Linux)可选用户端 → 券商账户
QMT(迅投)实盘下单(服务器在 Windows)可选用户端 → 券商账户
行情源盘中实时价自动启用管理后台 → 系统设置

关键设计原则:所有对接点默认关闭。没配凭据时,接口返回明确的「未配置」错误,而不是假装成功。这一点很重要——编造一个恒成功的支付 mock,用户付了钱套餐不开,比功能不存在糟糕得多。


二、支付渠道对接

2.1 支持的渠道

渠道形态商户资质要求
支付宝当面付用户扫码付款企业支付宝 + 当面付产品签约
微信支付 Native用户扫码付款微信支付商户号 + Native 支付产品

两者都是直连官方,不经过聚合支付。资金直达、对账清晰、无中间商。

2.2 支付宝配置

在管理后台「系统设置」填入以下项:

设置键说明示例
alipay_app_id开放平台应用 AppID2021000000000000
alipay_private_key应用私钥(PKCS#8)可填纯 base64 或完整 PEM
alipay_public_key支付宝公钥(不是应用公钥)用于验签回调
alipay_gateway网关地址https://openapi.alipay.com/gateway.do
payment_enabled总开关true

最容易搞错的两点:

  1. alipay_public_key 必须是支付宝公钥,不是你自己生成的公钥。在开放平台「应用详情 → 开发设置 → 接口加签方式」里能看到。填错的表现是:能正常下单,但回调全部验签失败,订单永远停在 pending。
  2. 应用私钥必须是 PKCS#8 格式。用 openssl 转换:
   # PKCS#1 转 PKCS#8
   openssl pkcs8 -topk8 -inform PEM -in private_key_pkcs1.pem \
     -outform PEM -nocrypt -out private_key_pkcs8.pem

回调地址:https://你的域名/api/payments/notify/alipay

在支付宝开放平台配置这个地址,必须是 https 公网可访问。回调是 form POST,平台返回纯文本 success。

2.3 微信支付 V3 配置

设置键说明
wechat_mch_id商户号
wechat_app_id绑定的公众号/小程序 AppID
wechat_api_v3_keyAPIv3 密钥(严格 32 字节)
wechat_serial_no商户 API 证书序列号
wechat_private_key商户 API 私钥(完整 PEM)
wechat_notify_url回调地址,必须 https 公网

回调地址:https://你的域名/api/payments/notify/wechat

V3 回调是 JSON + AES-256-GCM 密文。平台用 APIv3 密钥解密,解密成功即认为可信——攻击者不知道这个密钥就伪造不出能解开的密文,这是 V3 相比 V2 的核心改进。

2.4 金额单位约定

位置单位
数据库 amount_cents分(整数)
微信支付 API分(整数)
支付宝 API元(字符串,两位小数)
前端展示元

全部转换集中在 payment_service.py,不要在其他地方自己除 100——浮点误差会累积成一分钱的差错。

2.5 支付状态机

pending ──回调成功──> paid ──管理员标记──> refunded
   │
   ├──超时 30 分钟──> closed
   └──网关拒绝──────> failed

回调必须幂等。支付平台最多重试 8 次(间隔从 4 分钟涨到 24 小时)。平台用条件更新 WHERE status='pending' 实现幂等:受影响行数为 0 就说明别人已处理,直接返回成功不再重复开通套餐。

2.6 回调丢失怎么补单

管理后台 → 套餐支付 → 支付订单 → 找到 pending 订单 → 「补单」。

补单会直接把订单置为 paid 并开通/续期订阅,同时记审计日志(谁给谁补的、多少金额)。适用于线下转账、防火墙拦掉回调等情况。

2.7 退款

平台不做网页一键退款。资金操作做成按钮的风险是误点后钱退出去追不回。

正确流程:

  1. 到支付宝/微信商户后台操作退款
  2. 回到平台 → 套餐支付 → 「标记退款」
  3. 平台只改本地状态,用于对账

三、券商 HTTP 网关协议

3.1 为什么要自己写网关

A 股散户没有统一的下单 API。各家券商的 REST 网关字段名、签名算法、错误码都不一样,逐一适配是维护不完的泥潭。

平台定义一套极简协议,你用二三十行 Flask/FastAPI 桥接自己的券商 SDK 即可。比我们适配 N 家可持续得多。

3.2 通用约定

Header内容
X-Api-Key你在平台配置的 api_key
X-Timestamp秒级 Unix 时间戳
X-Signature见下方签名算法

签名算法:

signature = HMAC-SHA256(api_secret, timestamp + path + body)

平台允许 5 分钟时钟偏差。超过则视为重放攻击,网关应拒绝。

3.3 端点规范

GET /health → 自检

请求体:{}

响应:

{ "ok": true, "message": "网关正常" }

平台在「测试连接」时调用。返回 ok: false 会显示失败原因。

POST /account → 查资金

响应:

{
  "total_asset": 100000.00,
  "cash": 50000.00,
  "available": 48000.00,
  "market_value": 50000.00,
  "frozen": 2000.00
}

单位统一为元。available 是可用于买入的资金(已扣除冻结)。

POST /positions → 查持仓

响应(数组或 {"items": [...]}):

[
  {
    "ts_code": "000001.SZ",
    "name": "平安银行",
    "volume": 1000,
    "available": 1000,
    "cost_price": 11.20,
    "market_price": 11.35
  }
]

POST /order → 下单

请求:

{
  "ts_code": "000001.SZ",
  "side": "buy",
  "volume": 100,
  "price": 11.35,
  "price_type": "limit"
}

响应:

{
  "order_id": "20261024000001",
  "status": "submitted",
  "filled_price": 0,
  "filled_volume": 0,
  "message": "已报单"
}

POST /cancel → 撤单

请求:{"order_id": "20261024000001"}

响应:{"ok": true, "message": "已撤单"}

3.4 参考实现(FastAPI)

import hashlib
import hmac
import time
from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
API_KEY = "your-api-key"
API_SECRET = "your-api-secret"

def verify(ts: str, path: str, body: str, sig: str) -> bool:
    # 5 分钟时钟偏差
    if abs(time.time() - int(ts)) > 300:
        return False
    expect = hmac.new(
        API_SECRET.encode(), f"{ts}{path}{body}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expect, sig)

@app.post("/{path:path}")
async def handler(
    path: str,
    request: Request,
    x_api_key: str = Header(""),
    x_timestamp: str = Header(""),
    x_signature: str = Header(""),
):
    body = (await request.body()).decode("utf-8")
    if x_api_key != API_KEY or not verify(x_timestamp, "/" + path, body, x_signature):
        raise HTTPException(401, "签名校验失败")

    p = "/" + path
    if p == "/health":
        return {"ok": True, "message": "网关正常"}

    if p == "/account":
        # TODO: 调用你的券商 SDK
        return {"total_asset": 100000, "cash": 50000,
                "available": 48000, "market_value": 50000, "frozen": 0}

    if p == "/positions":
        # TODO: 调用你的券商 SDK
        return []

    if p == "/order":
        # TODO: 调用你的券商 SDK 下单,返回真实委托号
        return {"order_id": "MOCK001", "status": "submitted",
                "filled_price": 0, "filled_volume": 0, "message": "已报单"}

    if p == "/cancel":
        return {"ok": True, "message": "已撤单"}

    raise HTTPException(404, "未知端点")

安全要求:

3.5 用 easytrader 包一层

如果你的券商只提供客户端(无 API),可以用 GUI 自动化库包一层:

import easytrader
from fastapi import FastAPI

user = easytrader.use("ths")   # 同花顺
user.connect(r"C:\同花顺\xiadan.exe")

@app.post("/order")
def order(p: dict):
    if p["side"] == "buy":
        r = user.buy(p["ts_code"], price=p["price"], amount=p["volume"])
    else:
        r = user.sell(p["ts_code"], price=p["price"], amount=p["volume"])
    return {"order_id": str(r.get("entrust_no", "")), "status": "submitted",
            "message": "已报单"}

这种方案的现实问题:GUI 自动化极其脆弱——客户端升级、弹窗、分辨率变化都会让它失效。只适合小资金自用,不适合托管他人资金。平台把这类风险留给用户自己判断,不代替做决定。


四、QMT 对接

4.1 硬性前提(缺一不可)

  1. Windows 机器(xtquant 是 Windows-only 的本地库)
  2. 已安装 QMT 客户端,且开启极简模式(miniQMT)
  3. 券商已开通量化交易权限(多数要求资产 50 万以上)
  4. QMT 客户端已登录
  5. 平台后端与 QMT 在同一台机器(xtquant 通过本地 socket 通信)

4.2 配置方式

用户端 → 券商账户 → 新建 → 通道选 QMT,填:

字段说明示例
账户名称自定义我的 QMT
客户端路径QMT 安装目录下的 userdata_miniD:\国金QMT\userdata_mini
资金账号券商资金账号12345678
通道类型STOCK(普通股票)默认即可

4.3 Linux 服务器怎么办

xtquant 在 Linux 上 import 直接失败。平台做了延迟导入 + 明确报错,不会让整个模块崩溃。

如果生产后端在 Linux(本平台默认部署形态),需要用 Windows 中继机:

Linux 后端  ──HTTP──>  Windows 中继机(跑 QMT + 一个网关)  ──本地 socket──>  QMT 客户端

中继机上的网关按上面「券商 HTTP 网关协议」实现,把 /order 转发给 xtquant。这是唯一可靠的跨平台方案。

4.4 代码位置

backend/app/services/broker_service.py 的 QmtBroker 类,关键点:


五、行情源对接

5.1 为什么不用 tushare 做实时

tushare 免费账号的实时接口有严格频率限制,几十个标的轮询就被限流;数据延迟到分钟级,做下单决策太慢。

平台默认走新浪 + 腾讯的公开 Level-1 快照:免费、无需 token、支持批量、延迟 3~5 秒。tushare 保留做日线(回测用,需要复权因子和历史深度)。

5.2 默认优先级

新浪 hq.sinajs.cn  →  腾讯 qt.gtimg.cn  →  tushare  →  合成数据

可在管理后台「系统设置 → quote_source_order」调整,逗号分隔。

5.3 两个接口的差异

项目新浪腾讯
URLhttps://hq.sinajs.cn/list=sh600000,sz000001https://qt.gtimg.cn/q=sh600000,sz000001
编码GBK(必须显式解码)GBK
必需 HeaderReferer: https://finance.sina.com.cnReferer: https://gu.qq.com
成交量单位股手(需 ×100)
字段分隔,~

必踩的坑:

  1. 不带 Referer 时新浪直接 403。看起来像「接口挂了」,其实是防盗链。
  2. 用 resp.text 会按 UTF-8 猜,中文股票名变乱码。必须 resp.content.decode("gbk")。
  3. 停牌时现价为 0,要用昨收顶上,否则下游算出 -100% 跌幅。

这些都在 quote_service.py 里处理了。要加新源,照着 _fetch_sina 的形态写一个函数,注册到 _SOURCES 即可。

5.4 自检接口

GET /api/market/sources —— 真实探测每个源是否可用,返回延迟与实际价格。用户反馈「信号不动了」时,先调这个。

用户端「券商账户」页有「行情源自检」按钮,点一下就出结果。


六、自动交易风控接口

6.1 三道闸门

自动交易的委托在发出前要过三关,任一不过直接拒绝:

1. 全局熔断(管理员)      auto_trade_kill_switch
2. 用户开关               AutoTradeConfig.is_enabled
3. 九项风控校验            RiskRule

6.2 九项风控

#规则字段说明
1全局熔断auto_trade_kill_switch管理员一键停全站
2用户开关is_enabled用户可随时关
3当日熔断halted触发后当日停
4交易时段trading_windows09:30-11:30,13:00-15:00
5标的黑名单symbol_blacklist永不交易
6标的白名单symbol_whitelist留空=不限
7单笔金额max_order_amount0=不限
8当日笔数max_daily_orders
9当日金额max_daily_amount
10当日亏损max_daily_loss超限熔断
11单标的占比max_position_pct买入时查持仓

6.3 为什么风控在平台侧而不是券商侧

券商只保障「钱够不够、券够不够」,不会拦「今天已经下了 30 单」。

策略写错(比如均线参数填成 0)会在几分钟内打出几百笔委托,把手续费吃穿。风控必须在委托发出之前硬拦。

6.4 关键接口

接口用途
GET /api/autotrade/config读配置 + 可用账户 + 套餐状态 + 熔断状态
PUT /api/autotrade/config保存开关(开启时校验账户连通性)
GET/PUT /api/autotrade/risk风控规则
GET /api/autotrade/stats今日统计
GET /api/autotrade/logs执行流水(成功/风控拒绝/失败)
POST /api/autotrade/run-signal/{id}手动执行某条信号(验证配置用)
POST /api/autotrade/reset-halt解除当日熔断
POST /api/admin/autotrade/kill-switch?enable=管理员全局熔断

6.5 执行流水必看

每条信号都会落一条 AutoTradeLog,无论成功失败。失败原因写得是给人看的,例如:

失败推送比成功推送更有价值——只在成功时推送,用户会以为策略在跑,其实一笔都没成交。


七、管理后台配置清单

上线前必须过一遍:

7.1 必填(安全相关)

设置键说明
SECRET_KEY(.env)固定值,不配则重启后所有 JWT 失效
ADMIN_PASSWORD(.env)首次建库时生效
SITE_BASE_URL 或 payment_base_url支付回调地址的前缀

7.2 支付(要卖套餐才填)

payment_enabled         = true
alipay_app_id           = ...
alipay_private_key      = ...
alipay_public_key       = ...   ← 支付宝公钥,不是应用公钥
alipay_gateway          = https://openapi.alipay.com/gateway.do
wechat_mch_id           = ...
wechat_app_id           = ...
wechat_api_v3_key       = ...   ← 严格 32 字节
wechat_serial_no        = ...
wechat_private_key      = ...
wechat_notify_url       = https://你的域名/api/payments/notify/wechat

7.3 行情

quote_source_order      = sina,tencent,tushare
quote_timeout           = 6
quote_allow_mock        = true    ← 演示环境可开,生产建议 false

7.4 自动交易

auto_trade_kill_switch     = false   ← 出事时改 true 一键停全站
auto_trade_max_per_minute  = 5
broker_http_timeout        = 10

7.5 套餐

默认带三个:free / pro_monthly(¥99/月)/ pro_yearly(¥999/年)。可在「套餐支付 → 套餐配置」改价格、配额、是否允许实盘。

免费版不允许实盘是刻意的——实盘是最危险的能力,不该出现在最便宜的档位。


八、故障排查

支付能下单但订单永远 pending

  1. 回调地址是否 https 公网可访问(curl -I https://你的域名/api/payments/notify/alipay)
  2. alipay_public_key 是不是填成了应用公钥(应该是支付宝公钥)
  3. Nginx 是否拦了 POST(检查 client_max_body_size)
  4. 看后端日志有没有「验签失败」

微信回调解密失败

wechat_api_v3_key 必须严格 32 字节。多一个空格、少一个字符都不行。

行情取自检失败

  1. 服务器能否访问 hq.sinajs.cn(curl -I https://hq.sinajs.cn)
  2. 是否被运营商 DNS 污染(换 223.5.5.5 试)
  3. 检查有没有带 Referer(不带会被 403)

自动交易开了但不下单

按顺序查:

  1. GET /api/autotrade/logs 看 rejected_by_risk 的原因
  2. 检查 trading_windows(非交易时段全部拒绝)
  3. 检查 broker_account_id 指向的账户是否 is_enabled
  4. 检查套餐是否 allow_live_trade
  5. 检查全局熔断
  6. 券商账户「测试连接」看连通性

QMT 连接失败 code=-1

按顺序查:

  1. QMT 客户端是否已登录
  2. 是否开启了「极简模式」
  3. qmt_path 是否指向 userdata_mini 目录(不是安装根目录)
  4. 平台后端是否与 QMT 同机
  5. 是否已被别的程序占用 session(QMT 同一账户只允许一个连接)

附:代码位置索引

模块文件
支付网关backend/app/services/payment_service.py
支付路由backend/app/routers/payments.py
券商适配层backend/app/services/broker_service.py
券商路由backend/app/routers/brokers.py
自动交易引擎backend/app/services/auto_trade_service.py
自动交易路由backend/app/routers/autotrade.py
行情源backend/app/services/quote_service.py
行情统一入口backend/app/services/market_service.py
端到端测试backend/tests/payment_trade_test.py
生产验证deploy/verify_deployment.py(第 17 段)
页面验证frontend/tests/payment-pages.test.cjs