简介:rBergomi 模型是当前粗糙波动率研究中的代表性模型,该资源是其 C++ 实现,面向对冲基金、券商自营及衍生品领域的量化研究者和期权定价开发人员。代码使用蒙特卡洛方法完成欧式期权定价,并通过混合方案高效生成分数布朗运动,当前版本实现了恒定前向方差假设,可作为扩展随机波动率模型的基础。压缩包内共 185 个文件,包含 52 个 cpp 源文件、46 个 h 头文件,以及 29 个 R 脚本、4 个 Jupyter Notebook、多个 LaTeX/PDF 文档和工程辅助文件,兼顾核心算法、数据测试与文档展示,压缩包大小约为 62.37MB。资源需要 FFTW3 库、OpenMP 支持和 C++14 编译器,并附带了 Eclipse 工程、R 语言调用示例及 Sobol 序列生成等辅助程序。目前已有 173 人学习下载,适合用于学术复现、策略回测中的定价模块集成,或作为期权定价课程的项目参考。 做量化模型落地这件事,最烦人的不是数学推导,而是把一个在论文里看似干干净净的随机过程写成一套能跑、能调、能上线的高性能代码。rBergomi(Rough Bergomi)就是典型代表:模型本身不复杂,但真要动手用C++实现,你会在分数阶布朗运动生成、路径离散化、参数标定性能这些环节上踩掉一层皮。这篇文章我会把rBergomi模型的C++实现整个拆开,从模型结构、类设计到核心算法,再到实际跑Monte Carlo定价时遇到的性能问题和数值坑,全部讲清楚。
1. 项目概览与模型定位
1.1 rBergomi模型到底讲了什么事
rBergomi模型属于rough volatility一族,由Bayer、Friz和Gatheral在2016年前后系统提出。它最大的特点是:瞬时方差率不是一个普通扩散过程,而是由分数阶布朗运动驱动的,Hurst参数H通常取值在0.05到0.15之间,远小于经典Heston模型里隐含的0.5。
这个"rough"特性带来的效果很直观:模型能天然复现隐含波动率微笑在短到期期限下的陡峭形态,而不用像Heston那样靠跳跃或者复杂相关性结构去硬凑。在实际交易和风控场景里,短端波动率微笑的拟合质量直接决定了期权组合的delta和vega对冲表现,这正是rBergomi在业界受欢迎的核心原因。
它的核心方程是:
- 方差过程:V_t = ξ(t) * exp(η * √(2H) * W̃_t - 0.5 * η² * t^(2H))
- 标的价格过程:dS_t = √V_t * dW_t(风险中性下无漂移项,实际带利率项)
其中W̃_t是关键:它不是标准布朗运动,而是Riemann-Liouville分数阶布朗运动,它的存在让整个模型从"一个随机波动率模型"升级成"一个带有路径依赖记忆效应的随机波动率模型"。这个记忆效应直接导致了方差的rough行为,也是实现里最需要小心处理的部分。
1.2 为什么这个项目值得用C++写
市面上rBergomi的实现很多,Python、Matlab、Julia都有,但C++版本的价值主要在速度和可控性两个维度。
先说速度。期权定价要跑Monte Carlo模拟,一条路径要生成几百个时间步,每步还要做相关性分解后的高斯采样。一个典型的标定过程可能要跑几十万条路径乘以几百组参数,Python即便用numpy向量化,在内存带宽和循环开销上也有天然瓶颈。C++配合Eigen、OpenMP或者TBB,可以把同样规模的模拟压缩到亚秒级,这是实盘定价和批量回测的基本要求。
再说可控性。rBergomi模型没有封闭解,全部依赖模拟。这意味着随机数种子管理、协方差矩阵构造、方差路径的上下界控制这些细节都直接影响定价稳定性。C++没有运行时解释开销,内存布局完全自己说了算,在处理大规模路径、做方差缩减和分层采样时非常灵活。加上C++在交易系统里本来就是主流语言,用C++实现模型,后续接生产环境的工作量最小。
2. 整体架构设计:类划分与核心流程
2.1 合理拆分模块,避免"意大利面条"式代码
刚开始写这个项目时,我犯过一个典型错误:把模型参数、随机数生成、路径模拟、期权定价全塞进一个类里,结果代码写到一半,想换一个随机数引擎就要动全局。后来重构,花了整整一天时间重新设计接口,才稳定下来。
现在的架构我建议分为四个模块:
- 参数类
RBergomiParams:保存H、η、ρ、初始方差曲线、到期时间、时间步数、路径数等模型参数,同时负责参数合法性的校验。 - 随机数模块
GaussianRNG:封装正态分布采样,底层可替换为Mersenne Twister、PCG等伪随机引擎,支持多线程独立种子流。 - 路径生成器
PathSimulator:负责生成分数阶布朗运动W̃_t,以及方差路径V_t和标的价格路径S_t。这是核心计算模块。 - 定价引擎
MonteCarloPricer:接收路径生成器输出的价格序列,计算期权到期收益并做贴现求期望,输出价格和各种敏感度。
模块之间用接口解耦:路径生成器不关心你用的是哪种随机数算法,定价引擎也不关心路径生成器内部是精确模拟还是快速近似。这样每个模块可以独立测试和优化,这也是承接更大规模衍生品定价系统的合理起点。
2.2 参数管理是容易忽视的"隐形需求"
rBergomi模型的参数虽然少,但每个参数对路径行为的影响都很大。我在设计参数类时特意做了三个决策:
第一,参数全部用double类型,初始化时校验H必须在(0, 0.5)区间,η必须大于0,ρ必须在(-1, 1)区间。这些约束在数学上有明确含义:H>0.5意味着路径是平滑的,模型退化了;ρ不在区间内,资产价格与方差的瞬时相关性就失去意义。运行时一旦违反约束,直接抛异常,而不是带着错误参数跑出垃圾结果。
第二,初始方差曲线 ξ(t) 做成一个数组,而不是一个标量。论文里经常假设ξ(t)是常数,但实际标定时,我们通常用市场隐含方差曲线去拟合,这时候ξ是一条关于到期时间的曲线。用一个std::vector<double>保存曲线值,在时间步之间做线性插值,灵活性会好很多。
第三,时间步数和路径数全部可配,且与定价精度挂钩。模型默认时间步数252(对应一年交易日),路径数10万。这两个数字直接影响fBm协方差矩阵的规模和模拟耗时,必须做成参数而不是写死。
3. 核心算法实现:从随机数到期权价格
3.1 分数阶布朗运动的生成是最大的技术难点
rBergomi实现里最绕不开的就是W̃_t——Riemann-Liouville分数阶布朗运动。直接用定义做卷积积分是不现实的,因为核函数(t-s)^(H-0.5)在s接近t时发散。工程上常用的方案有三种:
第一种是精确协方差方法。把fBm在离散时间网格上的协方差矩阵构造出来,做Cholesky分解,然后对标准正态随机向量做线性变换。这个方法胜在精确,但协方差矩阵是N×N的,Cholesky分解复杂度O(N³),当N超过500时计算量已经很不友好,且存储开销大。
第二种是混合截断方法。把Volterra积分核展开成一组指数函数或幂函数的加权和,然后用有限项高斯过程去逼近。这个方法速度快,精度可通过增加项数控制,是目前工业界最实用的方案之一,也是我最终实现里采用的主流方法。
第三种是快速Hilbert变换方法,利用FFT把协方差矩阵对角化生成样本,复杂度O(N log N)。精度很高,但代码复杂度也高,适合追求极致性能且存储受限的场景。
从实战角度讲,我强烈建议第一次实现时直接从混合截断入手。它在精度和速度之间平衡最好,而且不会遇到Cholesky方法那种"路径数一多内存先爆"的尴尬。
混合截断的核心思想不复杂:把核函数k(t,s)近似成若干个高斯核的加权和,这样W̃_t就可以写成多个Ornstein-Uhlenbeck型过程的线性组合,每个过程用标准布朗运动驱动。离散化时,每个子过程用简单的递推公式更新,整体计算复杂度降到O(N·M),M是展开项数。
我实际的实现中M取10到15项。M太少,低频部分拟合精度不足,期权价格会偏差;M太多,计算量上升但收益递减。具体的截断形式、权重和基函数的计算方法,可以参考Bayer等在论文里给出的混合截断公式,直接按论文模板实现,验证通过后再调优。
3.2 方差过程与价格路径的离散化
有了分数阶布朗运动W̃_t,方差路径的离散化就比较直接了。在时间网格t_i上:
V_{t_{i+1}} = ξ(t_{i+1}) * exp( η * √(2H) * dW̃_i - 0.5 * η² * dt^(2H) )
其中dW̃_i是W̃_t在[Δt]区间上的增量。这个公式是精确的,因为方差过程本身就是指数形式,不需要Euler-Maruyama近似。
标的价格路径则需要做离散化。我用的是对数Euler格式:
log S_{t_{i+1}} = log S_{t_i} - 0.5 * V_i * dt + √(V_i * dt) * (ρ * dW_t + √(1-ρ²) * dW_perp)
其中dW_t是与驱动方差的分数阶布朗运动相关的标准正态增量,dW_perp是独立的标准正态增量。注意这里的dW_t不是直接等于dW̃_i,两者的相关性由模型定义给出,这里的处理需要转成增量形式后与ρ结合。
这里有个关键点:ρ的作用是在方差冲击和标的价格冲击之间引入即时相关性。实现时,需要先用两个独立标准正态变量Z1和Z2合成一个相关性变量:
Z_asset = ρ * Z_1 + √(1-ρ²) * Z_2
其中Z_1必须与方差过程中的随机增量完全一致(同一个随机数),否则相关性对不上,算出来的期权价格会直接错误。这个问题我在初版代码里踩过坑:用了两个独立的随机数流分别生成方差和标的价格增量,结果ρ无论设成什么值都不影响价格,检查半天最后发现是相关性没有共享随机数。
整数时间步方面,我默认用252步一年,也就是每个交易日一个时间节点。如果要更精细,比如考虑日内波动,可以增加到1008步,但fBm生成的计算量会线性上升,需要自己在速度和精度之间权衡。
3.3 Monte Carlo定价主循环与结果输出
定价主循环逻辑很简单:每条路径,按照3.2的离散化公式生成到期时的标的价格S_T,然后计算期权收益。欧式看涨期权收益是max(S_T - K, 0),看跌期权是max(K - S_T, 0),最后对所有路径收益取平均,乘以贴现因子exp(-rT)得到期权价格。
我还额外输出了两条信息:标准误和方差路径的平均形态。标准误用来判断当前路径数下价格是否收敛,方差路径平均形态则用来做模型合理性检查——如果平均方差曲线在数值上出现系统性偏离初始曲线的趋势,那说明fBm的生成或者参数设定有问题。
核心代码结构大致如下:
double priceEuropeanCall(const RBergomiParams& params, double strike) { std::vector<double> logS = generateLogPricePaths(params); double sum = 0.0, sumSq = 0.0; for (double logS_T : logS) { double payoff = std::max(std::exp(logS_T) - strike, 0.0); sum += payoff; sumSq += payoff * payoff; } double mean = sum / params.numPaths; double se = std::sqrt((sumSq / params.numPaths - mean * mean) / params.numPaths); double price = std::exp(-params.rate * params.maturity) * mean; return price; }实际工程里不会用单线程串行循环,而是用OpenMP并行化路径循环,同时注意每条线程独立随机数流。并行优化的细节我放在第4节详细说,这里先强调一个原则:模型结果必须保证"同样的输入参数在并行前后完全一致",否则在多线程下调参会非常痛苦。要做到这点,最简单的方法是在每个线程里使用独立的随机数引擎,并用固定的线程数+固定的种子初始化顺序来保证可复现性。
4. 性能优化、典型问题与排查思路
4.1 性能瓶颈在哪里,怎么优化
rBergomi模型的Monte Carlo定价,计算量分布很不均匀。路径数越大,标的价格离散化部分占的比重大;时间步数越大,fBm生成部分占比上升。我在实测中发现,当时间步数达到252、路径数10万时,fBm生成耗时约占总耗时的70%到80%。这意味着,如果不优化fBm生成,其它部分写得再快也白搭。
第一个优化点是避免在热循环里重复分配内存。路径模拟要求每个时间步都生成一组随机数、更新一组中间变量,如果每次循环都动态new数组,性能会崩塌。我在工程实现里把所有中间数组预先分配好,比如std::vector<double>对象在模拟开始前一次性resize到最大维度,循环体内只做索引访问和赋值,不做任何内存分配。
第二个优化点是随机数生成器。C++标准库的std::mt19937配合std::normal_distribution虽然方便,但normal_distribution内部可能有分支和三角函数计算,吞吐量不高。如果对性能有追求,可以换成Box-Muller变换自己做正态采样,或者用Ziggurat算法。实测下来,Ziggurat比标准库的normal_distribution快一倍以上。不过要注意,Ziggurat实现需要两张查找表,内存占用不大,但代码稍微复杂一些。
第三个优化点是并行化。Monte Carlo是天然的embarrassingly parallel,每条路径之间没有依赖关系,直接用OpenMP在路径维度上并行即可。我常用的做法是:
#pragma omp parallel for schedule(static) for (int i = 0; i < numPaths; ++i) { // 每条路径独立初始化随机数流 // 模拟并计算payoff,累加到线程私有变量 }这里的关键是随机数流的管理。不能多条线程共享同一个随机数引擎,那会造成竞争导致结果错误。我在实现里,每条线程创建独立的随机数引擎,用(线程ID + 用户指定种子)来初始化。这样既能复现结果,又能在不同线程间保证随机序列独立。
4.2 常见数值问题与排查清单
我在做这个项目时遇到过不少坑,记录几个最有代表性的:
第一个是方差路径为负的问题。rBergomi的方差过程是指数形式,理论上严格为正,但在数值实现时,如果ξ(t)插值不当或者η选得过大,可能导致中间计算的值域下溢或者出现不合理的爆炸式增长。我的处理办法是在每个时间步检查方差值是否落在合理区间,比如[1e-6, 10]之间,超出区间就截断,并在debug模式下输出警告。这个操作对期权定价结果影响不大,但能避免后面计算√V_t时出现NaN。
第二个是方差缩减技术。纯Monte Carlo定价的标准误和路径数开根号成反比,想提高一位精度要百倍路径数,太不划算。我加了对偶变量法(antithetic variates):每条路径生成时,同时生成一正一负两组随机数,用它们的平均收益作为这条路径的估计。这样方差能降低一半左右,而且实现成本极低,只需要在路径模拟时多走一遍同一个循环结构。
第三个是初始方差曲线平滑性的影响。模型里ξ(t)如果是直接从市场数据插值出来的,往往会带有噪声,这会让方差路径的分布出现假性波动,影响定价稳定性。我的做法是先对市场曲线做平滑处理,比如用三次样条或者局部回归,再用平滑后的曲线作为模型输入。这套流程写成一个独立的预处理函数,与模型本身解耦,方便复用。
第四个常见问题隐藏得比较深:当时间步数特别少时,比如只有10步,rBergomi模型的价格会明显偏离解析近似值。原因是fBm在粗网格上的离散化误差较大。我在项目里加了"步数敏感性检查"的功能,跑一组时间步数递增的参数,看价格是否趋于稳定。如果时间步数翻倍价格还在明显变化,就提高默认步数,直到变化在可接受范围内。
4.3 一些工程实现上的经验之谈
整个项目做完,我的一个深刻体会是:rBergomi模型的C++实现,难度其实不在数学部分,而在工程纪律。只要把fBm的生成方式选好、内存管理做好、随机数流的隔离和可复现性处理好,这个模型的代码量是可以控制在几千行以内并且非常好维护的。
还有一点价值很高,是模型参数的合理性检查。不要轻信标定得到的结果,尤其是H和η。H=0.01和H=0.1在期权价格上的差异往往很小,但在路径形态上差异巨大。我在代码里加了路径可视化导出功能,把生成的若干条典型方差路径输出成CSV,用Python直接绘图检查,一眼就能看出模型行为是否符合直觉。这个小功能省了我大量调试时间,值得推荐。
最后一个建议是接口设计上预留一个"回调函数"位置,用于在每条路径生成后即时处理或者存储自定义统计量。比如我需要计算路径上最大回撤、平均波动率、路径依赖期权的收益等,就不需要改核心模拟代码,只需要传入一个lambda表达式,在路径级完成统计。这样的设计让模型核心代码保持稳定,外围需求随意扩展,维护成本直线下降。
本文还有配套的精品资源,点击获取