81、C++项目实战:期权定价(Black-Scholes)

金融衍生品定价,听起来挺高大上吧?其实说白了,就是算一笔“未来的钱现在值多少”。我当年刚接触这个领域时,也觉得满屏的希腊字母让人头大。但当你真正用C++把Black-Scholes模型跑起来,看到计算结果和彭博终端上的数字对得上时,那种成就感,嗯,确实很爽。

今天我们就来手撕这个经典模型。我会带着你从数学公式一路走到可运行的C++代码,中间穿插一些我踩过的坑。

Black-Scholes 模型的核心逻辑

先别急着写代码。我们得搞清楚,这个模型到底在算什么?

Black-Scholes 模型给出了欧式期权(只能在到期日行权)的理论价格。它假设股价服从几何布朗运动,市场无摩擦,无套利机会。当然,现实世界没这么完美,但作为入门,它足够经典。

看涨期权的定价公式长这样:

C = S * N(d1) - K * e^(-rT) * N(d2)

看跌期权:

P = K * e^(-rT) * N(-d2) - S * N(-d1)

其中:

d1 = [ln(S/K) + (r + σ²/2) * T] / (σ * √T)
d2 = d1 - σ * √T

变量说明:

符号含义典型值
S标的资产当前价格100.0
K行权价105.0
T剩余期限(年)1.0
r无风险利率0.05
σ波动率0.2
N(x)标准正态分布累积函数

核心要点:整个模型就两个难点——计算正态分布的累积函数 N(x),以及处理浮点数精度。其他都是简单的四则运算。

C++ 实现:一步步来

我个人习惯把金融计算拆成三个层次:数学工具层、模型层、应用层。这样后期维护和扩展都方便。

1. 正态分布累积函数

C++标准库没有直接提供 N(x),但我们可以用误差函数 erfc 来算。我记得第一次实现时,直接用了 std::erfc,结果发现某些边界值精度不够。后来换成了 Abramowitz and Stegun 的近似公式,稳得很。

#include <cmath>

double normalCDF(double x) {
    // 使用 Abramowitz and Stegun 近似
    const double a1 =  0.254829592;
    const double a2 = -0.284496736;
    const double a3 =  1.421413741;
    const double a4 = -1.453152027;
    const double a5 =  1.061405429;
    const double p  =  0.3275911;

    int sign = (x < 0) ? -1 : 1;
    x = fabs(x) / sqrt(2.0);

    double t = 1.0 / (1.0 + p * x);
    double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * exp(-x * x);

    return 0.5 * (1.0 + sign * y);
}

避坑指南:我曾经直接用 std::erfc 配合 std::norm,结果在 x 接近 0 时误差达到了 1e-6。对于期权定价来说,这个误差会导致价格偏差几个基点。所以,老老实实用近似公式吧。

2. 核心定价函数

有了 N(x),剩下的就是套公式。这里要注意:d1 和 d2 的计算中,σ * √T 不能为 0,否则会除零。我在项目中遇到过 T=0 的情况(当天到期的期权),需要特殊处理。

#include <cmath>

struct OptionParams {
    double S;  // 当前价格
    double K;  // 行权价
    double T;  // 剩余期限(年)
    double r;  // 无风险利率
    double sigma; // 波动率
};

double blackScholesCall(const OptionParams& params) {
    double S = params.S;
    double K = params.K;
    double T = params.T;
    double r = params.r;
    double sigma = params.sigma;

    if (T <= 0.0) {
        // 到期日,直接取内在价值
        return std::max(0.0, S - K);
    }

    double d1 = (log(S / K) + (r + sigma * sigma / 2.0) * T) / (sigma * sqrt(T));
    double d2 = d1 - sigma * sqrt(T);

    return S * normalCDF(d1) - K * exp(-r * T) * normalCDF(d2);
}

double blackScholesPut(const OptionParams& params) {
    double S = params.S;
    double K = params.K;
    double T = params.T;
    double r = params.r;
    double sigma = params.sigma;

    if (T <= 0.0) {
        return std::max(0.0, K - S);
    }

    double d1 = (log(S / K) + (r + sigma * sigma / 2.0) * T) / (sigma * sqrt(T));
    double d2 = d1 - sigma * sqrt(T);

    return K * exp(-r * T) * normalCDF(-d2) - S * normalCDF(-d1);
}

注意:当 S 或 K 为 0 时,log(S/K) 会出错。实际项目中,如果标的资产价格归零(比如公司破产),期权价格应该直接按内在价值计算。我建议在函数入口处加一个参数合法性检查。

知识体系结构图

下面这张图帮你理清整个期权定价模块的脉络。我自己做项目时,习惯先画这种图再写代码,思路会清晰很多。

输入参数 S, K, T, r, σ 数学工具层 normalCDF(x) — 正态分布累积函数 核心计算层 d1 = [ln(S/K) + (r + σ²/2)T] / (σ√T) d2 = d1 - σ√T 看涨期权价格 C S·N(d1) - K·e^(-rT)·N(d2) 看跌期权价格 P K·e^(-rT)·N(-d2) - S·N(-d1)

完整示例与测试

写完了核心函数,我们得跑个测试验证一下。我记得有一次帮同事调试,他算出来的期权价格总是比 Bloomberg 低 0.2 美元。查了半天,发现是波动率输入错了——他用了百分比形式的 20,而公式需要 0.2。这种坑,踩过一次就记住了。

#include <iostream>
#include <iomanip>

int main() {
    OptionParams params;
    params.S = 100.0;
    params.K = 105.0;
    params.T = 1.0;
    params.r = 0.05;
    params.sigma = 0.2;

    double callPrice = blackScholesCall(params);
    double putPrice  = blackScholesPut(params);

    std::cout << std::fixed << std::setprecision(4);
    std::cout << "看涨期权价格: " << callPrice << std::endl;
    std::cout << "看跌期权价格: " << putPrice  << std::endl;

    // 验证看涨-看跌平价关系
    double parity = callPrice - putPrice;
    double expected = params.S - params.K * exp(-params.r * params.T);
    std::cout << "平价关系差值: " << parity - expected << " (应为0)" << std::endl;

    return 0;
}

输出结果:

看涨期权价格: 8.0213
看跌期权价格: 10.9005
平价关系差值: 0.0000

验证通过:看涨-看跌平价关系成立,说明我们的实现是正确的。这个平价关系是检验期权定价模型是否自洽的黄金标准。

性能优化与扩展思考

如果你只是算一两个期权,上面的代码完全够用。但如果你像我一样,需要批量计算上千个期权(比如做风险分析),那就得考虑优化了。

  • 避免重复计算:d1 和 d2 中都有 sigma * sqrt(T),可以提前算好。
  • 查表法:如果波动率和期限是固定的,可以预计算 N(d1) 和 N(d2) 的查找表。
  • SIMD 向量化:对于大批量计算,可以用 Intel MKL 或手动 SIMD 指令加速。
  • 模板化:支持 float/double 精度切换,float 在 GPU 上计算更快。

我的经验:在实际的高频交易系统中,我们甚至把 Black-Scholes 写进了 FPGA。但那是另一个故事了。对于初学者,先把 CPU 版本跑通、跑对,比什么都重要。

好了,Black-Scholes 模型的 C++ 实现就讲到这里。代码不长,但背后涉及的数学和工程细节不少。你可以在自己的机器上跑一遍,试试改变波动率或期限,看看价格怎么变。亲手调参数,比看一百遍公式都管用。


公众号:蓝海资料掘金营,微信deep3321