第四章 历史模拟法VaR:非参数方法、历史数据重采样、排序与分位数计算

说到VaR计算,很多新手第一反应就是“正态分布假设”。但说实话,金融市场哪有那么乖?我见过太多肥尾分布的例子了——2008年、2020年,哪次黑天鹅事件不是把正态分布的脸打得啪啪响?

所以这一章,我们来聊聊历史模拟法。它不假设任何分布,完全靠历史数据说话。说白了,就是“历史会重演”这个朴素逻辑。

4.1 历史模拟法的核心思想

历史模拟法(Historical Simulation)是非参数方法里最经典的一种。它不做任何分布假设,直接拿过去N天的收益率数据,排序后取分位数。

举个例子:你手里有过去1000天的日收益率数据。想算95%置信水平下的VaR?那就把这1000个收益率从小到大排个序,取第50个(也就是5%分位数)的值。就这么简单粗暴。

为什么我喜欢这个方法? 因为它在项目中真的省心。有一次我给某券商做压力测试,对方风控总监上来就问:“你们用的是什么分布?”我说“历史模拟法”,他愣了一下,然后说“好,这个我们审计能过”。你看,监管机构其实也认可这种透明的方法。

核心公式:

VaRα = -Percentile(R1, R2, ..., RN, 1-α)

其中Ri是历史收益率,α是置信水平(如95%),N是历史窗口长度。

4.2 数据重采样:别让坏数据毁了你的模型

历史模拟法有个致命弱点:它对数据质量极其敏感。我踩过这个坑——有一次用某交易所的日频数据,结果发现里面有三天连续停牌的数据没处理,算出来的VaR直接偏了20%。

所以数据重采样这一步,我建议你认真对待。常见的做法有:

  • 等间隔重采样:把不规则的时间序列变成固定频率(比如日频、周频)
  • 滚动窗口重采样:用过去250个交易日作为窗口,每天滚动计算
  • Bootstrap重采样:有放回地抽取历史数据,生成新的样本路径

我的经验: 做Bootstrap时,记得保持时间序列的自相关性。我曾经直接随机打乱数据,结果VaR被严重低估了。后来改用“块状Bootstrap”(Block Bootstrap),才解决了这个问题。

4.3 排序与分位数计算:Python实现

好了,理论说完了,咱们直接上代码。这部分我习惯用NumPy和Pandas,效率高、代码也干净。

import numpy as np
import pandas as pd

# 模拟生成1000天的收益率数据(实际项目中从数据库读取)
np.random.seed(42)
returns = np.random.normal(0.001, 0.02, 1000)

# 历史模拟法计算VaR
def historical_var(returns, confidence_level=0.95):
    """
    历史模拟法VaR计算
    :param returns: 收益率序列
    :param confidence_level: 置信水平,默认95%
    :return: VaR值(正数表示损失)
    """
    sorted_returns = np.sort(returns)
    index = int((1 - confidence_level) * len(sorted_returns))
    var = -sorted_returns[index]
    return var

# 计算95% VaR
var_95 = historical_var(returns, 0.95)
print(f"95% VaR (历史模拟法): {var_95:.4f}")

# 计算99% VaR
var_99 = historical_var(returns, 0.99)
print(f"99% VaR (历史模拟法): {var_99:.4f}")

这段代码看着简单,但有几个细节要注意:

  1. 排序方向:我习惯从小到大排,这样取左边尾部分位数就是损失
  2. 索引计算:95%置信水平对应5%分位数,所以索引是 (1-0.95)*N
  3. 负号处理:VaR通常表示为正数,所以收益率是负值时要取反

曾经踩过的坑: 有一次我直接用Pandas的quantile()函数,没注意它默认是线性插值。对于小样本数据,线性插值会平滑掉极端值,导致VaR被低估。后来我改用numpy.percentile()并设置method='lower',才得到正确结果。

4.4 滚动窗口VaR:动态风险监控

静态VaR只能告诉你“过去N天的风险水平”,但市场是动态的。我建议你做一个滚动窗口VaR,每天更新。

def rolling_historical_var(returns, window=250, confidence_level=0.95):
    """
    滚动窗口历史模拟法VaR
    :param returns: 收益率序列
    :param window: 滚动窗口大小,默认250个交易日(约1年)
    :param confidence_level: 置信水平
    :return: 每日VaR序列
    """
    var_series = []
    for i in range(window, len(returns)):
        window_returns = returns[i-window:i]
        var = historical_var(window_returns, confidence_level)
        var_series.append(var)
    return pd.Series(var_series, index=returns.index[window:])

# 计算滚动VaR
rolling_var = rolling_historical_var(returns, window=250, confidence_level=0.95)
print(f"滚动VaR均值: {rolling_var.mean():.4f}")
print(f"滚动VaR最大值: {rolling_var.max():.4f}")

你想想看,如果滚动VaR突然飙升,说明市场波动在加剧。这时候就该启动压力测试了。

4.5 历史模拟法的优缺点

优点 缺点
无需分布假设,完全基于数据 对数据质量要求极高
实现简单,代码量少 无法捕捉未发生过的极端事件
监管机构认可度高 历史窗口长度选择主观性强
可以捕捉非线性风险 对市场结构变化不敏感

我的建议: 历史模拟法适合作为基准模型。在实际项目中,我通常用它做第一道防线,再配合参数法或蒙特卡洛模拟做交叉验证。这样既能保证透明度,又能覆盖极端风险。

4.6 知识体系图

下面这张图展示了本章的核心逻辑。从原始数据开始,经过重采样、排序、分位数计算,最终得到VaR值。每一步都有坑,每一步也都有优化空间。

历史模拟法VaR计算流程 原始收益率数据 数据重采样 等间隔/滚动/Bootstrap 收益率排序 分位数计算 取(1-α)分位数 VaR值输出 ⚠️ 注意事项 • 数据清洗:处理缺失值、异常值、停牌数据 • 窗口选择:250天(1年)是行业惯例,但可根据产品流动性调整 • 分位数方法:小样本时注意插值方式,建议用lower或higher

嗯,这张图把整个流程串起来了。你从左边开始,一步步走到右边,每一步都有对应的Python代码实现。我个人建议你把这张图打印出来贴在工位上,写代码时对照着看,不容易出错。

实战小技巧: 在做历史模拟法时,我习惯同时计算多个置信水平(90%、95%、99%)的VaR,然后画成曲线图。如果三条曲线突然发散,说明市场结构可能发生了变化,这时候就要警惕了。

好了,历史模拟法就讲到这里。它虽然简单,但却是很多金融机构的标配。下一章我们会讲参数法VaR,到时候你会看到,正态分布假设到底有多大的局限性。

专注资料整理