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

Pricing And Risk

OTC 衍生品 · 19 JUL 2026 · 14 min read · 2,896 words
· · ·

ODTS 08 — 定价与风险:引擎如何工作

为什么需要这篇文档

定价引擎 (hedging-as) 是整个 ODTS 中智力密度最高的部分,也是所有开发者觉得最难 debug 的——不是代码逻辑复杂,而是数学不熟悉。这篇文档从业务直觉讲到代码结构。


1. 定价管线 (Pricing Pipeline)

每一个价格——无论实时报价还是 EOD 批量估值——都流经同样的五步管线:

Inputs → Payoff → Valuation → Greeks → Output

第一步:输入 (Inputs)

输入来源代码位置
标的价格 (spot)eds-price-server → Reuters/Bloombergpricing/dataCenter/
波动率曲面 (vol surface)eds-price-server → SurfaceBuilderpricing/dataCenter/VolSurface.java
利率曲线 (IR curve)eds-price-server → CurveBuilderpricing/dataCenter/IRCurve.java
股息率 (dividend yield)eds-price-server → CorpActionsservice/contract/DividendHelper.java
汇率 (FX rate)eds-price-servereds-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闭式贴现已知/估测的现金流

蒙特卡洛过程(简化):

  1. 为标的价格路径生成 10,000–100,000 条随机路径
  2. 对每条路径计算 payoff
  3. 取所有 payoff 的平均值
  4. 贴现回今天
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 保证金工作流

  1. EOD 估值 对所有合约计算 MTM
  2. MarginCalculator 按保证金模型对每笔合约计算 IM
  3. VM = MTM 价值(为正时客户欠我们,为负时我们欠客户)
  4. 按交易对手汇总 (NetMargin) = 全部合约的 IM + VM 之和
  5. 如果 NetMargin 超过阈值(通常 50–100 万 RMB)→ 生成追保通知 (Margin Call)
  6. 追保通知发给交易对手 → 对方必须补充担保品

错发催缴的现实成本: 保证金计算是 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
  1. 情景定义: 文件/DB 记录定义市场变动(如 “标的价格 -20%, vol +30%, 利率 +100bp”)
  2. 应用: 情景应用于当前行情数据 → 产生扰动后的输入
  3. 重估值: 用扰动后的输入对所有合约重新定价
  4. P&L 影响: 当前组合价值 − 压力值 = 亏损
  5. 报告: 结果存入 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,做三件事:

  1. 标的价格变化 → 实时重算 delta/gamma
  2. 价格接近障碍位 → 触发障碍预警
  3. 组合风险超限 → 发出告警

5. 常见定价 Debug 场景

症状可能原因先查哪里
价格是 NaNPayoff 中除以零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 —— 如何拆解原来的单体应用