Pricing And Risk
ODTS 08 — 定价与风险:引擎如何工作
为什么需要这篇文档
定价引擎 (hedging-as) 是整个 ODTS 中智力密度最高的部分,也是所有开发者觉得最难 debug 的——不是代码逻辑复杂,而是数学不熟悉。这篇文档从业务直觉讲到代码结构。
1. 定价管线 (Pricing Pipeline)
每一个价格——无论实时报价还是 EOD 批量估值——都流经同样的五步管线:
Inputs → Payoff → Valuation → Greeks → Output
第一步:输入 (Inputs)
| 输入 | 来源 | 代码位置 |
|---|---|---|
| 标的价格 (spot) | eds-price-server → Reuters/Bloomberg | pricing/dataCenter/ |
| 波动率曲面 (vol surface) | eds-price-server → SurfaceBuilder | pricing/dataCenter/VolSurface.java |
| 利率曲线 (IR curve) | eds-price-server → CurveBuilder | pricing/dataCenter/IRCurve.java |
| 股息率 (dividend yield) | eds-price-server → CorpActions | service/contract/DividendHelper.java |
| 汇率 (FX rate) | eds-price-server | eds-price-server/ |
| 合约条款 | CtrContract 数据库表 | service/contract/model/ |
行情数据出错的业务后果: 定价引擎的准确性完全取决于输入数据的质量。2020 年有一次路透数据源推送了错误的股息率(从 2% 跳到 0%),TRS 的估值全部低估了约 3%。交易员早上一看组合 P&L 变”亏损”了,立即电话开发查问题。开发和量化花了 3 小时才定位到是数据源问题,更正后 P&L 回归正常。但这 3 小时里交易员已经做了一个 200 万的对冲调整(基于错误数据),第二天要反向平仓——损失约 5 万交易成本。此后系统加了行情数据变更的告警:每个输入来源的数据变动超过阈值时自动通知。
行情数据架构:
eds-price-server
├── Reuters feed (RFA) → 实时价格
├── Bloomberg feed (BBG) → 参考数据 + vol
└── Manual input UI → OTC / 非上证标的
│
▼
dataCenter (hedging-as)
├── PriceCache — 标的价格 (实时更新)
├── VolSurfaceManager — 波动率曲面 (每日更新)
├── IRCurveManager — 利率曲线
└── DividendForecast — 未来股息预测
第二步:收益函数 (Payoff)
收益函数 (payoff function) 定义了一个产品在不同市场情景下支付什么。每个产品类型有自己的 payoff 类:
hedging-as/src/com/cicc/edslib/payoff/
├── TrsPayoff.java — 总收益互换
├── SnowballPayoff.java — 雪球
├── NdollarPayoff.java — 无本金交割远期
├── VanillaOptionPayoff.java — 香草期权
├── BarrierOptionPayoff.java — 障碍期权
└── SwapPayoff.java — 利率 / 股权互换
payoff 函数接受:
- 状态 (State):当前标的价格、波动率、利率、到期时间
- 参数 (Parameters):行权价、障碍位、票息计划、利差
返回:该状态下产品的现值 (PV)。
例子——VanillaOptionPayoff(简化):
public double payoff(double spot, double strike, double vol, double rate, double time) {
double d1 = (Math.log(spot/strike) + (rate + vol*vol/2)*time) / (vol*Math.sqrt(time));
double d2 = d1 - vol*Math.sqrt(time);
return spot * N(d1) - strike * Math.exp(-rate*time) * N(d2);
}
第三步:估值 (Valuation)
估值 = 在所有相关状态下运行 payoff 函数。
hedging-as/src/com/cicc/edslib/valuation/
├── MonteCarloValuation.java — 路径依赖产品 (Snowball, Barrier)
├── ClosedFormValuation.java — 有解析公式的产品 (Vanilla, Forward)
├── TreeValuation.java — 美式期权 (二叉树)
└── PDEValuation.java — 早期行权产品 (有限差分)
每种产品用什么方法:
| 产品类型 | 估值方法 | 原因 |
|---|---|---|
| TRS | 闭式 (Closed-form) | 线性产品,直接贴现现金流 |
| NDF | 闭式 | 远期定价 |
| 香草期权(欧式) | 闭式 (Black-Scholes) | 快且精确 |
| 香草期权(美式) | 二叉树 | 可随时行权 |
| 雪球(标准) | 蒙特卡洛 (Monte Carlo) | 路径依赖:KO/KI 取决于路径 |
| 雪球(记忆/阶梯敲出) | 蒙特卡洛 | 更复杂的路径依赖 |
| 障碍期权 | 蒙特卡洛 / 闭式 | 取决于障碍类型 |
| Swap | 闭式 | 贴现已知/估测的现金流 |
蒙特卡洛过程(简化):
- 为标的价格路径生成 10,000–100,000 条随机路径
- 对每条路径计算 payoff
- 取所有 payoff 的平均值
- 贴现回今天
double sum = 0;
for (int i = 0; i < NUM_PATHS; i++) {
double[] path = simulatePath(spot, vol, rate, steps);
double payoff = payoffFunction.evaluate(path, params);
sum += payoff;
}
return sum / NUM_PATHS * discountFactor;
第四步:Greeks——风险敏感性
Greeks 告诉交易台:这个市场变量每变动 1 个单位,我们的 P&L 变化多少?
| Greek | 测量什么 | 代码位置 |
|---|---|---|
| Delta (Δ) | 标的价格每涨 $1,价格变化多少 | pricing/calculation/DeltaCalculator.java |
| Gamma (Γ) | Delta 自身每 $1 变化多少(二阶导数) | pricing/calculation/GammaCalculator.java |
| Vega (ν) | 波动率每变化 1%,价格变化多少 | pricing/calculation/VegaCalculator.java |
| Theta (Θ) | 每过一天,价格变化多少 | pricing/calculation/ThetaCalculator.java |
| Rho (ρ) | 利率每变化 1%,价格变化多少 | pricing/calculation/RhoCalculator.java |
Greeks 怎么算的(bump-and-run):
大多数 Greeks 通过扰动输入然后重新运行估值来计算:
double basePrice = valuationEngine.evaluate(spot, params);
double bumpedPrice = valuationEngine.evaluate(spot + 0.01, params);
double delta = (bumpedPrice - basePrice) / 0.01;
这意味着计算一个雪球的全部 5 个 Greeks = 6 次估值运行(1 次基准 + 5 次扰动)。按 10,000 次蒙特卡洛路径 + 10,000 笔活跃合约来算,EOD 批处理是计算密集型的——所以 hedging-as 跑在专用多核服务器上。
交易台怎么用 Greeks:
- Delta 对冲: 如果组合在沪深 300 上总 delta = +500,000,卖出 50 万沪深 300 期货对冲
- Gamma 监控: Gamma 高 = delta 变化快 = 需要更频繁地重新平衡对冲
- Vega 监控: Vega 大时,交易台会买期权来对冲波动率风险
- Theta 衰退: Theta 为正 = 交易台每天赚时间价值(雪球卖方正是如此)
第五步:输出 (Output)
定价引擎产生一个 PricingResult:
class PricingResult {
double midPrice; // 公允价
double bidPrice; // 中价 - 点差
double offerPrice; // 中价 + 点差
double delta, gamma, vega, theta, rho;
double pv01; // 利率每变化 1bp 的敞口
double[] cashFlows; // 预计现金流
List<String> warnings; // 例如 "barrier very close to spot"
}
2. 保证金计算 (Margin)
2.1 什么是保证金
保证金 (margin) 是担保品 (collateral)——双方为确保支付义务而押注的资金。OTC 衍生品不在交易所交易,因此没有中央清算所。交易台和客户必须约定保证金条款。
2.2 两类保证金
| 类型 | 是什么 | 怎么算 | 频率 |
|---|---|---|---|
| 初始保证金 (IM) | 覆盖潜在未来敞口的担保品 | VaR 类模型(95–99% 置信度,5–10 天平仓期) | 开仓时 + 定期重算 |
| 变动保证金 (VM) | 覆盖当前市值 (MTM) 的担保品 | = 合约的 Mark-to-Market 价值 | 每日 |
代码:
hedging-as/src/com/cicc/action/margin/ — 保证金动作入口
hedging-as/src/com/cicc/service/margin/ — 保证金服务层
hedging-as/src/com/cicc/service/fdsMargin/ — FDS 保证金模型实现
hedging-as/src/com/cicc/pricing/margin/ — 保证金分析
2.3 保证金工作流
- EOD 估值 对所有合约计算 MTM
- MarginCalculator 按保证金模型对每笔合约计算 IM
- VM = MTM 价值(为正时客户欠我们,为负时我们欠客户)
- 按交易对手汇总 (NetMargin) = 全部合约的 IM + VM 之和
- 如果 NetMargin 超过阈值(通常 50–100 万 RMB)→ 生成追保通知 (Margin Call)
- 追保通知发给交易对手 → 对方必须补充担保品
错发催缴的现实成本: 保证金计算是 EOD 中风险最高的步骤——算多了客户投诉,算少了交易台承担风险。2021 年有一笔 TRS 的 IM 计算因为 VaR 参数配置错误(平仓期设成了 3 天而不是 5 天),给客户发了低于实际需要的催缴通知。直到 2 天后另一个对手方事件才暴露这个问题。少催缴的金额约 200 万。虽然最后追回了,但这 2 天的”缺口”意味着交易台的暴露超出预期。此后运营团队每天对保证金结果做人工抽样核对——花 1 人 × 30 分钟/天。这是 EOD 流程中唯一的”手工把关”环节。
保证金 bug 的代码入口:
hedging-as/src/com/cicc/pricing/margin/MarginCalculator.java— 方法calculateMargin(contract, marketData)。
3. 压力测试 (Stress Testing)
3.1 是什么
压力测试回答:“如果市场暴跌 30%,我们会亏多少?”
与保证金(基于风险中性)不同,压力测试应用极端但合理的情景:
| 情景 | 描述 | 典型变动 |
|---|---|---|
| 股权崩盘 | 沪深 500 一个月跌 30% | 标的价格 -30%,Vol +50% |
| Vol 飙升 | 类似 VIX 的事件 | Vol 翻三倍 |
| 利率冲击 | 央行加息 200bp | 利率 +2% |
| 汇率危机 | RMB 贬值 10% | USD/CNY +10% |
| Gap 风险 | 股票隔夜跳空 20% | 标的价格 -20%(vol 不变) |
| 相关性崩溃 | 所有相关性变为 1 | 分散化消失 |
3.2 压力测试管线
hedging-as/src/com/cicc/action/pressTest/ — 压力测试编排
hedging-as/src/com/cicc/pricing/pressTest/ — 情景定义
odts-option-web/src/modules/stress-testing/ — 压力测试 UI
- 情景定义: 文件/DB 记录定义市场变动(如 “标的价格 -20%, vol +30%, 利率 +100bp”)
- 应用: 情景应用于当前行情数据 → 产生扰动后的输入
- 重估值: 用扰动后的输入对所有合约重新定价
- P&L 影响: 当前组合价值 − 压力值 = 亏损
- 报告: 结果存入
StressTestResult表,在压力测试 UI 中展示
代码入口:
hedging-as/src/com/cicc/pricing/pressTest/— 情景逻辑- 使用与
PricingService相同的定价管线,只是输入不同
压力测试的真实使用场景(2022 年 3 月): 俄乌冲突爆发后,A 股连续大跌,交易台的风控负责人要求立刻跑一次”标的价格-20% + vol+50%“的压力测试。结果出来:如果市场继续下跌到这个程度,组合将亏损约 8 亿 RMB——接近交易台的亏损限额。这个结果直接导致了两个决策:降低 TRS 的杠杆倍数上限(5x → 3x)、暂停新开中证 500 雪球(直到市场稳定)。压力测试在这里不是”合规演练”——它直接影响了风控决策。
3.3 交易台怎么用压力测试
- 限额监控 (Limit monitoring): 压力亏损超过限额(如 ¥5 亿)→ 交易台必须减仓
- 集中度风险: 如果 80% 的亏损来自一个指数 → 交易台过于集中
- Stress-to-VaR 比率: 压力亏损远大于 VaR → 交易台暴露于 VaR 无法捕捉的尾部风险
- 反向压力测试: “市场要跌到什么程度我们才会亏 10 亿?“
4. 复杂事件处理 (CEP)
ODTS 用 CEP 做实时风险监控:
hedging-as/src/com/cicc/cep/ — CEP 引擎集成
hedging-as/src/com/cicc/cep/util/ — CEP 工具
CEP 在 2022 年雪球危机中的表现: 当年 3 月中证 500 单日暴跌 5% 时,CEP 的障碍预警功能在盘中 10:30 就发出了”50+ 个雪球接近敲入障碍”的告警——比 EOD 批处理早了 6 小时。交易员利用这个预警在盘中提前调整了对冲头寸。如果等 EOD 才知道敲入规模,对冲成本会高出约 100 万。这是 CEP 唯一一次在实战中证明了自己的价值。
CEP 引擎实时处理行情数据 tick,做三件事:
- 标的价格变化 → 实时重算 delta/gamma
- 价格接近障碍位 → 触发障碍预警
- 组合风险超限 → 发出告警
5. 常见定价 Debug 场景
| 症状 | 可能原因 | 先查哪里 |
|---|---|---|
| 价格是 NaN | Payoff 中除以零 | payoff/YourProductPayoff.java |
| 价格是 0 | 行情数据缺失 | dataCenter/PriceCache.java |
| Greeks 全是 0 | 扰动失败 | calculation/DeltaCalculator.java |
| 保证金不对 | 保证金模型参数过期 | margin/MarginCalculator.java |
| EOD 定价超过 10 小时 | MC 路径太多或 DB 慢 | valuation/MonteCarloValuation.java |
| 障碍从未触发 | KO/KI 检查未运行 | service/contract/ContractKnockInOut.java |
| 定盘用的汇率不对 | 定盘源配置错误 | action/Fixing/ → eds-price-server |
6. 定价技术栈总览
┌─────────────────────────────────────────────────────┐
│ 前端 │
│ new-edsweb / odts-option-web (Vue 2) │
│ 用户输入条款 → 看到报价 │
└──────────┬──────────────────────────────────────────┘
│ HTTP (Axios)
┌──────────▼──────────────────────────────────────────┐
│ eds-web-app (Spring Boot) │
│ ContractController → PreBookController │
│ 路由请求到定价引擎 │
└──────────┬──────────────────────────────────────────┘
│ Protobuf / Dipper / ActiveMQ
┌──────────▼──────────────────────────────────────────┐
│ hedging-as (JFinal + 自定义框架) │
│ │
│ ┌────────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ 定价编排 │ │ 估值引擎 │ │ 保证金计算 │ │
│ └────────────┘ └──────────┘ └────────────────┘ │
│ ┌────────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ Payoff 库 │ │ Greeks │ │ 压力测试 │ │
│ │ (edslib) │ │ (bump) │ │ │ │
│ └────────────┘ └──────────┘ └────────────────┘ │
│ ┌──────────────────────────────────────────┐ │
│ │ 行情数据 (dataCenter) │ │
│ │ Spot | Vol | IR | FX | Div │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
自测
- 一个雪球的 Delta、Gamma、Vega、Theta 分别告诉交易台什么?
- 压力测试和保证金有什么本质区别?
- EOD 估值跑得慢,怎么排查?(三条可能原因)
- 一个交易台在沪深 500 上组合 Delta = +300 万,他们要做什么?
7. 内行才知道的:定价引擎里”实时报价”和”EOD 估值”用的是两套代码路径
很多人默认”报价和估值就是同一个函数传不同参数”。在 ODTS 里不是——它们是两条被分开维护的代码路径,而且行为有微妙差异,是定价 bug 的高发区。
两条路径在哪
实时报价 (quote):
eds-web-app/.../PreBookController.java
→ Dipper → hedging-as/.../pricing/service/*PricingService.java
→ 轻量 MC / 解析解,强调速度 (SLA 500ms 内)
EOD 估值 (valuation):
hedging-as/.../action/valuation/ValuationAction.java
→ hedging-as/.../valuation/MonteCarloValuation.java
→ 完整 MC,强调精度 (跑几小时无所谓)
关键差异:实时路径为了快,会做近似。 比如雪球的实时报价可能用缩减的 MC 路径数(例如 1 万条),而 EOD 用全量(例如 10 万条);实时路径还经常跳过部分 Greeks 只算 Delta,EOD 才全算。这意味着一笔交易在白天报价时显示的价格,和当晚 EOD 算出的 MTM 本来就允许存在微小差异——这不是 bug,是设计取舍。
真实踩坑:报价价和估值价差了 0.8%,对账对到崩溃
2020 年有一次,交易台拿白天给客户报的雪球价和当晚 EOD MTM 对账,发现差了 0.8%。运营拉了三天,最后定位到:实时路径里 SnowballPayoff 的敲出观察频率被设成了”每日观察”,而 EOD 路径设成了”每月观察”——这是两年前某次为了加速实时报价做的临时改动,从没同步回 EOD 路径。两种观察频率下敲出概率差很多,所以价格差了 0.8%。
业务影响: 这 0.8% 落在客户的票息上,等于每笔 1 亿名义本金差了 80 万。那次只影响了几笔在途交易,但暴露了一个治理问题:两套代码路径的”配置一致性”没有任何自动校验。后来加了一个每晚的对账断言——把实时路径和 EOD 路径用同一组输入各跑一遍,差异超过阈值就报警。这个对账脚本现在还在跑,是定价团队睡得着的关键。
下一篇:09-Migration-from-Monolith.md —— 如何拆解原来的单体应用