4、历史模拟法:非参数方法的原理、排序与分位数计算、滚动窗口与衰减因子、优缺点分析、与参数法的对比

历史模拟法,说白了就是「让历史替我们回答」。

我刚开始做风控那会儿,领导让我算一个股票组合的VaR。当时手头数据不多,参数法算出来总觉得心里没底。后来一个老同事跟我说:「你试试历史模拟法,简单粗暴,但有时候比那些花里胡哨的模型管用。」

嗯,这一试,就是好几年。

4.1 非参数方法的原理

历史模拟法的核心思想很简单:过去发生过的损失,未来也可能发生

它不假设收益率服从任何分布(正态、t分布什么的统统不要),直接拿历史数据说话。你想想看,市场暴跌的时候,收益率分布往往有厚尾,参数法很容易低估风险。但历史模拟法不会——只要历史上出现过,它就能捕捉到。

核心逻辑:

  • 收集过去N天的收益率数据
  • 对收益率进行排序(从最差到最好)
  • 取第5%分位数(95%置信水平)或第1%分位数(99%置信水平)
  • 这个分位数就是VaR值

举个例子。假设我们有过去500天的日收益率数据,要算95%置信水平的VaR。那就把500个收益率从小到大排好,第25个(500×5%)就是VaR值。简单吧?

我的经验:历史模拟法对数据质量要求很高。我曾经遇到过数据里混入了「异常值」——比如某天因为数据录入错误,收益率显示-99%。如果不做清洗,VaR直接崩掉。所以,数据预处理这一步千万别省。

4.2 排序与分位数计算

排序这一步,看起来简单,但细节里藏着坑。

假设我们有1000个历史收益率,要算99% VaR。理论上应该取第10个(1000×1%)最差的收益率。但问题是:如果数据量不是整数倍怎么办?

常见的做法有两种:

  • 位置法:直接取第⌈n×(1-α)⌉个值(向上取整)
  • 插值法:在两个相邻值之间做线性插值

我个人习惯用插值法。为什么?因为位置法有时候会「跳变」——数据稍微多一点或少一点,VaR值可能突然变化。插值法平滑一些,结果更稳定。

# Python示例:历史模拟法计算VaR
import numpy as np

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

# 示例数据
np.random.seed(42)
returns = np.random.normal(0, 0.02, 1000)  # 模拟1000天收益率
var_95 = historical_var(returns, 0.95)
print(f"95% VaR: {var_95:.4f}")

注意:排序时一定要确认方向。收益率从最差到最好排,VaR取的是最差的那一端。我曾经见过有人排反了,算出来的VaR是负的——那其实是收益,不是风险。

4.3 滚动窗口与衰减因子

历史模拟法有个天然的问题:该用多长的历史窗口?

窗口太短(比如100天),样本量不够,分位数估计不稳定。窗口太长(比如1000天),早期的数据可能已经不能反映当前市场状况了。

常见的做法是滚动窗口:固定窗口长度(比如250个交易日,约一年),每天滚动更新。

但这里有个进阶技巧——衰减因子

衰减因子的思路是:越近的数据,权重越大;越远的数据,权重越小。这样既保留了足够的历史数据,又让模型对近期变化更敏感。

# 带衰减因子的历史模拟法
def weighted_historical_var(returns, confidence_level=0.95, lambda_factor=0.97):
    """
    带指数衰减权重的历史模拟法
    lambda_factor: 衰减因子,通常取0.94-0.99
    """
    n = len(returns)
    weights = np.array([(1 - lambda_factor) * (lambda_factor ** (n - 1 - i)) 
                        for i in range(n)])
    weights = weights / weights.sum()  # 归一化
    
    # 按收益率排序,同时调整权重顺序
    sorted_indices = np.argsort(returns)
    sorted_weights = weights[sorted_indices]
    sorted_returns = returns[sorted_indices]
    
    # 计算累积权重,找到分位点
    cum_weights = np.cumsum(sorted_weights)
    target_weight = 1 - confidence_level
    idx = np.searchsorted(cum_weights, target_weight)
    
    var = -sorted_returns[idx]
    return var

避坑指南:衰减因子λ的取值很关键。λ=0.94时,半衰期约11天;λ=0.97时,半衰期约23天。我一般建议:如果是做日频风控,λ取0.94-0.97之间;如果是做周频或月频,λ可以取大一点。具体选多少?回测一下,选历史表现最好的那个。

4.4 优缺点分析

历史模拟法不是万能的。我用它这么多年,总结一下它的优缺点。

优点 缺点
无需假设分布,能捕捉厚尾 对历史数据依赖性强,数据量要求大
实现简单,容易理解 对市场结构变化不敏感(比如突然换了交易规则)
可以处理非线性风险(如期权) 计算效率低(每次都要排序)
结果直观,容易向管理层解释 极端事件外推能力差(没发生过的事,算不出来)

我记得有一次,某只股票因为财报造假,一天跌了80%。历史模拟法完全没捕捉到这个风险——因为过去500天里,最大跌幅才15%。这就是历史模拟法的局限性:它只能告诉你「历史上发生过的最坏情况」,但无法告诉你「未来可能发生但还没发生过的情况」。

4.5 与参数法的对比

参数法(比如方差-协方差法)和历史模拟法,是VaR计算的两大流派。我经常被问到:「到底该用哪个?」

我的回答是:看场景

对比维度 参数法 历史模拟法
分布假设 需要(通常假设正态分布) 不需要
计算速度 快(O(n)) 慢(O(n log n))
厚尾处理 差(正态分布低估厚尾) 好(只要历史数据中有)
数据需求 少(几十个样本即可) 多(至少250个样本)
极端事件预测 差(正态分布尾部太薄) 有限(只能预测已发生过的)
实现复杂度 中等(需要估计协方差矩阵) 低(排序即可)

我个人习惯是:日常监控用历史模拟法,压力测试用参数法。为什么?因为历史模拟法更贴近实际,而参数法可以模拟「极端但尚未发生」的场景——比如假设波动率突然翻倍。

一句话总结:历史模拟法告诉你「过去最差的情况是什么」,参数法告诉你「如果市场疯了,最差可能是什么」。两者结合,才是完整的风险管理。

4.6 知识体系图

下面这张图,是我自己梳理的历史模拟法知识体系。每次做培训,我都会先画一遍这张图——它帮你把零散的知识点串起来。

历史模拟法知识体系 历史模拟法 非参数原理 排序与分位数 滚动窗口与衰减 与参数法对比 不假设分布,直接使用历史数据 能捕捉厚尾和尖峰特征 排序 → 取分位数 → VaR 位置法 vs 插值法 固定窗口 vs 衰减因子 λ=0.94~0.97 常用范围 参数法:快但假设强 历史法:慢但更真实 优点:简单、直观、无分布假设 缺点:数据量大、外推能力弱 日常监控用历史法,压力测试用参数法

这张图把历史模拟法的核心脉络都串起来了。从上到下,从左到右,你可以看到:原理是什么、怎么算、窗口怎么选、优缺点在哪、和参数法怎么对比。

嗯,内容就这些。历史模拟法虽然简单,但用好它并不容易。数据清洗、窗口选择、衰减因子调参——每一步都有坑。我踩过的坑,上面都提到了,希望能帮你少走弯路。


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