Learning
VOL. VII · NO. 61 · OTC Derivatives · 19 JUL 2026

Market Data Pipeline

OTC 衍生品 · 19 JUL 2026 · 15 min read · 2,804 words
· · ·

ODTS 12 — 行情数据:数字从哪里来

业务问题

系统产生的每一个价格——每一个 MTM、每一笔保证金、每一份风险报告——都始于外部世界的一个数字:

系统需要:              来源:
  股票价格              → 交易所 (HKEX, SSE, SZSE)
  汇率                  → 央行定盘 / Bloomberg / Reuters
  波动率曲面 (Vol surface) → 从期权价格算得或供应商 (Bloomberg)
  利率曲线 (IR curve)   → SHIBOR, TONA, HIBOR 定盘
  股息预测              → 供应商 (Bloomberg) 或人工估计
  公司行为              → 供应商或人工录入

这些数字中任何一个错了,下游数据全部错。 下午 4 点一个错误的价格意味着整个 EOD 批处理产出垃圾数据。

行情数据的杠杆效应: 行情数据的一个错误会被系统以杠杆方式放大。假设一个期权 book 有 500 笔交易,每笔每日 MTM 依赖于 vol surface、股票价格、利率曲线三个数据源。一个 vol surface 错误会同时影响全部 500 笔交易的 MTM——单笔偏差可能只有 0.1%,但汇总到 P&L 就是 500 万 RMB 的差距。这就是为什么行情数据团队只有 1–2 个人,但他们出问题时的 P&L 影响可以超过整个交易团队一天的利润。

行情数据流

外部供应商
(Bloomberg, Reuters, 交易所)      ───→ eds-price-server


                                      PriceCache (内存)
                                      TodayContractRaInfo (DB)
                                      VolSurface 表
                                      IRCurve 表


                                  ┌───────────┼───────────┐
                                  ▼           ▼           ▼
                               hedging-as  eds-web-app  EOD batch
                               (定价)       (展示)       (估值)

第 1 步:接入 (Ingestion)

~/odts1/eds-web-app/src/com/cicc/edsBoot/market/
├── MarketDataService.java        — 编排所有数据获取
├── BloombergDataService.java     — Bloomberg API 适配器
├── ReutersDataService.java       — Reuters API 适配器
├── DividendService.java          — 股息数据
├── CorporateActionService.java   — 公司行为
└── ManualOverrideService.java    — 人工价格覆盖

eds-price-server(独立进程)跑在有 Bloomberg 访问权限的服务器上。它与 Bloomberg Server API (BSAPI) 建立 TCP socket 长连接,订阅需要的数据源。订阅按 ticker 维度管理——每只股票/期权对应一个订阅。

eds-price-server 内部结构:
  PricingEngine.java        — 定价引擎入口
  OptionCalculator.java     — 期权定价计算
  PriceUtils.java           — 价格工具函数
  gpricingengine/           — Greeks 定价引擎
    ├── GPricingEngine.java  — Greeks 引擎入口
    ├── MCPricing.java       — Monte Carlo 定价
    ├── MCSparkSimulator.java — Spark 分布式 MC
    └── MCValuationSparkUtil.java — MC 汇总

接入的数据流分两条:

实时流 (Real-time Feed): 交易时段 (09:30–16:00) 通过 Bloomberg 的实时数据推送,每 tick 更新 PriceCache 内存。PriceCache 用 Java ConcurrentHashMap 维护最新价格,每条记录有时间戳和来源标记。

盘后快照 (EOD Snapshot): 收盘后 16:00–17:00,系统拉取全量 EOD 快照,包括当日所有交易品种的收盘价、波动率曲面和利率曲线。快照写入数据库持久化,供 EOD 批处理使用。

第 2 步:缓存 (Caching)

hedging-as/src/com/cicc/pricing/dataCenter/
├── PriceCache.java              — 股票/指数价格缓存
├── VolSurfaceManager.java       — 波动率曲面缓存
├── IRCurveManager.java          — 利率曲线缓存
└── FixingManager.java           — 外汇定盘缓存

每个缓存有自己的刷新策略:

  • PriceCache: 盘中每 30 秒刷新一次,EOD 时刷新一次。实时模式下订阅 Bloomberg tick 推送,缓存键为 <exchangeCode_stockCode>(如 SH_600519
  • VolSurface: 每日刷新一次(收盘后),或按需刷新。曲面数据经过插值后缓存到 VolSurfaceManager,供定价引擎按 underlyingId + strikeRatio + tenor 查取
  • IRCurve: 每日刷新一次(SHIBOR 定盘后 11:00)。缓存 SHIBOR 各期限(O/N, 1W, 1M, 3M, 6M, 1Y)的利率
  • Fixing: 09:15 央行发布定盘价时刷新,缓存当日人民币汇率中间价

第 3 步:持久化 (Storage)

价格保存在以下表中:

  • TodayContractRaInfo — 合约级估值结果
  • PriceData — 原始价格时间序列
  • VolSurface — 波动率曲面网格
  • IRCurve — 利率曲线关键点

数据供应商策略与降级

系统使用 Bloomberg 作为主要数据源,Reuters 作为备份。当 Bloomberg 不可用时,系统自动尝试从 Reuters 获取数据。

价格请求流程:
  请求股票价格

    ├─ BloombergDataService.getPrice(ticker)
    │   ├─ 成功 → 返回 Bloomberg 价格
    │   └─ 失败 → 转 Reuters

    ├─ ReutersDataService.getPrice(ticker)
    │   ├─ 成功 → 返回 Reuters 价格
    │   └─ 失败 → 转 EOD 缓存

    └─ PriceCache.getLastEOD(ticker)
        ├─ 昨日收盘价存在 → 返回,标记为"陈旧的"
        └─ 不存在 → 返回错误,交易不能定价

这种降级策略(新鲜→备份→陈旧的)确保系统尽可能继续运行,但产生的价格质量不同:

价格来源新鲜度适用场景风险
Bloomberg 实时秒级盘中盯市最佳质量
Reuters 实时秒级Bloomberg 故障与 Bloomberg 有 0.01-0.1% 差异
EOD 缓存(昨日)15 小时极端故障MTM 可能与市场严重偏离
人工覆盖手动以上全部无效依赖操作员判断

2020 年台风实例: Bloomberg 服务器断电,Reuters 也因为同一区域网络故障失效。系统降级到 EOD 缓存,交易台用昨日收盘价看了 6 小时的 MTM。下午交易台报告”+5000 万 P&L”,实际 EOD 重新定价后是”-2000 万”——无人知晓,直到第二天。

第二天早上交易台发现 7000 万的 P&L 反转后,早会 30 分钟都在讨论”我们盘中到底该信什么”。讨论结果是加了一个手动标记:当 eds-price-server 处于降级模式时,操作员在管理后台切换一个”行情降级”开关,前端显示橙色 banner 提示。这个方案不漂亮——它依赖操作员在压力下记住打开关。但在这个场景下,一个手动标记比没有标记好。

后续验证: 这个”行情降级”开关在之后 2 年中被用到至少 3 次(2021 年另一次台风、Bloomberg 服务器硬件故障、网络割接)。每次都起作用——交易台看到 banner 就知道盘中 MTM 不可靠。但也有一次操作员忘了打开关,直到前端显示”行情降级”的代码本身出了 bug(某个条件判断写反了),5 个月后才被发现。这说明数据质量的监控本身也需要被监控——而交易台一直没有全天候的数据质量监控系统。

更隐蔽的降级问题:部分数据到达

最危险的场景不是 Bloomberg 完全不可用——而是只有部分数据到达:

Bloomberg → 股票价格正常(A 类) ✓
          → 波动率曲面没有更新(失败) ✗
          → 交易台用昨日的 vol surface
          → 市场波动率已变化 50%
          → 期权定价产生系统性偏差

系统没有一个全局的”所有数据新鲜度状态”仪表盘——定价引擎不知道它用的是过时的 vol surface。

Tick 数据处理

每笔新报价在进入 PriceCache 之前经过三个检查:

第 1 步:时间顺序检查
  if (tick.timestamp <= cache.lastTimestamp) → 丢弃(迟到数据)

第 2 步:价格合理性检查
  if (tick.price / cache.lastPrice > 1.2 || tick.price / cache.lastPrice < 0.8)
    → 标记可疑,需要人工复核
  (容差 20%,这是硬编码阈值)

第 3 步:更新缓存
  cache.put(ticker, tick.price, tick.timestamp, source)
  → PriceCache 用 AtomicReference 保存最新价格,保证线程安全

但从 50 涨到 55(10% 变动)在阈值内,仍可能是坏 tick。阈值是静态的——没有动态调整(如按历史波动率自适应)。

什么会出错

1. 过期价格(最常见)

症状:交易 MTM 三天没变
原因:行情数据源断了
检测:PriceCache 过期时间戳超过 24 小时
修复:重启 eds-price-server,或手动输入价格

真实故事(2020 年): 台风天 Bloomberg 服务器断电。6 小时没有任何价格进来。交易台靠昨日收盘价看 MTM。交易台以为有 5000 万 P&L——实际亏损 2000 万。没人知道,直到第二天。

2. 错误价格(单点)

症状:某只股票 MTM 变化 +200%(其他都正常)
原因:交易所发送了一个坏 tick(如 500 而不是 50)
检测:价格变动超过阈值(如 >20%)
修复:在 ManualOverrideService 中人工覆盖

系统有一个价格合理性检查 (price sanity check)

if (newPrice / lastPrice > 1.2 || newPrice / lastPrice < 0.8) {
    log.warning("检测到价格跳变: " + stock + " 从 " + lastPrice + " 到 " + newPrice);
    // 标记为人工复核,不要自动使用
}

但只有”明显”错误会被捕获。从 50 涨到 55(10% 变动)在阈值内,但仍可能是坏 tick。

3. 缺失波动率曲面

症状:期权价格不对(太高或太低)
原因:某个行权价/到期日的 vol surface 没有更新
检测:VolSurface 查询返回零或从很远的数据点插值
修复:手动更新 vol surface,或检查 Bloomberg vol 数据

Vol surface 每天收盘后算一次。如果曲面构建器在 16:30 失败了(因为 Bloomberg 当时挂了),期权交易台直到修复完成都没有 vol surface 可用。

Vol surface 异常的静默损失: 2020 年中某次,vol surface 构建器连续 3 天收盘后失败(Bloomberg 数据源不稳定,日志显示 connection timeout)。期权 book 的 MTM 在这 3 天里基于 3 天前的 vol surface。期间中证 500 realised vol 从 18% 涨到 24%。对于一个 vega 约 300 万 RMB/vol 点的期权 book:

  • 每天 MTM 偏差:300 万 × 6 = 1800 万 RMB
  • 3 天累计 P&L 不确定性:~5400 万 RMB
  • 第 4 天修复后,显示 P&L 与实际相差 ~4000 万

这不是系统停机。系统一直在运行,精准地计算错误的价格。没有警报,没有红色闪烁。第 4 天才由一个 trader 发现——他注意到一个即将到期的期权 delta 对冲成本异常,花了 3 小时手动排查才找到根因。这种”静默错误”比系统崩溃更危险:系统崩溃时大家都知道有事发生,静默错误只有最细心的 trader 才能发现。

4. 公司行为未应用

症状:拆股后某只股票的 TRS 头寸没有调整
原因:公司行为未处理
检测:头寸名义本金与股票价格不匹配
修复:手动运行 CorporateActionService

这是代价最高的数据故障类型。如果发生 1:10 拆股而系统没有调整:

  • TRS 客户被记入 10 倍股数(错误)
  • 融资计算错误(名义本金不对)
  • 修复错误需要一个回溯调整交易

2021 年恒生指数调仓: 汇丰被从恒指移除、腾讯加入时,50+ 个 TRS 合约需要调整。每个调整需要人工复核。运营团队花了 3 天。那 3 天里,TRS P&L 不可靠。

5. 日期边界问题(最难调试)

行情数据中最微妙的 bug 来自日期边界

EDSConst.java 中的交易日常量:
  TODAY_DATE = new Date()
  LAST_EOD_DATE = TODAY_DATE - 1(如果是工作日)

场景:T+0 定价使用 EOD 市场数据
  问题:市场数据日期与交易日期不匹配
  结果:定价用了昨日波动率 × 今日即期 → 价差偏离

根本原因:eds-price-serverhedging-as 各自维护自己的日期上下文。如果 EOD 批处理跨天执行,日期上下文不同步会导致定价数据引用不一致。

代码位置

数据类型来源缓存存储
股票价格Bloomberg/ReutersPriceCache.javaPriceData
指数价格交易所PriceCache.javaPriceData
汇率央行/BloombergFixingManager.javaFixingData
Vol surface计算(供应商)VolSurfaceManager.javaVolSurface
利率曲线SHIBOR 定盘IRCurveManager.javaIRCurve
股息BloombergDividendService.javaDividendData
公司行为Bloomberg/人工CorporateActionService.javaCorpAction

定价引擎源码位置:

组件路径
定价引擎入口eds-price-server/src/.../pricingengine/PricingEngine.java
期权计算器eds-price-server/src/.../pricingengine/OptionCalculator.java
Greeks 引擎eds-price-server/src/.../gpricingengine/GPricingEngine.java
Monte Carloeds-price-server/src/.../gpricingengine/pricing/MCPricing.java
交易日常量eds-price-server/src/.../common/EDSConst.java
交易日历工具eds-price-server/src/.../common/TradeDate.java
价格工具eds-price-server/src/.../util/PriceUtils.java
产品定义eds-price-server/src/.../instruments/(Vanilla, Barrier, TRS, etc.)

如何调试行情数据问题

1. 检查价格:
   → curl /api/v1/market-data/price/{stockCode}
   → 价格正确? 是 → 查估值。否 → 查来源。

2. 检查来源:
   → eds-price-server 日志(是否收到数据?)
   → Bloomberg 终端(Bloomberg 正常工作吗?)
   → 人工覆盖(是否有人工覆盖?)

3. 修复:
   → 安排一次价格刷新
   → 或输入人工覆盖
   → 对受影响合约重新运行 EOD 估值

关键时间点

行情数据的时间点是最微妙的 bug 来源:

事件时间系统动作
央行人民币定盘09:15 CSTFixingManager 刷新
SHIBOR 定盘11:00 CSTIRCurveManager 刷新
股票市场开盘09:30 CST实时价格开始流入
股票市场收盘15:00 CSTEOD 快照
Vol surface 构建17:00 CSTVolSurfaceManager 重建曲面
EOD 批处理开始18:00 CST使用所有已刷新数据

如果 vol surface 在 17:00 构建,但行情数据在 15:00 断过,vol surface 会用过期期权价格来计算。系统不知道——它检查的是”数据存在”而不是”数据足够新鲜”。

行情数据的真实成本

行情数据不是免费的。交易台每年在这上面的支出是一笔不小的固定成本:

成本项年费用(估算)说明
Bloomberg 终端(4–6 台)120–250 万 RMB每台 ~$2,000-3,000/月;交易、风控、运营各需访问
Reuters Eikon(2–3 台)30–60 万 RMB备份供应商,必须保持活跃订阅才能作为降级来源
交易所行情授权100–200 万 RMB沪深港通等——每只股票/指数需交易所授权才能展示与使用
增值数据包20–50 万 RMBVol surface、股息预测、公司行为等 Bloomberg 附加包
专线与基础设施10–30 万 RMBBloomberg 专线(leased line)、eds-price-server 服务器、网络带宽
数据运维人力1–2 人 × 50–80 万每日检查数据完整性、处理人工覆盖、排查数据异常
合计约 300–600 万 RMB/年在年化 ~4 亿 P&L 中占比 <2%,但在业务下滑年份仍然固定

数据成本的一个趋势是持续上升。A 股市场每增加一个新品种(科创板、北交所、ETF 期权),就多一笔交易所授权费。境外市场扩展(港股通、美股、债券通)带来的是外币计价的供应商账单。交易台 2017 年的数据成本大约是现在的 60%——6 年涨了近一倍。在牛市里这不是问题;在熊市里,数据授权费是第一批被财务审查的预算项。

此外还有一个隐形成本:行情数据排错的时间。 当 EOD 批处理产出异常 P&L 时,第一步永远是”检查行情数据”。这个排错时间不固定——运气好 30 分钟找到问题,运气不好(如 vol surface 失败了但没有明显日志)需要 3 小时。一个三人交易台一年花在行情排错上的总时间大约是 2–3 人月。这笔时间如果可以交易,就是 10–20 万 RMB 的机会成本。

关键文件

组件路径
定价引擎入口eds-price-server/src/.../pricingengine/PricingEngine.java
定价引擎结果eds-price-server/src/.../pricingengine/PricingResults.java
Greeks 引擎eds-price-server/src/.../gpricingengine/GPricingEngine.java
MC 定价eds-price-server/src/.../gpricingengine/pricing/MCPricing.java
MC Payoffeds-price-server/src/.../gpricingengine/pricing/MCPayOffSeries.java
交易日常量eds-price-server/src/.../common/EDSConst.java
产品定义eds-price-server/src/.../instruments/(含 Vanilla, Barrier, TRS, Loan 等)

教训: 这就是为什么行情数据是 60% 的”定价 bug”的原因。定价代码是正确的;定价代码的输入是错的。下次看到定价差异,先检查行情数据。系统有供应商降级策略,但没有全局数据新鲜度监控——部分数据到达的降级场景是最危险的。