Pricing Engine Architecture
ODTS 23 — 定价引擎架构:Greeks 是如何算出来的
业务问题
每笔场外衍生品交易都需要一个价格。但”价格”在不同语境下含义不同:交易簿记 (trade booking) 时是客户支付/收到的权利金 (premium),计算于交易时 T+0;每日 P&L 时是盯市价值 (mark-to-market),计算于 EOD 批处理;保证金 (margin) 时是需要多少抵押品,计算于 EOD 批处理和次日早上;风险 (risk) 时是持仓对市场波动的敏感度,计算于 EOD 批处理;提前了结 (unwind) 时是平仓要多少成本,按需计算。所有这些都使用相同的定价引擎,但参数不同。
两个定价引擎
系统中有两个定价引擎,不和谐地共存着。旧引擎 (JFinal) 位于 hedging-as/,语言 Java + JFinal,用于每日风险和 EOD 批处理。新引擎 (Spring Boot) 位于 eds-web-app/,语言 Java + Spring Boot,用于交易簿记、保证金和确认。
为什么有两个引擎?系统从 JFinal 迁移到 Spring Boot,但定价代码没有被迁移。JFinal 引擎是风险数字的”真相源” (source of truth)。Spring Boot 引擎有更新的功能,但数值结果不同。这意味着同一笔交易根据哪个引擎计算会有不同的净现值 (NPV)。
JFinal 定价引擎的工作原理
计算流程分为 6 步:
第 1 步:加载交易。 ContractQueryHelper.queryContract(CONTRACT_ID) 执行 SQL 从 EDS_contract 和 EDS_contractElementValue 表中读取全部 40+ 个 EAV 属性行,构建 Protobuf Contract 对象。
第 2 步:加载合约结构。 ContractLegAction.buildLegs(contractId) 构建 Protobuf ContractLegStruct 对象,包含 legs(支)和支付计划 (payment schedule)。
第 3 步:加载市场数据。 获取今日市场快照的即期价格 (spot price)、波动率曲面 (vol surface)——对应行权价/期限的隐含波动率、利率曲线 (interest rate curve)、股息率 (dividend yield)。
第 4 步:选择定价模型。 根据产品类型选择:香草期权 (VANILLA_OPTION) → Black-76;障碍期权 (BARRIER) → Monte Carlo;美式期权 (AMERICAN) → Bjerksund-Stensland;奇异期权 (EXOTIC) → Finite Difference;TRS → TrsPricer;互换 (SWAP) → SwapPricer。
第 5 步:计算 NPV + Greeks。 调用 model.calculateNpv(params)、model.calculateDelta(params)、model.calculateGamma(params)、model.calculateVega(params) 等。
第 6 步:持久化到 EOD 表。 INSERT INTO EDS_CTRCONTRACTEOD (CONTRACTID, EODDATE, OPTIONNPV, DELTA, ...)
Black-76 模型(香草期权)
最常用的模型。核心公式与 Black-Scholes 类似,但用于期货/远期而非即期价格。输入包括远期价格 (F)、行权价 (K)、剩余期限 (T)、无风险利率 (r) 和隐含波动率 (v)。对看涨期权:e^(-rT) * (F * N(d1) - K * N(d2))。对看跌期权:e^(-rT) * (K * N(-d2) - F * N(-d1))。其中 d1 = (ln(F/K) + (v²/2) * T) / (v * √T),d2 = d1 - v * √T。
Greeks 是什么
| Greek | 定义 | 衡量什么 | 业务用途 |
|---|---|---|---|
| Delta (Δ) | dV/dS | 标的每波动 1 元,价格变化多少 | 对冲比率 (hedge ratio) |
| Gamma (Γ) | d²V/dS² | 标的每波动 1 元,Delta 变化多少 | 再平衡频率 |
| Vega (ν) | dV/dσ | 波动率每变化 1%,价格变化多少 | 波动率风险 |
| Theta (Θ) | dV/dt | 每天时间价值损耗 | 时间衰减成本 |
| Rho (ρ) | dV/dr | 利率每变化 1%,价格变化多少 | 利率风险 |
前线的交易员和风控部门每天就看这几个数字。Delta 告诉你需要买/卖多少股票来对冲;Gamma 告诉你多久需要对冲一次;Vega 告诉你波动率风险敞口。
定价争议的真实案例
2021 年发生过一次著名的跨部门争议:交易台和风控团队对同一笔雪球产品定价相差 38 万(名义本金 5000 万,差异约 0.76%)。
交易台用 Spring Boot 引擎:
- 波动率曲面:当日 Bloomberg 实时曲线
- NPV = 10,238,000
风控用 JFinal 引擎:
- 波动率曲面:前一日 EOD 曲线(因为 EOD 批处理用快照数据)
- NPV = 9,858,000
差异:380,000
根因: 不是模型代码不同——是市场数据快照时间不同。交易台用了”now”的市场数据,风控用了”昨天 EOD”的数据。对于雪球这类高 Vega 产品,波动率曲面的微小变化就可以产生数十万的 NPV 差异。
解决的代价: 量化团队花了 3 周确认两边的模型代码一致,最后发现是数据时间不同。这个案例说明:定价争议的一半不是因为数学错了,而是因为”用什么数据”没有定义清楚。
Greeks 的日内过时问题
Greeks 计算开销很大。系统通过在 EDS_CTRCONTRACTEOD 表中缓存 Greeks 结果来解决这个问题。昨晚 EOD 批处理的 Greeks 用于今日的保证金催缴。
问题是:如果标的物价格在日内波动 5%,缓存的 Greeks 就过时了。保证金催缴可能出错,对冲比率可能不准。
真实后果:
某 TRS 客户持仓 5000 万,上午标的物下跌 3%
→ 实际需要增加的对冲量:Delta × 3% = 约 150 万
→ 但系统用的 EOD Greeks 没更新
→ 交易台按照 EOD Delta 做对冲,少对冲了约 150 万敞口
→ 标的物继续下跌 2% 后交易台才被发现 Delta 不准确
→ 额外损失约 3 万(未被对冲的部分)
临时方案:对于高波动标的,风险团队在中午手工重新运行 Greeks 计算。
新定价引擎(Spring Boot)
新引擎使用了数学公式的 Java 移植版。但它和旧引擎产生了略微不同的结果:对同一笔交易,旧引擎 NPV 为 1,234,567.89 元,新引擎为 1,234,001.23 元,差异 566.66 元(0.05%)。
为什么有差异?三个原因:累积正态分布 (cumulative normal distribution) 的不同实现(多项式近似变体不同);剩余期限处理中工作日 vs 日历日的不同;波动率曲面的不同插值方法(线性 vs 样条)。
业务影响:交易员信任旧引擎,因为”它已经跑了 5 年”。新引擎在数字完全匹配之前无法取代旧引擎。但找到 0.05% 的差异需要数周的调试。这听起来像是小事,但在衍生品领域,0.05% 乘以几十亿的名义本金就变成了百万级的差异。
波动率曲面的”选结果”问题
因为波动率曲面是离散点插值的,选择不同的插值方法相当于选择了不同的定价结果。这在实践中导致了一个微妙的问题:
某交易台想给客户报价一笔 6 个月到期的香草期权:
行权价/即期比 = 1.03
曲面数据点:1.00 (25%) 和 1.05 (28%)
线性插值:25% + (1.03-1.00)/(1.05-1.00) × (28%-25%) = 26.8%
样条插值:27.5%(因为样条平滑了上升趋势)
差异:0.7 个 vol 点
NPV 影响:名义本金 5000 万 × 0.7% × 期限调整 = 约 17.5 万
业务含义: 交易员可以说”我用线性插值”,也可以说”我用样条插值”——取决于哪个结果对自己有利。在缺乏统一的定价治理 (pricing governance) 的情况下,同样的报价可以用不同的方法得到不同的数字。这不是欺诈,是灰色地带——也是为什么大型银行有专门的定价模型治理团队 (Model Validation / Model Risk Management) 来审批哪些模型和插值方法可以用。
波动率曲面问题
定价引擎需要每笔交易的隐含波动率 (implied volatility)。隐含波动率随标的(每只股票有自己的 vol)、行权价(越高行权价 → 不同 vol——“波动率微笑” vol smile)和期限(不同到期日 → 不同 vol——“波动率期限结构” vol term structure)而变化。
波动率曲面存储在 EDS_volSurface 表中,包含 underlyingId、surfaceDate(快照日期)、strikeRatio(行权价/即期价比,如 0.8, 0.9, 1.0, 1.1, 1.2)、tenor(剩余天数)和 volatility(隐含波动率)。定价时,引擎在曲面点之间进行插值:比如交易的行权价/即期比为 1.05,剩余 90 天,曲面有 4 个相邻点,取平均值得到约 28.75% 的波动率。
代码库中存在 SVI (Stochastic Volatility Inspired) 模型的引用(ResponseVolatility.java 中有 //private SVI svi 注释),表明 SVI 参数化曾经被考虑但未启用。当前系统使用线性插值,但更精确的样条插值 (cubic spline) 在离线计算中使用。不同的插值方法(线性、三次样条、SVI)对同一笔交易给出不同的波动率——这是定价争议的主要来源之一。
Monte Carlo 模拟
为什么需要 MC
障碍期权、雪球、ACS (AutoCallable Swap) 等路径依赖型产品无法用 Black-76 等解析公式定价——它们的收益取决于标的物经历的整条路径,而不仅仅是终局价格。
MC 在 ODTS 中的实现
MCPricing.java(核心控制器)
└── AssetPaths.getAssetPaths() — 生成随机路径(几何布朗运动)
├── 每次模拟生成 N 条路径 (path count)
├── 每条路径从初始价格到到期日,按天走
└── 路径路径使用 Box-Muller 变换生成正态随机数
MCPayOffSeries.java — 对每条路径计算收益并汇总
MCValuationSparkUtil.java — 将模拟结果聚合为 NPV + Greeks
MC 路径生成中的关键参数:
每日步长:标的物价格的每日模拟
几何布朗运动公式:
S(t+dt) = S(t) * exp((r - q - σ²/2) * dt + σ * √dt * Z)
其中 Z 是标准正态随机数,由 Mersenne Twister 或类似 PRNG 生成
MC 的性能特征
模拟路径数:通常 10,000 - 100,000 条
路径步长:每日(障碍产品)或到期日(欧式产品)
单笔耗时:~100ms(10,000 路径)/ ~1s(100,000 路径)
EOD 扫描:100 笔 MC 产品 → 100s - 1000s → 需要分布式运行
这就是为什么 MC 产品只在 EOD 批处理中计算,不在交易时间实时计算。
方差缩减
代码中 MCPayOffSeries 和 MCValuationSparkUtil 实现了两种方差缩减技术:
-
对偶变量法 (Antithetic Variates):每个生成的随机数 Z 同时生成 -Z,产生两条路径,降低方差约 40-60%。代价是路径数翻倍,但同样的精度需要的原始路径数更少。
-
控制变量法 (Control Variates):用解析可解的产品(如香草期权)价值作为基准,校准 MC 结果。尚未在代码中完全实现。
MC 的数值风险
MC 模拟的结果不是精确值——是有统计误差的估计值。
95% 置信区间:NPV ± 1.96 * σ / √N
10000 路径:σ_NPV = 2.0 → 置信区间 ±0.0392 (3.9%)
50000 路径:σ_NPV = 2.0 → 置信区间 ±0.0175 (1.75%)
100000 路径:σ_NPV = 2.0 → 置信区间 ±0.0124 (1.24%)
业务影响:如果 MC 噪声导致 NPV 波动 2%,交易员可能看到一个完全随机的 PnL 摆动。这就是为什么对 MC 产品会额外运行若干次并取平均值。
有限差分法 (Finite Difference / FD)
为什么需要 FD
FD 用于美式期权(可以提前行权)和其他含有最优停时 (optimal stopping) 特征的产品。MC 在处理提前行权时效率很低——它需要前向模拟路径+反向动态规划,计算量巨大。
FD 在 ODTS 中的实现
FD 实现在 eds-price-server/.../structuredproducts/fds/ 目录下:
PPDSFValue.java — 美式/百慕大期权定价 (PDE 求解器)
PPSFValue.java — 美式/百慕大期权定价
NPPValue.java — 含路径依赖的 FD
LOANValue.java — LOAN 产品的 FD 定价
TRSValue.java — TRS 的 FD 估值
Crank-Nicolson 格式
系统使用 Crank-Nicolson 有限差分法 求解 Black-Scholes PDE:
∂V/∂t + ½σ²S² ∂²V/∂S² + (r - q)S ∂V/∂S - rV = 0
求解网格:
├── S 方向:200-500 个网格点(从 0 到 3-5 倍行权价)
├── t 方向:每交易日一个时间步
└── 边界条件:
├── S=0: V=0(看涨)或 V=K(看跌)
└── S→∞: V=S(看涨)或 V=0(看跌)
FD 的美式期权处理
在每个时间步,FD 求解器检查提前行权条件:
在每个网格点 (S_i, t_j):
V(S_i, t_j) = max(V_continuation(S_i, t_j), payoff(S_i))
如果 V_continuation < payoff → 提前行权
这个”max”操作引入了非线性——这就是为什么美式期权定价比欧式期权慢 2-3 倍。
波动率插值方法
波动率曲面是有限的数据点集合(比如 5 个行权价 × 7 个期限 = 35 个点),而定价时需要任意行权价/期限的波动率。插值方法的选择对定价有显著影响。
| 插值方法 | 原理 | ODTS 使用场景 | 价格影响 |
|---|---|---|---|
| 线性插值 (Linear) | 相邻点之间直线连接 | EOD 批处理(默认) | 基准 |
| 三次样条 (Cubic Spline) | 分段三次多项式,保证一阶/二阶导数连续 | 离线/风险管理 | 比线性高 0.1-0.5% |
| SVI (随机波动率启发) | 参数化 volatility smile 曲线 | 代码中引用但未启用 | N/A |
业务含义: 如果 EOD 系统用线性插值,风险团队用样条插值,每笔交易的 NPV 都会不同。交易台可以轻松地”挑选”有利于自己的插值方法。这就是为什么方法一致性比方法选择本身更重要。
双引擎定价问题
系统有时会同时在两个地方为同一笔交易定价。交易员 A 在新 Spring Boot 引擎中定价(前台),同时 EOD 批处理在旧 JFinal 引擎中为同一笔交易定价(风险),数字相差 0.1%。交易员的 P&L = 100,000,风险的 P&L = 99,900。谁是对的?没人知道。
交易员说”我定价是 10 万”,风险说”我们系统显示是 9.99 万”。差异(100 元)很小,但如果每天发生,就会累积。真正的解决方案是只有一个定价引擎,所有人都用。但实现这个需要干掉旧 JFinal 引擎,没人愿意承担这个责任——因为迁移期间任何一天的数字差异都可能导致交易员与风控之间的争吵。
模型校验和基准测试
ODTS 的 MC 和 FD 定价结果需要定期与基准 (benchmark) 比照:
内部基准:
─ 已知解析解的简单产品(如香草期权用 Black-76 解)
─ 历史回测——同一笔交易在不同日期的 NPV 变化是否合理
外部基准:
─ Bloomberg OVML/MC 定价
─ 与交易对手的确认书盯市价值对比
─ 差异超过阈值(如 2%)时触发争议
如果模型的 Greeks 与 Bloomberg 的 Greeks 差异超过容差,量化团队会介入调试——但修复可能需要几周时间,因为 CICC 使用的是自研 Java 实现,与 Bloomberg 的 C++ 实现在浮点运算顺序、正态分布近似、日期计数惯例等方面都存在差异。
关键文件
| 组件 | 路径 |
|---|---|
| Black-76 模型 | hedging-as/.../model/Black76Model.java |
| Monte Carlo 主类 | eds-price-server/.../gpricingengine/pricing/MCPricing.java |
| MC Payoff 序列 | eds-price-server/.../gpricingengine/pricing/MCPayOffSeries.java |
| MC 估值工具 | eds-price-server/.../gpricingengine/pricing/MCValuationSparkUtil.java |
| MC Spark 模拟器 | eds-price-server/.../gpricingengine/pricing/MCSparkSimulator.java |
| FD 定价(美式期权) | eds-price-server/.../structuredproducts/fds/PPDSFValue.java |
| FD 定价(TRS) | eds-price-server/.../structuredproducts/fds/TRSValue.java |
| 合约查询 | hedging-as/.../ContractQueryHelper.java |
| EOD 风险存储表 | eds-web-app/db/EDS/V1.8/EDS20171107170003_CREATE_CTCONTRACTEOD.sql |
| 波动率曲面表 | 迁移脚本中的 Schema |
| 新定价控制器 | eds-web-app/.../PricingController.java |
| 定价器接口 | hedging-as/.../model/Pricer.java |
| 资产路径生成 | eds-price-server/.../pricingengine/AssetPaths.java |
| 波动率参考 | eds-price-server/.../pricingengine/ResponseVolatility.java(SVI 引用) |
定价引擎建立在可靠的金融数学基础上(Black-76、Monte Carlo、Crank-Nicolson FD),但实现分散在两个代码库中,产生微妙不同的结果——而波动率插值方法的选择对价格的影响与模型本身一样大。