Market Data Pipeline
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-server 和 hedging-as 各自维护自己的日期上下文。如果 EOD 批处理跨天执行,日期上下文不同步会导致定价数据引用不一致。
代码位置
| 数据类型 | 来源 | 缓存 | 存储 |
|---|---|---|---|
| 股票价格 | Bloomberg/Reuters | PriceCache.java | PriceData 表 |
| 指数价格 | 交易所 | PriceCache.java | PriceData 表 |
| 汇率 | 央行/Bloomberg | FixingManager.java | FixingData 表 |
| Vol surface | 计算(供应商) | VolSurfaceManager.java | VolSurface 表 |
| 利率曲线 | SHIBOR 定盘 | IRCurveManager.java | IRCurve 表 |
| 股息 | Bloomberg | DividendService.java | DividendData 表 |
| 公司行为 | Bloomberg/人工 | CorporateActionService.java | CorpAction 表 |
定价引擎源码位置:
| 组件 | 路径 |
|---|---|
| 定价引擎入口 | eds-price-server/src/.../pricingengine/PricingEngine.java |
| 期权计算器 | eds-price-server/src/.../pricingengine/OptionCalculator.java |
| Greeks 引擎 | eds-price-server/src/.../gpricingengine/GPricingEngine.java |
| Monte Carlo | eds-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 CST | FixingManager 刷新 |
| SHIBOR 定盘 | 11:00 CST | IRCurveManager 刷新 |
| 股票市场开盘 | 09:30 CST | 实时价格开始流入 |
| 股票市场收盘 | 15:00 CST | EOD 快照 |
| Vol surface 构建 | 17:00 CST | VolSurfaceManager 重建曲面 |
| 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 万 RMB | Vol surface、股息预测、公司行为等 Bloomberg 附加包 |
| 专线与基础设施 | 10–30 万 RMB | Bloomberg 专线(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 Payoff | eds-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”的原因。定价代码是正确的;定价代码的输入是错的。下次看到定价差异,先检查行情数据。系统有供应商降级策略,但没有全局数据新鲜度监控——部分数据到达的降级场景是最危险的。