深色模式
行情数据源接入协议(档位 B:多源解耦)
一、背景
主程序(Futures.Desktop)不绑定任何特定第三方行情源。实时行情通过「行情数据源」接入: 任何实现下述文本协议的服务(本地采集器 / 远程行情网关 / 自定义插件)都可注册为一个数据源。
设计目标:
- 数据源授权责任归数据源提供方(用户/服务器),主程序只负责协议与展示;
- 多源共存:按优先级依次尝试,全部失败后回落到 PostgreSQL 历史最新价;
- 现有业务代码零改动(统一走
IRealtimePriceCache接口)。
二、协议(极简单行文本)
请求: GET {symbol}\n
响应: {price}\n 有效价格(十进制,>0)
NONE\n 该合约暂无行情(协议可达)实现要点:
- TCP 流式,单次请求单行响应,响应后服务端可关闭连接;
symbol为合约代码(如au2608、DCE.m2609、IO2603-C-4100);- 连接/响应超时由客户端控制(默认 120ms,可配置);
- 健康检查:客户端发送
GET HEALTH\n,收到任何响应行(含NONE)即视为服务在线。
三、数据源配置
设置页「行情数据源」区可增删改数据源,持久化到 config/appsettings.json:
json
"QuoteSources": [
{
"Id": "futures-local", "Name": "本地期货行情",
"Host": "127.0.0.1", "Port": 7751,
"Priority": 0, "IsEnabled": true, "TimeoutMs": 120
},
{
"Id": "options-local", "Name": "本地期权行情",
"Host": "127.0.0.1", "Port": 7752, "Priority": 0, "IsEnabled": true
},
{
"Id": "overseas-local", "Name": "本地外盘行情",
"Host": "127.0.0.1", "Port": 7753, "Priority": 0, "IsEnabled": true
}
]字段说明:
| 字段 | 含义 |
|---|---|
Id | 唯一标识 |
Name | 显示名(设置页/监控页) |
Host | 127.0.0.1 = 本机采集器;远程 = 服务器行情网关地址 |
Port | 端口 |
Priority | 升序,越小越先尝试;同优先级按列表顺序 |
IsEnabled | 是否启用 |
TimeoutMs | 连接/响应超时(默认 120) |
四、内置参考实现
script/quote_server.py 是协议的标准参考实现(标准库,零依赖):
- 内存 dict 保存
symbol → last_price,采集脚本每 tick 调用update(symbol, price); start()后监听127.0.0.1:{port};- 天勤采集器 exe(tq_futures_pg / unified_options_main / tq_overseas_futures_pg)内嵌该服务。
四·B、CTP 双脚本采集(SimNow 仿真行情,替代天勤)
天勤方案与 CTP 方案并行可选:谁启动谁往
quotes_realtime/quotes_option_realtime写。 CTP 只做行情订阅(MdApi),不连交易,用 SimNow 仿真账户即可(无需穿透式认证)。
1. 前置
bash
pip install openctp-ctpscript/local_config.py 配置(SimNow 账号,simnow.com.cn 注册):
python
CTP_BROKER_ID = "9999" # SimNow 统一
CTP_USER_ID = "你的SimNow账号"
CTP_PASSWORD = "你的SimNow密码"
CTP_MD_FRONT = "tcp://182.254.243.31:30011" # 行情前置;7x24 用 tcp://182.254.243.31:400112. 启动(两个脚本,各自占用一个行情源端口)
bash
# 终端1:期货行情(六所全覆盖)→ 写 quotes_realtime + 本地服务 7751
python script/ctp_futures_pg.py
# 终端2:期权行情(六所全覆盖)→ 写 quotes_option_realtime(自算 Greeks/IV)+ 本地服务 7752
# 依赖期货脚本先跑(Greeks 需标的期货最新价;脚本也会从库兜底刷新)
python script/ctp_options_pg.py生产/客户机:用
script/build_exe.py打包为 exe(已注册ctp_futures_pg/ctp_options_pg入口, openctp_ctp SWIG 绑定已配置完整收集)。
3. 验证(逐个检查)
bash
# ① 脚本日志:应显示"行情登录成功" + "订阅 N 个合约"
# 查看 log/ctp_futures_pg.log / log/ctp_options_pg.log
# ② 本地行情服务协议可达(C# 数据源用)
echo -e "GET SHFE.cu2609\n" | nc 127.0.0.1 7751 # 返回价格
echo -e "GET SHFE.cu2609C46000\n" | nc 127.0.0.1 7752 # 返回期权价格
# ③ 数据库写入
# 注意:quotes_realtime/quotes_option_realtime 的 datetime 列写入的是 UTC 时间(与天勤脚本一致),
# 而服务器 now() 是北京时间(Asia/Shanghai),直接 now()-interval 会查不到刚写入的行!
# 正确写法:UTC 窗口,或 datetime + interval '8 hours' > now()
psql -h 10.10.44.35 -U postgres -d futures -c \
"SELECT symbol, last_price, datetime FROM quotes_realtime WHERE datetime > now()-interval '8 hours' ORDER BY datetime DESC LIMIT 5;"
psql -h 10.10.44.35 -U postgres -d futures -c \
"SELECT symbol, last_price, implied_volatility, greeks_delta FROM quotes_option_realtime WHERE datetime > now()-interval '8 hours' ORDER BY datetime DESC LIMIT 5;"
# ④ 六所覆盖确认(各交易所至少一个合约;无夜盘品种——CFFEX 股指、GFEX——夜盘时段无新行情属正常)
psql -h 10.10.44.35 -U postgres -d futures -c \
"SELECT DISTINCT split_part(symbol, '.', 1) FROM quotes_realtime WHERE datetime > now()-interval '8 hours';"
# 期望: CFFEX / SHFE / DCE / CZCE / INE / GFEX 六个都有(日盘时段查)
# ⑤ 客户端:设置页行情方案选 AUTO(CTP 优先,天勤兜底)→ 期货/期权列表看最新价4. 覆盖范围
| 数据 | 六所(CFFEX/SHFE/DCE/CZCE/INE/GFEX) | 说明 |
|---|---|---|
| 期货行情 | ✅ 全订阅 | ctp_futures_pg.py |
| 期权行情 | ✅ 全订阅 | ctp_options_pg.py(从 options 表加载合约) |
| 期权 Greeks/IV | ✅ 自算 | BS 定价(options_utils,与天勤同一套) |
| 仓单/持仓排名 | 不涉及 | 走 Tushare 数据服务同步,与行情源无关 |
5. 已知限制
- CFFEX 指数期权(IO/HO/MO):天勤版用 put-call 平价合成指数作标的 S,CTP 版先用标的期货价兜底(合成指数需同价配对,逐 tick 回调下未实现);
- 行情断线:脚本重启/网络异常时自动重连(openctp-ctp 内部);配合客户端 AUTO 模式自动回退天勤;
- 同一交易所不要同时跑天勤 + CTP 两套写库(双写无意义且占空间),切换前先停旧脚本。
6. 常见故障排查
症状:日志显示"登录成功 + 订阅 N 个合约",但 7751/7752 返回 NONE、数据库无新行
- 根因(2026-08-20 修复):openctp/SimNow 行情前置返回的 tick
ExchangeID是空字符串, 旧代码if exchange not in EXCHANGE_MAP: return把所有 tick 静默丢弃(日志无任何报错)。 修复:订阅时从数据库保存{合约代码: 交易所}映射(load_futures_contracts返回 dict), 空 ExchangeID 时用映射补全,再兜底infer_exchange()(品种前缀 → 交易所)。 - 关联修复:
_flush()内global pg_conn指向模块级,而pg_conn原在main()局部定义—— tick 打通后首次 flush 报name 'pg_conn' is not defined。已在main()顶部加global pg_conn。
症状:期权脚本日志报 numeric field overflow ... precision 20, scale 4
- 根因:openctp 的
AveragePrice/Turnover等字段可能返回异常大值(≥10^16), 而quotes_option_realtime的 numeric(20,4) 列上限是 10^16。已加clean_number()数值清洗 (NaN/Inf/超界→截断或丢弃),期货脚本同加clamp_db_number()。
验证技巧:UTC/北京时区混存导致 now()-interval 过滤失效——CTP 与天勤写 UTC、 外盘(KQD@)写行情所在市场时间。排查实时行情一律用 UTC 窗口 (如 datetime >= now() - interval '8 hours'),别用 5 分钟窗口。
7. 回退天勤
bash
# 停 CTP 脚本(Ctrl+C)→ 启动天勤脚本
python script/tq_futures_pg.py
python script/unified_options_main.py
# 设置页行情方案改回 TQ五、接入新的数据源(编写插件/网关)
- 实现协议:监听 TCP 端口,响应
GET {symbol}\n; - 在设置页添加数据源(Host/Port/优先级),保存;
- 系统监控页「行情数据源」区可看到在线状态;
- 数据写入:若插件同时把行情批量写入
quotes_realtime/quotes_option_realtime, 主程序在数据源全部不可用时仍能读到历史最新价。
参考:src/Futures.Core/Data/QuoteServerDataSource.cs(C# 客户端适配器)、 src/Futures.Core/Data/QuoteDataSourceRegistry.cs(注册表)。
六、优先级与降级
数据源1(Priority=0) → 数据源2(Priority=1) → ... → PostgreSQL 兜底- 任一数据源返回有效价即停止;
- 全部数据源不可用 → 读 PG
quotes_realtime(期货)→quotes_option_realtime(期权)最新价; - PG 也无数据 → 返回空(页面显示无行情/旧数据)。
七、合规说明
- 行情数据版权归各数据源提供方所有,使用方(用户/运营方)自行确认授权;
- 主程序不随发布包捆绑任何第三方行情采集器(默认不含
py_scripts实时采集 exe), 由用户在首启向导/系统监控页按需下载安装或配置远程网关。