资产价格模型
预计学习时间:3-4 小时
难度:⭐⭐⭐⭐
核心问题:用什么数学模型描述资产价格的随机运动?不同模型各有什么优缺点?
概述
上一节我们学了随机微积分(工具)和无套利定价理论(思想)。本节把两者结合起来,讨论具体的资产价格模型。
从最简单的 GBM 出发,逐步引入随机波动率、跳跃扩散、利率模型,讨论每个模型的假设、优缺点和适用场景。
一、股票价格模型
1.1 几何布朗运动(GBM)回顾
GBM 是最基础的股票价格模型:
解析解:
GBM 的四条核心假设:
| 假设 | 数学表述 | 白话解读 | 现实中成立吗? |
|---|---|---|---|
| 对数正态 | 股价的对数收益率服从正态分布 | 大致成立,但尾部比正态厚 | |
| 连续交易 | 路径连续无跳跃 | 股价不会突然”跳空” | 不成立(闪崩、跳空开盘) |
| 恒定波动率 | 是常数 | 波动率不随时间和股价变化 | 不成立(波动率微笑) |
| 独立增量 | 收益率之间互不影响 | 今天的收益率不影响明天的 | 大致成立,但存在聚类效应 |
1.2 GBM 的 Python 模拟与参数影响
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm, skew, kurtosis
# ============================================================
# GBM 路径模拟 + 参数敏感性分析
# ============================================================
np.random.seed(42)
S0 = 100.0
T = 1.0
n_steps = 252
dt = T / n_steps
n_paths = 10000
time_points = np.linspace(0, T, n_steps + 1)
def simulate_gbm(S0, mu, sigma, T, n_steps, n_paths, seed=42):
"""模拟 GBM 路径"""
np.random.seed(seed)
dt_local = T / n_steps
Z = np.random.normal(0, 1, size=(n_paths, n_steps))
log_S = np.cumsum((mu - sigma**2/2) * dt_local + sigma * np.sqrt(dt_local) * Z, axis=1)
log_S = np.hstack([np.zeros((n_paths, 1)), log_S])
return S0 * np.exp(log_S)
# ---- 不同参数组合 ----
param_sets = [
{"mu": 0.08, "sigma": 0.15, "label": "低波动蓝筹股"},
{"mu": 0.08, "sigma": 0.30, "label": "中等波动大盘股"},
{"mu": 0.10, "sigma": 0.50, "label": "高波动小盘股"},
{"mu": 0.06, "sigma": 0.80, "label": "极高波动 crypto"},
]
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
for idx, params in enumerate(param_sets):
ax = axes[idx // 2][idx % 2]
S = simulate_gbm(S0, params["mu"], params["sigma"], T, n_steps, n_paths)
# 画 5 条路径
for i in range(5):
ax.plot(time_points, S[i], alpha=0.4, lw=0.8)
# 期望路径
ax.plot(time_points, S0 * np.exp(params["mu"] * time_points), 'k--', lw=2, label='E[S_t]')
# 2 倍标准差包络
std = S0 * np.exp(params["mu"] * time_points) * np.sqrt(np.exp(params["sigma"]**2 * time_points) - 1)
ax.fill_between(time_points,
S0*np.exp(params["mu"]*time_points) - 2*std,
S0*np.exp(params["mu"]*time_points) + 2*std,
alpha=0.1, color='blue')
# 统计
final = S[:, -1]
daily_returns = np.diff(np.log(S), axis=1).flatten()
ax.set_title(f'{params["label"]}\n'
f'μ={params["mu"]}, σ={params["sigma"]}, '
f'E[S_T]={final.mean():.1f}, '
f'偏度={skew(daily_returns):.2f}, '
f'超额峰度={kurtosis(daily_returns):.2f}')
ax.set_xlabel('时间(年)')
ax.set_ylabel('股价')
ax.legend(fontsize=8)
plt.tight_layout()
plt.show()
# ---- GBM 收益率统计特征 ----
print("=== GBM 收益率统计特征 ===")
S = simulate_gbm(S0, 0.08, 0.30, T, n_steps, n_paths)
daily_returns = np.diff(np.log(S), axis=1).flatten()
print(f"均值:{daily_returns.mean():.6f}(理论 {(0.08-0.045)/252:.6f})")
print(f"标准差:{daily_returns.std():.6f}(理论 {0.30/np.sqrt(252):.6f})")
print(f"偏度:{skew(daily_returns):.4f}(理论 ≈ 0)")
print(f"超额峰度:{kurtosis(daily_returns):.4f}(理论 ≈ 0)")
print()
print("注意:GBM 的收益率是正态分布,偏度≈0,峰度≈0")
print("而真实金融数据的峰度通常远大于 0(厚尾)")1.3 为什么 GBM 不完美
问题一:波动率微笑(Volatility Smile)
如果 GBM 完全正确,那么用同一只股票的不同行权价期权反推出的”隐含波动率”应该是一样的。但实际上:
- 虚值看跌期权的隐含波动率偏高
- 深度虚值期权的隐含波动率更高
- 呈现”微笑”或”偏斜”的形状
原因:真实市场对”暴跌”的恐惧超过了对”暴涨”的期待。GBM 假设波动率恒定,无法捕捉这种不对称性。
问题二:厚尾(Fat Tails)
GBM 假设对数收益率服从正态分布。但真实数据中:
- 极端事件(涨跌超过 4 个标准差)出现的频率远超正态分布的预测
- 1987 年黑色星期一的跌幅,在正态假设下是”几十亿年一遇”的事件
问题三:波动率聚类(Volatility Clustering)
GBM 假设每天的波动率独立。但现实中:
- 高波动之后往往跟着高波动
- 低波动之后往往跟着低波动
- 这被称为”波动率聚集”或”GARCH 效应”
问题四:跳空(Jumps)
GBM 假设路径连续。但现实中:
- 重大新闻发布后,开盘价可能和前日收盘价相差巨大
- 闪崩、熔断都是”跳跃”的表现
- GBM 无法刻画这种不连续的价格变动
二、随机波动率模型
2.1 为什么需要随机波动率?
白话解释:GBM 假设波动率 是常数。但看看真实市场的波动率指数(VIX),它本身就是一个剧烈波动的随机变量。
核心思想:让波动率本身也服从一个随机过程。
2.2 Heston 模型
Heston 模型是最著名的随机波动率模型之一:
每个参数的白话解释:
| 参数 | 符号 | 含义 | 白话解读 | 典型值 |
|---|---|---|---|---|
| 长期方差 | 波动率的长期均衡值 | 波动率最终会回归到这个水平 | 0.04(对应 ) | |
| 回归速度 | 波动率回归均值的速度 | 像弹簧的硬度, 大则回归快 | 2.0 | |
| 方差波动率 | 波动率本身的波动程度 | ”波动的波动”, 大则波动率变化剧烈 | 0.3 | |
| 方差初始值 | 当前的波动率平方 | 现在的波动率是多少 | 0.04 | |
| 相关性 | 股价和波动率变化的相关性 | 通常为负(股价跌 → 波动率升),即”杠杆效应” | -0.7 |
两个布朗运动的相关性:
的金融含义(杠杆效应):
- 股价下跌时,公司的杠杆率升高(负债/权益比变大),风险增加
- 风险增加 → 波动率上升
- 所以股价和波动率往往负相关
2.3 Heston 模型的 Python 模拟
import numpy as np
import matplotlib.pyplot as plt
# ============================================================
# Heston 随机波动率模型模拟
# dS = mu*S*dt + sqrt(v)*S*dW1
# dv = kappa*(theta - v)*dt + xi*sqrt(v)*dW2
# dW1*dW2 = rho*dt
# ============================================================
np.random.seed(42)
# 参数
S0 = 100.0 # 初始股价
v0 = 0.04 # 初始方差(对应 σ = 20%)
mu = 0.08 # 股价漂移率
kappa = 2.0 # 方差回归速度
theta = 0.04 # 长期方差
xi = 0.3 # 方差波动率
rho = -0.7 # 股价-波动率相关性
T = 2.0 # 模拟 2 年
n_steps = 504 # 约 2 年交易日
dt = T / n_steps
time_points = np.linspace(0, T, n_steps + 1)
n_paths = 5
def simulate_heston(S0, v0, mu, kappa, theta, xi, rho, T, n_steps, n_paths, seed=42):
"""模拟 Heston 模型路径(Euler-Maruyama 方法)"""
np.random.seed(seed)
dt_local = T / n_steps
S = np.zeros((n_paths, n_steps + 1))
v = np.zeros((n_paths, n_steps + 1))
S[:, 0] = S0
v[:, 0] = v0
for t in range(n_steps):
# 生成相关的布朗运动增量
Z1 = np.random.normal(0, 1, n_paths)
Z2 = np.random.normal(0, 1, n_paths)
# dW2 = rho*dW1 + sqrt(1-rho^2)*Z2
dW1 = np.sqrt(dt_local) * Z1
dW2 = rho * dW1 + np.sqrt(1 - rho**2) * np.sqrt(dt_local) * Z2
# Euler-Maruyama 离散化
sqrt_v = np.sqrt(np.maximum(v[:, t], 0)) # 防止方差为负
S[:, t+1] = S[:, t] * (1 + mu * dt_local + sqrt_v * dW1)
v[:, t+1] = v[:, t] + kappa * (theta - v[:, t]) * dt_local + xi * sqrt_v * dW2
v[:, t+1] = np.maximum(v[:, t+1], 0) # Feller 条件不满足时截断
return S, v
# 模拟
S_heston, v_heston = simulate_heston(S0, v0, mu, kappa, theta, xi, rho, T, n_steps, n_paths)
# ---- 对比 GBM(用平均波动率)----
sigma_avg = np.sqrt(theta)
S_gbm = simulate_gbm(S0, mu, sigma_avg, T, n_steps, n_paths, seed=42)
# ---- 绘图 ----
fig, axes = plt.subplots(2, 2, figsize=(16, 10))
# 1. Heston 路径
for i in range(n_paths):
axes[0, 0].plot(time_points, S_heston[i], alpha=0.7, lw=1)
axes[0, 0].set_title('Heston 模型:股价路径')
axes[0, 0].set_xlabel('时间(年)')
axes[0, 0].set_ylabel('股价')
# 2. 波动率路径
for i in range(n_paths):
axes[0, 1].plot(time_points, np.sqrt(v_heston[i]) * 100, alpha=0.7, lw=1)
axes[0, 1].axhline(y=np.sqrt(theta)*100, color='black', linestyle='--', lw=2,
label=f'长期波动率 √θ = {np.sqrt(theta)*100:.0f}%')
axes[0, 1].set_title('Heston 模型:波动率路径')
axes[0, 1].set_xlabel('时间(年)')
axes[0, 1].set_ylabel('波动率(%)')
axes[0, 1].legend()
# 3. Heston vs GBM 对比
axes[1, 0].plot(time_points, S_heston[0], color='#e41a1c', lw=1.5, label='Heston')
axes[1, 0].plot(time_points, S_gbm[0], color='#377eb8', lw=1.5, linestyle='--', label='GBM')
axes[1, 0].set_title('Heston vs GBM(同一条随机种子)')
axes[1, 0].legend()
axes[1, 0].set_xlabel('时间(年)')
# 4. 收益率分布对比
# 模拟更多路径来对比分布
S_h_large, _ = simulate_heston(S0, v0, mu, kappa, theta, xi, rho, T, n_steps, 5000, seed=123)
S_g_large = simulate_gbm(S0, mu, sigma_avg, T, n_steps, 5000, seed=123)
log_ret_h = np.diff(np.log(S_h_large), axis=1).flatten()
log_ret_g = np.diff(np.log(S_g_large), axis=1).flatten()
axes[1, 1].hist(log_ret_g, bins=80, density=True, alpha=0.4, color='#377eb8', label='GBM')
axes[1, 1].hist(log_ret_h, bins=80, density=True, alpha=0.4, color='#e41a1c', label='Heston')
axes[1, 1].set_title(f'收益率分布对比\n'
f'GBM 峰度={kurtosis(log_ret_g):.2f}, Heston 峰度={kurtosis(log_ret_h):.2f}')
axes[1, 1].legend()
axes[1, 1].set_xlabel('日对数收益率')
plt.tight_layout()
plt.show()
print("=== Heston vs GBM 对比 ===")
print(f"Heston 收益率偏度: {skew(log_ret_h):.4f}")
print(f"GBM 收益率偏度: {skew(log_ret_g):.4f}")
print(f"Heston 收益率峰度: {kurtosis(log_ret_h):.4f}")
print(f"GBM 收益率峰度: {kurtosis(log_ret_g):.4f}")
print(f"(Heston 的峰度更大,更接近真实市场的厚尾特征)")2.4 SABR 模型简介
SABR(Stochastic Alpha Beta Rho)模型是业界广泛使用的波动率曲面模型:
| 参数 | 含义 | 白话解读 |
|---|---|---|
| 当前波动率水平 | ”波动的烈度” | |
| CEV 指数(通常取 0 或 1) | 是对数正态, 是正常态 | |
| 波动率的波动率 | ”波动的波动” | |
| 远期价格和波动率的相关性 | 通常为负 |
SABR 的优势:有近似的封闭公式可以直接拟合波动率曲面,计算速度快,适合做市商实时报价。
三、跳跃扩散模型
3.1 为什么需要跳跃?
白话解释:看看真实的股价图,你会发现价格不是”平滑”变化的。有时候一条重大新闻出来,开盘价直接跳空 5%。GBM 和 Heston 都无法捕捉这种现象。
跳跃的现实例子:
- 财报超预期/低于预期 → 开盘跳空
- 突然的政策变化 → 全市场跳空
- 闪崩 → 几分钟内跌 5%+
- 地缘政治事件 → 恐慌性抛售
3.2 Merton 跳跃扩散模型
在 GBM 的基础上叠加一个”跳跃过程”:
每一项的白话解释:
| 项 | 含义 | 白话解读 |
|---|---|---|
| 漂移项(扣除跳跃补偿) | 长期平均趋势(已扣除跳跃的平均影响) | |
| 扩散项 | 日常的连续波动 | |
| 跳跃项 | 突然的、不连续的价格变动 |
跳跃过程的参数:
| 参数 | 符号 | 含义 |
|---|---|---|
| 跳跃频率 | 单位时间内跳跃的平均次数(强度) | |
| 跳跃幅度 | 跳跃大小的分布(通常是对数正态) | |
| 平均跳跃补偿 | 跳跃对期望收益的平均影响 |
白话解读:
- :平均每年发生几次大跳跃
- :跳跃幅度平均是涨还是跌(通常 ,因为暴跌比暴涨更猛烈)
- :跳跃幅度的不确定性
3.3 跳跃对定价的影响
跳跃扩散模型下的期权价格没有简单的封闭公式,但可以用蒙特卡洛模拟。
跳跃的关键影响:
- 虚值看跌期权变贵:因为有”暴跌”的可能性,保护性的看跌期权更有价值
- 隐含波动率出现微笑:深度虚值期权的隐含波动率更高
- 更厚的尾部:跳跃使得极端事件更频繁
3.4 Merton 模型的 Python 模拟
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import skew, kurtosis
# ============================================================
# Merton 跳跃扩散模型模拟
# dS = (mu - lambda*kappa)*S*dt + sigma*S*dW + S*J*dN
# ============================================================
np.random.seed(42)
# 参数
S0 = 100.0
mu = 0.08 # 真实漂移率
sigma = 0.20 # 扩散波动率(日常波动)
lam = 3.0 # 每年平均跳跃 3 次
mu_J = -0.03 # 跳跃幅度均值(平均跳跌 3%)
sigma_J = 0.05 # 跳跃幅度标准差
kappa = np.exp(mu_J + sigma_J**2/2) - 1 # 平均跳跃补偿
T = 2.0
n_steps = 504
dt = T / n_steps
time_points = np.linspace(0, T, n_steps + 1)
n_paths = 5000
def simulate_merton(S0, mu, sigma, lam, mu_J, sigma_J, T, n_steps, n_paths, seed=42):
"""模拟 Merton 跳跃扩散模型"""
np.random.seed(seed)
dt_local = T / n_steps
drift = mu - lam * kappa # 调整后的漂移率
S = np.zeros((n_paths, n_steps + 1))
S[:, 0] = S0
for t in range(n_steps):
# 布朗运动增量
dW = np.sqrt(dt_local) * np.random.normal(0, 1, n_paths)
# 泊松跳跃
N_jump = np.random.poisson(lam * dt_local, n_paths) # 每步跳跃次数
J = np.random.normal(mu_J, sigma_J, n_paths) * N_jump # 跳跃幅度(可能为 0)
# 价格更新
S[:, t+1] = S[:, t] * np.exp(drift * dt_local - 0.5 * sigma**2 * dt_local + sigma * dW + J)
return S
# 模拟
S_merton = simulate_merton(S0, mu, sigma, lam, mu_J, sigma_J, T, n_steps, n_paths)
S_gbm = simulate_gbm(S0, mu, sigma, T, n_steps, n_paths, seed=42)
# ---- 绘图 ----
fig, axes = plt.subplots(2, 2, figsize=(16, 10))
# 1. 典型路径对比
for i in range(3):
axes[0, 0].plot(time_points, S_merton[i], color='#e41a1c', alpha=0.7, lw=1, label='Merton' if i==0 else '')
axes[0, 0].plot(time_points, S_gbm[i], color='#377eb8', alpha=0.7, lw=1, linestyle='--',
label='GBM' if i==0 else '')
axes[0, 0].set_title('Merton 跳跃扩散 vs GBM')
axes[0, 0].legend()
axes[0, 0].set_xlabel('时间(年)')
axes[0, 0].set_ylabel('股价')
# 2. 放大看跳跃
axes[0, 1].plot(time_points[:100], S_merton[0, :100], color='#e41a1c', lw=2)
axes[0, 1].plot(time_points[:100], S_gbm[0, :100], color='#377eb8', lw=2, linestyle='--')
axes[0, 1].set_title('局部放大:注意 Merton 路径的"跳跃"')
axes[0, 1].set_xlabel('时间(年)')
# 3. 收益率分布对比
log_ret_m = np.diff(np.log(S_merton), axis=1).flatten()
log_ret_g = np.diff(np.log(S_gbm), axis=1).flatten()
bins = np.linspace(-0.15, 0.15, 120)
axes[1, 0].hist(log_ret_g, bins=bins, density=True, alpha=0.5, color='#377eb8', label='GBM')
axes[1, 0].hist(log_ret_m, bins=bins, density=True, alpha=0.5, color='#e41a1c', label='Merton')
axes[1, 0].set_title('收益率分布对比')
axes[1, 0].set_xlabel('日对数收益率')
axes[1, 0].legend()
# 4. 尾部放大
axes[1, 1].hist(log_ret_g, bins=bins, density=True, alpha=0.5, color='#377eb8', label='GBM')
axes[1, 1].hist(log_ret_m, bins=bins, density=True, alpha=0.5, color='#e41a1c', label='Merton')
axes[1, 1].set_xlim(-0.12, -0.02)
axes[1, 1].set_title('左尾部放大:Merton 的厚尾更明显')
axes[1, 1].set_xlabel('日对数收益率')
axes[1, 1].legend()
plt.tight_layout()
plt.show()
print("=== Merton vs GBM 统计对比 ===")
print(f"Merton 偏度: {skew(log_ret_m):.4f}(更负,因为跳跃偏向暴跌)")
print(f"GBM 偏度: {skew(log_ret_g):.4f}")
print(f"Merton 峰度: {kurtosis(log_ret_m):.4f}(更大,尾部更厚)")
print(f"GBM 峰度: {kurtosis(log_ret_g):.4f}")
print()
# 尾部概率
threshold = -0.05 # 日跌超过 5%
print(f"P(日跌幅 > 5%):")
print(f" Merton: {(log_ret_m < threshold).mean():.6f}")
print(f" GBM: {(log_ret_g < threshold).mean():.6f}")
print(f"(Merton 的极端下跌概率显著高于 GBM)")四、利率模型
4.1 为什么利率模型不同于股票模型?
利率和股价有三个本质区别:
| 维度 | 股价 | 利率 |
|---|---|---|
| 是否有均值回归 | 通常假设没有(GBM) | 有(利率会围绕长期水平波动) |
| 是否可以为负 | 通常不能(对数正态假设) | 理论上可以为负(但很多模型限制为非负) |
| 波动率特征 | 和利率水平关系不大 | 波动率通常和利率水平正相关 |
白话解读:
- 股价没有”天花板”,可以无限上涨,所以用 GBM
- 利率不会无限上涨也不会无限下跌,央行会调控,所以有均值回归
- 利率理论上可以为负(欧洲、日本有过负利率),但大多数模型假设非负
4.2 Vasicek 模型
每个参数的白话解释:
| 参数 | 含义 | 白话解读 | 典型值 |
|---|---|---|---|
| 长期平均利率 | 利率的”家”在哪里 | 0.05(5%) | |
| 回归速度 | 利率偏离后多快回归 | 0.5 | |
| 利率波动率 | 利率的不确定性 | 0.01(1%) |
Vasicek 的优点:
- 有解析解(方便定价)
- 利率有均值回归(符合直觉)
Vasicek 的缺点:
- 利率可能变为负值(在极端情况下)
- 波动率是常数,不随利率水平变化
白话解读:Vasicek 模型就像 OU 过程——利率像一个被弹簧拉住的粒子,围绕长期均值 来回波动。问题是有时候会”弹过头”变成负数。
4.3 CIR 模型
Cox-Ingersoll-Ross(CIR)模型解决了利率可以为负的问题:
和 Vasicek 的唯一区别:扩散项从 变成了 。
白话解读:利率越低,随机波动的幅度越小。当利率接近 0 时,,利率”被推离”负值的力量极小。这保证了利率永远不会变成负数(只要满足 Feller 条件 )。
import numpy as np
import matplotlib.pyplot as plt
# ============================================================
# Vasicek vs CIR 利率模型对比
# ============================================================
np.random.seed(42)
# 参数
r0 = 0.05 # 初始利率 5%
a = 0.5 # 回归速度
b = 0.05 # 长期均值 5%
sigma = 0.02 # 波动率 2%
T = 20.0 # 模拟 20 年
n_steps = 2000
dt = T / n_steps
time_points = np.linspace(0, T, n_steps + 1)
n_paths = 5
# ---- Vasicek 模型 ----
r_vasicek = np.zeros((n_paths, n_steps + 1))
r_vasicek[:, 0] = r0
for t in range(n_steps):
Z = np.random.normal(0, 1, n_paths)
r_vasicek[:, t+1] = r_vasicek[:, t] + a * (b - r_vasicek[:, t]) * dt + sigma * np.sqrt(dt) * Z
# ---- CIR 模型 ----
r_cir = np.zeros((n_paths, n_steps + 1))
r_cir[:, 0] = r0
for t in range(n_steps):
Z = np.random.normal(0, 1, n_paths)
sqrt_r = np.sqrt(np.maximum(r_cir[:, t], 0))
r_cir[:, t+1] = r_cir[:, t] + a * (b - r_cir[:, t]) * dt + sigma * sqrt_r * np.sqrt(dt) * Z
r_cir[:, t+1] = np.maximum(r_cir[:, t+1], 0) # 额外保险
# ---- 绘图 ----
fig, axes = plt.subplots(2, 2, figsize=(16, 10))
# 1. Vasicek 路径
for i in range(n_paths):
axes[0, 0].plot(time_points, r_vasicek[i] * 100, alpha=0.7, lw=1)
axes[0, 0].axhline(y=b*100, color='red', linestyle='--', lw=2, label=f'长期均值 b={b*100}%')
axes[0, 0].axhline(y=0, color='black', linestyle='-', lw=1)
axes[0, 0].set_title('Vasicek 模型(利率可能为负)')
axes[0, 0].set_xlabel('时间(年)')
axes[0, 0].set_ylabel('利率(%)')
axes[0, 0].legend()
# 2. CIR 路径
for i in range(n_paths):
axes[0, 1].plot(time_points, r_cir[i] * 100, alpha=0.7, lw=1)
axes[0, 1].axhline(y=b*100, color='red', linestyle='--', lw=2, label=f'长期均值 b={b*100}%')
axes[0, 1].set_title('CIR 模型(利率始终非负)')
axes[0, 1].set_xlabel('时间(年)')
axes[0, 1].set_ylabel('利率(%)')
axes[0, 1].legend()
# 3. 终点分布对比
# 大量路径看分布
n_large = 10000
r_vas = np.zeros(n_large)
r_ci = np.zeros(n_large)
for _ in range(n_large):
for t in range(n_steps):
r_vas = r_vas + a*(b-r_vas)*dt + sigma*np.sqrt(dt)*np.random.normal()
sqrt_r = np.sqrt(max(r_ci, 0))
r_ci = r_ci + a*(b-r_ci)*dt + sigma*sqrt_r*np.sqrt(dt)*np.random.normal()
r_ci = max(r_ci, 0)
# 用向量化方式重新模拟
r_vas_large = np.zeros((n_large, n_steps + 1))
r_vas_large[:, 0] = r0
for t in range(n_steps):
Z = np.random.normal(0, 1, n_large)
r_vas_large[:, t+1] = r_vas_large[:, t] + a*(b-r_vas_large[:, t])*dt + sigma*np.sqrt(dt)*Z
r_cir_large = np.zeros((n_large, n_steps + 1))
r_cir_large[:, 0] = r0
for t in range(n_steps):
Z = np.random.normal(0, 1, n_large)
sqrt_r = np.sqrt(np.maximum(r_cir_large[:, t], 0))
r_cir_large[:, t+1] = r_cir_large[:, t] + a*(b-r_cir_large[:, t])*dt + sigma*sqrt_r*np.sqrt(dt)*Z
r_cir_large[:, t+1] = np.maximum(r_cir_large[:, t+1], 0)
axes[1, 0].hist(r_vas_large[:, -1]*100, bins=60, density=True, alpha=0.5,
color='#e41a1c', label='Vasicek')
axes[1, 0].hist(r_cir_large[:, -1]*100, bins=60, density=True, alpha=0.5,
color='#377eb8', label='CIR')
axes[1, 0].axvline(x=0, color='black', linestyle='-', lw=2)
axes[1, 0].set_title('20 年后利率分布')
axes[1, 0].set_xlabel('利率(%)')
axes[1, 0].legend()
# 4. 波动率随利率水平变化(CIR 特性)
# CIR 模型中,波动率 = sigma * sqrt(r),所以低利率时波动率更小
axes[1, 1].plot(time_points, np.sqrt(r_vasicek[0]) * sigma, color='#e41a1c', lw=1,
label='Vasicek: σ (常数)')
axes[1, 1].plot(time_points, np.sqrt(r_cir[0]) * sigma, color='#377eb8', lw=1,
label='CIR: σ√r')
axes[1, 1].set_title('波动率水平:Vasicek 恒定 vs CIR 随利率变化')
axes[1, 1].set_xlabel('时间(年)')
axes[1, 1].set_ylabel('瞬时波动率')
axes[1, 1].legend()
plt.tight_layout()
plt.show()
print("=== Vasicek vs CIR 对比 ===")
print(f"Vasicek 负利率概率: {(r_vas_large[:, -1] < 0).mean():.2%}")
print(f"CIR 负利率概率: {(r_cir_large[:, -1] < 0).mean():.2%}")
print(f"Feller 条件 2ab >= σ²: {2*a*b:.4f} >= {sigma**2:.4f} → {'满足' if 2*a*b >= sigma**2 else '不满足'}")4.4 为什么选择不同的利率模型?
| 模型 | 利率可负? | 解析解? | 适用场景 |
|---|---|---|---|
| Vasicek | 可能为负 | 有(债券价格有封闭公式) | 欧洲利率市场、简单定价 |
| CIR | 保证非负 | 有(有限制) | 非负利率约束重要时 |
| Hull-White | 可能为负 | 有(可拟合初始期限结构) | 需要精确匹配当前利率曲线时 |
| Black-Karasinski | 保证非负 | 无(需要数值方法) | 对数正态利率建模 |
五、Python 综合对比
5.1 不同模型路径对比
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import skew, kurtosis
# ============================================================
# 综合对比:GBM vs Heston vs Merton vs 跳跃扩散
# ============================================================
np.random.seed(42)
# 共享参数
S0 = 100.0
mu = 0.08
sigma_base = 0.25
T = 1.0
n_steps = 252
dt = T / n_steps
time_points = np.linspace(0, T, n_steps + 1)
n_paths = 10000
# ---- 1. GBM ----
Z_base = np.random.normal(0, 1, size=(n_paths, n_steps))
log_S_gbm = np.cumsum((mu - sigma_base**2/2)*dt + sigma_base*np.sqrt(dt)*Z_base, axis=1)
log_S_gbm = np.hstack([np.zeros((n_paths, 1)), log_S_gbm])
S_gbm = S0 * np.exp(log_S_gbm)
# ---- 2. Heston(简化版)----
kappa_h, theta_h, xi_h, rho_h, v0_h = 2.0, sigma_base**2, 0.5, -0.7, sigma_base**2
S_heston = np.zeros((n_paths, n_steps + 1))
v_heston = np.zeros((n_paths, n_steps + 1))
S_heston[:, 0] = S0
v_heston[:, 0] = v0_h
for t in range(n_steps):
Z1 = Z_base[:, t]
Z2 = np.random.normal(0, 1, n_paths)
dW1 = np.sqrt(dt) * Z1
dW2 = rho_h * dW1 + np.sqrt(1 - rho_h**2) * np.sqrt(dt) * Z2
sqrt_v = np.sqrt(np.maximum(v_heston[:, t], 0))
S_heston[:, t+1] = S_heston[:, t] * (1 + mu*dt + sqrt_v*dW1)
v_heston[:, t+1] = v_heston[:, t] + kappa_h*(theta_h - v_heston[:, t])*dt + xi_h*sqrt_v*dW2
v_heston[:, t+1] = np.maximum(v_heston[:, t+1], 0)
# ---- 3. Merton ----
lam_m, mu_J_m, sigma_J_m = 3.0, -0.02, 0.04
kappa_m = np.exp(mu_J_m + sigma_J_m**2/2) - 1
drift_m = mu - lam_m * kappa_m
S_merton = np.zeros((n_paths, n_steps + 1))
S_merton[:, 0] = S0
for t in range(n_steps):
dW = np.sqrt(dt) * Z_base[:, t]
N_jump = np.random.poisson(lam_m * dt, n_paths)
J = np.random.normal(mu_J_m, sigma_J_m, n_paths) * N_jump
S_merton[:, t+1] = S_merton[:, t] * np.exp(drift_m*dt - 0.5*sigma_base**2*dt + sigma_base*dW + J)
# ---- 对比统计 ----
models = {
'GBM': S_gbm,
'Heston': S_heston,
'Merton': S_merton,
}
print("=" * 70)
print(f"{'模型':<10} {'E[S_T]':>10} {'中位数':>10} {'偏度':>10} {'峰度':>10} {'P(跌>20%)':>10}")
print("=" * 70)
for name, S in models.items():
final = S[:, -1]
log_ret = np.diff(np.log(S), axis=1).flatten()
drop_prob = (final < S0 * 0.8).mean()
print(f"{name:<10} {final.mean():>10.2f} {np.median(final):>10.2f} "
f"{skew(log_ret):>10.4f} {kurtosis(log_ret):>10.4f} {drop_prob:>10.4f}")
print("=" * 70)
# ---- 可视化 ----
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
for idx, (name, S) in enumerate(models.items()):
# 路径
for i in range(3):
axes[0, idx].plot(time_points, S[i], alpha=0.6, lw=1)
axes[0, idx].set_title(f'{name} 模型')
axes[0, idx].set_xlabel('时间(年)')
axes[0, idx].set_ylabel('股价')
# 收益率分布
log_ret = np.diff(np.log(S), axis=1).flatten()
axes[1, idx].hist(log_ret, bins=80, density=True, alpha=0.6, color='steelblue')
axes[1, idx].set_title(f'{name} 收益率\n偏度={skew(log_ret):.3f}, 峰度={kurtosis(log_ret):.3f}')
axes[1, idx].set_xlabel('日对数收益率')
plt.tight_layout()
plt.show()5.2 模型选择决策指南
# ============================================================
# 模型选择决策树(概念性代码)
# ============================================================
def choose_model(question):
"""根据你的问题选择合适的模型"""
if question["资产类型"] == "利率":
if question["需要非负保证"]:
if question["需要匹配当前曲线"]:
return "Hull-White (扩展 CIR)"
else:
return "CIR"
else:
return "Vasicek"
elif question["资产类型"] == "股票":
if question["关注短期定价"]:
if question["需要精确波动率曲面"]:
return "SABR 或 Heston"
else:
return "Black-Scholes (GBM)"
else:
if question["有跳跃风险"]:
if question["有随机波动率"]:
return "SVJ (Stochastic Volatility + Jumps)"
else:
return "Merton Jump-Diffusion"
else:
if question["波动率变化大"]:
return "Heston"
else:
return "GBM"
else:
return "需要更具体的模型分析"
# 示例
scenarios = [
{"资产类型": "股票", "关注短期定价": True, "需要精确波动率曲面": True},
{"资产类型": "股票", "关注短期定价": False, "有跳跃风险": True, "有随机波动率": False},
{"资产类型": "利率", "需要非负保证": True, "需要匹配当前曲线": False},
{"资产类型": "股票", "关注短期定价": False, "有跳跃风险": False, "波动率变化大": True},
]
for s in scenarios:
print(f"场景 {s} → 推荐: {choose_model(s)}")六、本节要点回顾
模型对比总结
| 模型 | SDE | 核心优势 | 核心缺陷 | 适用场景 |
|---|---|---|---|---|
| GBM | 简单、有解析解 | 恒定波动率、无跳跃 | 基础定价教学、简单估算 | |
| Heston | , | 波动率微笑、厚尾 | 参数多、校准复杂 | 期权定价、波动率交易 |
| SABR | , | 波动率曲面拟合快 | 无解析解、路径模拟复杂 | 做市商报价、IV曲面 |
| Merton | 捕捉跳跃 | 跳跃频率难估计 | 带有事件风险的定价 | |
| Vasicek | 有解析解、均值回归 | 利率可能为负 | 简单利率衍生品 | |
| CIR | 利率非负 | 灵活性较低 | 非负利率重要时 |
选择模型的实用原则
- 从简单开始:先用 GBM,看结果是否合理
- 根据问题选模型:不是”最好的模型”而是”最适合你问题的模型”
- 了解局限性:每个模型都有假设,知道什么时候会失效比知道怎么用更重要
- 参数比模型更重要:好的参数估计比换一个更复杂的模型往往更有价值
- 组合使用:实践中经常用 GBM 做快速估算,用 Heston 做精确定价
本模块(数理金融)到此结束。 你已经掌握了传统量化的理论基石——随机微积分(工具)、无套利定价(思想)、资产价格模型(应用)。接下来可以继续学习计量经济学模块,或者如果对衍生品定价感兴趣,可以直接进入衍生品定价模块。