实盘对接开发文档
A股量化平台 · 支付渠道 / 券商网关 / QMT / 行情源对接指南 正式入口:https://quant.quasarxode.cn
一、对接总览
平台有四类外部对接点,彼此独立,按需启用:
| 对接点 | 用途 | 必需性 | 配置位置 |
|---|---|---|---|
| 支付渠道 | 在线售卖套餐 | 可选(关闭时前端隐藏充值入口) | 管理后台 → 系统设置 |
| 券商 HTTP 网关 | 实盘下单(服务器在 Linux) | 可选 | 用户端 → 券商账户 |
| QMT(迅投) | 实盘下单(服务器在 Windows) | 可选 | 用户端 → 券商账户 |
| 行情源 | 盘中实时价 | 自动启用 | 管理后台 → 系统设置 |
关键设计原则:所有对接点默认关闭。没配凭据时,接口返回明确的「未配置」错误,而不是假装成功。这一点很重要——编造一个恒成功的支付 mock,用户付了钱套餐不开,比功能不存在糟糕得多。
二、支付渠道对接
2.1 支持的渠道
| 渠道 | 形态 | 商户资质要求 |
|---|---|---|
| 支付宝当面付 | 用户扫码付款 | 企业支付宝 + 当面付产品签约 |
| 微信支付 Native | 用户扫码付款 | 微信支付商户号 + Native 支付产品 |
两者都是直连官方,不经过聚合支付。资金直达、对账清晰、无中间商。
2.2 支付宝配置
在管理后台「系统设置」填入以下项:
| 设置键 | 说明 | 示例 |
|---|---|---|
alipay_app_id | 开放平台应用 AppID | 2021000000000000 |
alipay_private_key | 应用私钥(PKCS#8) | 可填纯 base64 或完整 PEM |
alipay_public_key | 支付宝公钥(不是应用公钥) | 用于验签回调 |
alipay_gateway | 网关地址 | https://openapi.alipay.com/gateway.do |
payment_enabled | 总开关 | true |
最容易搞错的两点:
alipay_public_key必须是支付宝公钥,不是你自己生成的公钥。在开放平台「应用详情 → 开发设置 → 接口加签方式」里能看到。填错的表现是:能正常下单,但回调全部验签失败,订单永远停在pending。- 应用私钥必须是 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_key | APIv3 密钥(严格 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 退款
平台不做网页一键退款。资金操作做成按钮的风险是误点后钱退出去追不回。
正确流程:
- 到支付宝/微信商户后台操作退款
- 回到平台 → 套餐支付 → 「标记退款」
- 平台只改本地状态,用于对账
三、券商 HTTP 网关协议
3.1 为什么要自己写网关
A 股散户没有统一的下单 API。各家券商的 REST 网关字段名、签名算法、错误码都不一样,逐一适配是维护不完的泥潭。
平台定义一套极简协议,你用二三十行 Flask/FastAPI 桥接自己的券商 SDK 即可。比我们适配 N 家可持续得多。
3.2 通用约定
- 所有请求:
POST {base_url}{path} - Content-Type:
application/json - 必须带三个 Header:
| Header | 内容 |
|---|---|
X-Api-Key | 你在平台配置的 api_key |
X-Timestamp | 秒级 Unix 时间戳 |
X-Signature | 见下方签名算法 |
签名算法:
signature = HMAC-SHA256(api_secret, timestamp + path + body)
path是不含域名的路径,如/orderbody是原始 JSON 字符串(与你实际发出的字节完全一致,不要重新序列化)- 结果用小写十六进制表示
平台允许 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
}
]
volume单位是股(不是手)available是 T+1 后可卖数量ts_code必须是代码.交易所格式
POST /order → 下单
请求:
{
"ts_code": "000001.SZ",
"side": "buy",
"volume": 100,
"price": 11.35,
"price_type": "limit"
}
side:buy|sellvolume:股数,必须是 100 的整数倍price_type:limit|marketprice:市价单可传 0
响应:
{
"order_id": "20261024000001",
"status": "submitted",
"filled_price": 0,
"filled_volume": 0,
"message": "已报单"
}
order_id必须返回,平台用它记账与撤单status:submitted(已报)/filled(已成交)/rejected(被拒)- 失败时返回
order_id: ""并在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, "未知端点")
安全要求:
- 网关只能监听内网或 127.0.0.1,通过 SSH 隧道或同机部署访问
- 不要开公网。开了就等于把下单权限暴露给全网
api_secret在平台侧是 AES-256-GCM 加密存储,接口返回只显示has_api_secret: true
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 硬性前提(缺一不可)
- Windows 机器(
xtquant是 Windows-only 的本地库) - 已安装 QMT 客户端,且开启极简模式(miniQMT)
- 券商已开通量化交易权限(多数要求资产 50 万以上)
- QMT 客户端已登录
- 平台后端与 QMT 在同一台机器(
xtquant通过本地 socket 通信)
4.2 配置方式
用户端 → 券商账户 → 新建 → 通道选 QMT,填:
| 字段 | 说明 | 示例 |
|---|---|---|
| 账户名称 | 自定义 | 我的 QMT |
| 客户端路径 | QMT 安装目录下的 userdata_mini | D:\国金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 类,关键点:
- 延迟导入:
from xtquant.xttrader import XtQuantTrader放在方法内 - 代码格式:QMT 用纯数字(
600000),平台内部用600000.SH,_with_suffix做转换 - 常量映射:方向
STOCK_BUY=23/STOCK_SELL=24;价格类型FIX_PRICE=11/LATEST_PRICE=5
五、行情源对接
5.1 为什么不用 tushare 做实时
tushare 免费账号的实时接口有严格频率限制,几十个标的轮询就被限流;数据延迟到分钟级,做下单决策太慢。
平台默认走新浪 + 腾讯的公开 Level-1 快照:免费、无需 token、支持批量、延迟 3~5 秒。tushare 保留做日线(回测用,需要复权因子和历史深度)。
5.2 默认优先级
新浪 hq.sinajs.cn → 腾讯 qt.gtimg.cn → tushare → 合成数据
可在管理后台「系统设置 → quote_source_order」调整,逗号分隔。
5.3 两个接口的差异
| 项目 | 新浪 | 腾讯 |
|---|---|---|
| URL | https://hq.sinajs.cn/list=sh600000,sz000001 | https://qt.gtimg.cn/q=sh600000,sz000001 |
| 编码 | GBK(必须显式解码) | GBK |
| 必需 Header | Referer: https://finance.sina.com.cn | Referer: https://gu.qq.com |
| 成交量单位 | 股 | 手(需 ×100) |
| 字段分隔 | , | ~ |
必踩的坑:
- 不带 Referer 时新浪直接 403。看起来像「接口挂了」,其实是防盗链。
- 用
resp.text会按 UTF-8 猜,中文股票名变乱码。必须resp.content.decode("gbk")。 - 停牌时现价为 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_windows | 09:30-11:30,13:00-15:00 |
| 5 | 标的黑名单 | symbol_blacklist | 永不交易 |
| 6 | 标的白名单 | symbol_whitelist | 留空=不限 |
| 7 | 单笔金额 | max_order_amount | 0=不限 |
| 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,无论成功失败。失败原因写得是给人看的,例如:
999999.SZ 在禁止交易黑名单中单笔金额 12000.00 超过上限 5000.00当前 12:05 不在允许交易时段(09:30-11:30,13:00-15:00)执行账户连通性测试失败:网关请求失败:Connection refused
失败推送比成功推送更有价值——只在成功时推送,用户会以为策略在跑,其实一笔都没成交。
七、管理后台配置清单
上线前必须过一遍:
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
- 回调地址是否 https 公网可访问(
curl -I https://你的域名/api/payments/notify/alipay) alipay_public_key是不是填成了应用公钥(应该是支付宝公钥)- Nginx 是否拦了 POST(检查
client_max_body_size) - 看后端日志有没有「验签失败」
微信回调解密失败
wechat_api_v3_key 必须严格 32 字节。多一个空格、少一个字符都不行。
行情取自检失败
- 服务器能否访问
hq.sinajs.cn(curl -I https://hq.sinajs.cn) - 是否被运营商 DNS 污染(换
223.5.5.5试) - 检查有没有带 Referer(不带会被 403)
自动交易开了但不下单
按顺序查:
GET /api/autotrade/logs看rejected_by_risk的原因- 检查
trading_windows(非交易时段全部拒绝) - 检查
broker_account_id指向的账户是否is_enabled - 检查套餐是否
allow_live_trade - 检查全局熔断
- 券商账户「测试连接」看连通性
QMT 连接失败 code=-1
按顺序查:
- QMT 客户端是否已登录
- 是否开启了「极简模式」
qmt_path是否指向userdata_mini目录(不是安装根目录)- 平台后端是否与 QMT 同机
- 是否已被别的程序占用 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 |