Skip to content

行情数据源接入协议(档位 B:多源解耦)

一、背景

主程序(Futures.Desktop)不绑定任何特定第三方行情源。实时行情通过「行情数据源」接入: 任何实现下述文本协议的服务(本地采集器 / 远程行情网关 / 自定义插件)都可注册为一个数据源。

设计目标:

  • 数据源授权责任归数据源提供方(用户/服务器),主程序只负责协议与展示;
  • 多源共存:按优先级依次尝试,全部失败后回落到 PostgreSQL 历史最新价;
  • 现有业务代码零改动(统一走 IRealtimePriceCache 接口)。

二、协议(极简单行文本)

请求:   GET {symbol}\n
响应:   {price}\n       有效价格(十进制,>0)
        NONE\n          该合约暂无行情(协议可达)

实现要点:

  • TCP 流式,单次请求单行响应,响应后服务端可关闭连接;
  • symbol 为合约代码(如 au2608DCE.m2609IO2603-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显示名(设置页/监控页)
Host127.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-ctp

script/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:40011

2. 启动(两个脚本,各自占用一个行情源端口)

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

五、接入新的数据源(编写插件/网关)

  1. 实现协议:监听 TCP 端口,响应 GET {symbol}\n
  2. 在设置页添加数据源(Host/Port/优先级),保存;
  3. 系统监控页「行情数据源」区可看到在线状态;
  4. 数据写入:若插件同时把行情批量写入 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), 由用户在首启向导/系统监控页按需下载安装或配置远程网关。

瓯衍期货分析系统 · 温州博益网络科技有限公司