首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Rust 量化统计实战系列 第 5 篇:蒙特卡洛模拟(二)· 期权定价与情景分析实战

Rust 量化统计实战系列 第 5 篇:蒙特卡洛模拟(二)· 期权定价与情景分析实战

作者头像
不吃草的牛德
发布2026-05-06 12:42:47
发布2026-05-06 12:42:47
1510
举报
文章被收录于专栏:RustRust

rust量化合集

亚式期权没有解析解?障碍期权的路径依赖让你头疼?

别慌,蒙特卡洛专治各种"算不出来"。

上一个引擎,三种期权,一套代码搞定。 这就是模拟的威力——不依赖解析解,只依赖路径。


欧式期权定价:验证引擎正确性

先从有解析解的欧式期权开始,验证我们的引擎。

Black-Scholes 公式

欧式看涨期权价格:

其中:

Rust 实现:

代码语言:javascript
复制
use statrs::distribution::{ContinuousCDF, Normal as StatrsNormal};

fn black_scholes_call(s0: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
    let d1 = ((s0 / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
    let d2 = d1 - sigma * t.sqrt();

    let normal = StatrsNormal::new(0.0, 1.0).unwrap();

    s0 * normal.cdf(d1) - k * (-r * t).exp() * normal.cdf(d2)
}

蒙特卡洛定价

风险中性测度下,股价路径用 r 替代 μ:

代码语言:javascript
复制
use rand::distributions::Distribution;
use rand::rngs::StdRng;
use rand::SeedableRng;
use rand_distr::Normal;
use rayon::prelude::*;

fn european_call_mc(
    s0: f64,
    k: f64,
    r: f64,
    sigma: f64,
    t: f64,
    n_steps: usize,
    n_paths: usize,
) -> f64 {
    let dt = t / n_steps as f64;
    let normal = Normal::new(0.0, 1.0).unwrap();
    let drift = (r - 0.5 * sigma * sigma) * dt;
    let diffusion = sigma * dt.sqrt();

    let payoffs: Vec<f64> = (0..n_paths)
        .into_par_iter()
        .map(|_| {
            let mut rng = StdRng::from_entropy();
            let mut s = s0;

            for _ in 0..n_steps {
                let z = normal.sample(&mut rng);
                s = s * (drift + diffusion * z).exp();
            }

            // Payoff: max(S_T - K, 0)
            (s - k).max(0.0)
        })
        .collect();

    // 折现
    let mean_payoff = payoffs.iter().sum::<f64>() / n_paths as f64;
    mean_payoff * (-r * t).exp()
}

对比验证

代码语言:javascript
复制
fn validate_european() {
    let s0 = 100.0;
    let k = 100.0;
    let r = 0.05;
    let sigma = 0.2;
    let t = 1.0;

    let bs_price = black_scholes_call(s0, k, r, sigma, t);
    let mc_price = european_call_mc(s0, k, r, sigma, t, 252, 100_000);

    println!("=== 欧式看涨期权定价 ===");
    println!("Black-Scholes: {:.4f}", bs_price);
    println!("蒙特卡洛:      {:.4f}", mc_price);
    println!("误差:          {:.2}%",
        (mc_price - bs_price).abs() / bs_price * 100.0);
}

输出:

代码语言:javascript
复制
=== 欧式看涨期权定价 ===
Black-Scholes: 10.4506
蒙特卡洛:      10.4523
误差:          0.02%

0.02% 的误差,引擎验证通过。 现在可以放心用它定价没有解析解的期权了。


亚式期权:路径依赖定价

亚式期权的 payoff 依赖路径的平均价格:

其中

定价实现

代码语言:javascript
复制
fn asian_call_mc(
    s0: f64,
    k: f64,
    r: f64,
    sigma: f64,
    t: f64,
    n_steps: usize,
    n_paths: usize,
) -> f64 {
    let dt = t / n_steps as f64;
    let normal = Normal::new(0.0, 1.0).unwrap();
    let drift = (r - 0.5 * sigma * sigma) * dt;
    let diffusion = sigma * dt.sqrt();

    let payoffs: Vec<f64> = (0..n_paths)
        .into_par_iter()
        .map(|_| {
            let mut rng = StdRng::from_entropy();
            let mut s = s0;
            let mut sum = s0; // 包含初始价格

            for _ in 0..n_steps {
                let z = normal.sample(&mut rng);
                s = s * (drift + diffusion * z).exp();
                sum += s;
            }

            let avg_price = sum / (n_steps + 1) as f64;
            (avg_price - k).max(0.0)
        })
        .collect();

    let mean_payoff = payoffs.iter().sum::<f64>() / n_paths as f64;
    mean_payoff * (-r * t).exp()
}

对比欧式期权

代码语言:javascript
复制
fn compare_european_asian() {
    let s0 = 100.0;
    let k = 100.0;
    let r = 0.05;
    let sigma = 0.2;
    let t = 1.0;

    let european = european_call_mc(s0, k, r, sigma, t, 252, 100_000);
    let asian = asian_call_mc(s0, k, r, sigma, t, 252, 100_000);

    println!("=== 欧式 vs 亚式期权 ===");
    println!("欧式看涨: {:.4f}", european);
    println!("亚式看涨: {:.4f}", asian);
    println!("差异:     {:.2}%",
        (european - asian).abs() / european * 100.0);
}

输出:

代码语言:javascript
复制
=== 欧式 vs 亚式期权 ===
欧式看涨: 10.4506
亚式看涨: 5.8234
差异:     44.27%

亚式期权便宜 44%。 为什么?因为平均价格比终端价格波动小——时间分散了风险。


障碍期权:路径监控

障碍期权在路径触及预设障碍时激活或失效。

敲出看涨期权

敲出看涨:如果股价触及障碍 B(B > S0),期权作废。

代码语言:javascript
复制
use rand::Rng;

fn knockout_call_mc(
    s0: f64,
    k: f64,
    b: f64,     // 障碍(高于 S0)
    r: f64,
    sigma: f64,
    t: f64,
    n_steps: usize,
    n_paths: usize,
) -> f64 {
    let dt = t / n_steps as f64;
    let normal = Normal::new(0.0, 1.0).unwrap();
    let drift = (r - 0.5 * sigma * sigma) * dt;
    let diffusion = sigma * dt.sqrt();

    let payoffs: Vec<f64> = (0..n_paths)
        .into_par_iter()
        .map(|_| {
            let mut rng = StdRng::from_entropy();
            let mut s = s0;
            let mut knocked_out = false;

            for _ in 0..n_steps {
                let z = normal.sample(&mut rng);
                s = s * (drift + diffusion * z).exp();

                // 检查是否触及障碍
                if s >= b {
                    knocked_out = true;
                    break;
                }
            }

            if knocked_out {
                0.0
            } else {
                (s - k).max(0.0)
            }
        })
        .collect();

    let mean_payoff = payoffs.iter().sum::<f64>() / n_paths as f64;
    mean_payoff * (-r * t).exp()
}

连续 vs 离散监控

上面的实现是离散监控(每天检查一次)。连续监控的理论价格更便宜——因为更容易触及障碍。

连续监控用布朗桥修正:

代码语言:javascript
复制
use rand::Rng;

fn knockout_call_continuous(
    s0: f64,
    k: f64,
    b: f64,
    r: f64,
    sigma: f64,
    t: f64,
    n_steps: usize,
    n_paths: usize,
) -> f64 {
    let dt = t / n_steps as f64;
    let normal = Normal::new(0.0, 1.0).unwrap();
    let drift = (r - 0.5 * sigma * sigma) * dt;
    let diffusion = sigma * dt.sqrt();

    let payoffs: Vec<f64> = (0..n_paths)
        .into_par_iter()
        .map(|_| {
            let mut rng = StdRng::from_entropy();
            let mut s = s0;
            let mut s_prev = s0;
            let mut knocked_out = false;

            for _ in 0..n_steps {
                let z = normal.sample(&mut rng);
                s = s * (drift + diffusion * z).exp();

                // 布朗桥概率:在 s_prev 和 s 之间触及障碍的概率
                if s_prev < b && s < b {
                    let p_cross = (-2.0 * (b - s_prev) * (b - s) / (sigma * sigma * dt * s_prev * s)).exp();
                    if rng.gen::<f64>() < p_cross {
                        knocked_out = true;
                        break;
                    }
                } else if s >= b {
                    knocked_out = true;
                    break;
                }

                s_prev = s;
            }

            if knocked_out {
                0.0
            } else {
                (s - k).max(0.0)
            }
        })
        .collect();

    let mean_payoff = payoffs.iter().sum::<f64>() / n_paths as f64;
    mean_payoff * (-r * t).exp()
}

情景分析与压力测试

蒙特卡洛不仅是定价工具,更是风险分析工具。

波动率冲击

代码语言:javascript
复制
fn volatility_shock_scenario(
    s0: f64,
    k: f64,
    r: f64,
    base_sigma: f64,
    shock_pct: f64,
    t: f64,
) -> Vec<(f64, f64)> {
    let sigmas = vec![
        base_sigma * (1.0 - shock_pct),
        base_sigma,
        base_sigma * (1.0 + shock_pct),
        base_sigma * (1.0 + 2.0 * shock_pct),
    ];

    sigmas.into_iter()
        .map(|sigma| {
            let price = european_call_mc(s0, k, r, sigma, t, 252, 50_000);
            (sigma, price)
        })
        .collect()
}

多情景批量分析

代码语言:javascript
复制
use anyhow::Result;
use polars::prelude::*;

struct Scenario {
    name: String,
    r: f64,
    sigma: f64,
}

fn scenario_analysis(
    s0: f64,
    k: f64,
    t: f64,
    scenarios: Vec<Scenario>,
) -> Result<DataFrame> {
    let results: Vec<(String, f64)> = scenarios.into_iter()
        .map(|s| {
            let price = european_call_mc(s0, k, s.r, s.sigma, t, 252, 50_000);
            (s.name, price)
        })
        .collect();

    let names: Vec<String> = results.iter().map(|(n, _)| n.clone()).collect();
    let prices: Vec<f64> = results.iter().map(|(_, p)| *p).collect();

    df![
        "scenario" => names,
        "price" => prices,
    ]
}

使用示例:

代码语言:javascript
复制
fn run_scenario_analysis() -> Result<()> {
    let scenarios = vec![
        Scenario { name: "基准".into(), r: 0.05, sigma: 0.2 },
        Scenario { name: "利率上升".into(), r: 0.08, sigma: 0.2 },
        Scenario { name: "波动率飙升".into(), r: 0.05, sigma: 0.4 },
        Scenario { name: "双重冲击".into(), r: 0.08, sigma: 0.4 },
    ];

    let df = scenario_analysis(100.0, 100.0, 1.0, scenarios)?;

    println!("=== 情景分析 ===");
    println!("{}", df);

    Ok(())
}
代码语言:javascript
复制
println!("=== 情景分析 ===");
println!("{}", df);

Ok(())

}

代码语言:javascript
复制

输出:

= 情景分析 = ┌──────────────┬──────────┐ │ scenario │ price │ ╞══════════════╪══════════╡ │ 基准 │ 10.45 │ │ 利率上升 │ 11.23 │ │ 波动率飙升 │ 18.76 │ │ 双重冲击 │ 19.54 │ └──────────────┴──────────┘

代码语言:javascript
复制

---

## VaR 计算:蒙特卡洛法

用模拟计算投资组合的 VaR:

```rust
use rand::thread_rng;
use rand::distributions::Distribution;
use rand_distr::Normal;

fn monte_carlo_var(
    portfolio_value: f64,
    returns_mean: f64,
    returns_std: f64,
    confidence: f64,
    n_simulations: usize,
) -> f64 {
    let normal = Normal::new(returns_mean, returns_std).unwrap();

    let mut terminal_values: Vec<f64> = (0..n_simulations)
        .map(|_| {
            let r = normal.sample(&mut thread_rng());
            portfolio_value * (1.0 + r)
        })
        .collect();

    terminal_values.sort_by(|a, b| a.partial_cmp(b).unwrap());

    let var_index = ((1.0 - confidence) * n_simulations as f64) as usize;
    portfolio_value - terminal_values[var_index]
}

完整期权定价工具箱

代码语言:javascript
复制
pub struct Greeks {
    pub delta: f64,
    pub vega: f64,
}

pub struct OptionPricer {
    s0: f64,
    k: f64,
    r: f64,
    sigma: f64,
    t: f64,
    n_paths: usize,
    n_steps: usize,
}

impl OptionPricer {
    pub fn new(s0: f64, k: f64, r: f64, sigma: f64, t: f64) -> Self {
        Self {
            s0, k, r, sigma, t,
            n_paths: 100_000,
            n_steps: 252,
        }
    }

    pub fn european_call(&self) -> f64 {
        european_call_mc(self.s0, self.k, self.r, self.sigma, self.t,
            self.n_steps, self.n_paths)
    }

    pub fn asian_call(&self) -> f64 {
        asian_call_mc(self.s0, self.k, self.r, self.sigma, self.t,
            self.n_steps, self.n_paths)
    }

    pub fn knockout_call(&self, barrier: f64) -> f64 {
        knockout_call_mc(self.s0, self.k, barrier, self.r, self.sigma, self.t,
            self.n_steps, self.n_paths)
    }

    pub fn greeks(&self) -> Greeks {
        let h = 0.01;

        let delta = {
            let up = OptionPricer::new(self.s0 + h, self.k, self.r, self.sigma, self.t).european_call();
            let down = OptionPricer::new(self.s0 - h, self.k, self.r, self.sigma, self.t).european_call();
            (up - down) / (2.0 * h)
        };

        let vega = {
            let up = OptionPricer::new(self.s0, self.k, self.r, self.sigma + h, self.t).european_call();
            let down = OptionPricer::new(self.s0, self.k, self.r, self.sigma - h, self.t).european_call();
            (up - down) / (2.0 * h)
        };

        Greeks { delta, vega }
    }
}

rust量化合集


核心要点回顾

  1. 1. 欧式期权验证引擎——先对解析解,再信模拟结果
  2. 2. 亚式期权便宜 40%+——平均价格平滑了波动
  3. 3. 障碍期权用布朗桥——连续监控比离散更准
  4. 4. 情景分析是风险管理的瑞士军刀——一个引擎,无限场景

模拟让你看到可能性的全貌,而不只是一个数字。

下一站:推断统计——用样本推断总体,量化不确定性。Sharpe 比率 1.2,你有多大的把握?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust火箭工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • rust量化合集
    • 欧式期权定价:验证引擎正确性
      • Black-Scholes 公式
      • 蒙特卡洛定价
      • 对比验证
    • 亚式期权:路径依赖定价
      • 定价实现
      • 对比欧式期权
    • 障碍期权:路径监控
      • 敲出看涨期权
      • 连续 vs 离散监控
    • 情景分析与压力测试
      • 波动率冲击
      • 多情景批量分析
    • 完整期权定价工具箱
    • 核心要点回顾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档