问题描述
解决方案
历史模拟法计算投资组合 VaR
历史模拟法(Historical Simulation)是一种计算风险价值(Value at Risk, VaR)的非参数方法。它的核心假设是**“历史会重演”**。
该方法不假设资产收益率服从正态分布,而是利用投资组合中各项资产的当前权重,配合这些资产在过去一段时间的历史收益率,计算出投资组合在历史每一天的假设收益率,从而构建出一个经验分布,最后根据置信度选取对应的分位数作为 VaR。
以下是在 JoinQuant 聚宽平台上实现该算法的详细步骤和完整代码。
算法逻辑步骤
- 获取当前持仓:确定当前投资组合中持有的标的及其市值权重。
- 获取历史数据:获取这些标的在过去 N 天(例如 250 天)的历史收盘价。
- 计算历史收益率:计算每个标的在历史区间内的日收益率。
- 构建组合模拟收益:假设当前持仓权重在历史上保持不变,计算组合在历史每一天的模拟收益率。
- 公式:$R_{p,t} = \sum (w_i \times R_{i,t})$
- 其中 $w_i$ 是资产 $i$ 的当前权重,$R_{i,t}$ 是资产 $i$ 在历史第 $t$ 天的收益率。
- 计算 VaR:将组合模拟收益率从小到大排序,根据置信度(如 95% 或 99%)找到对应的分位数(Quantile),该分位数即为 VaR(收益率形式),再乘以当前总资产即可得到金额形式的 VaR。
JoinQuant 策略代码实现
以下是一个完整的策略示例。该策略会在每天收盘后计算当前持仓在 95% 置信度下的单日 VaR。
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 设定日志级别
log.set_level('order', 'error')
# 设定VaR计算参数
g.lookback_days = 250 # 回溯历史天数(约一年)
g.confidence_level = 0.95 # 置信度 95%
# 运行函数(为了演示,我们在开盘买入一些股票,收盘计算VaR)
run_daily(market_open, time='09:30')
run_daily(calculate_portfolio_var, time='15:30')
def market_open(context):
# 示例:构建一个简单的多股票组合
# 如果没有持仓,则买入
if len(context.portfolio.positions) == 0:
# 买入平安银行、万科A、贵州茅台作为测试组合
stocks = ['000001.XSHE', '000002.XSHE', '600519.XSHG']
# 资金三等分买入
cash_per_stock = context.portfolio.available_cash / len(stocks)
for stock in stocks:
order_value(stock, cash_per_stock)
def calculate_portfolio_var(context):
"""
使用历史模拟法计算投资组合的 VaR
"""
# 1. 获取当前持仓标的及市值
positions = context.portfolio.positions
if not positions:
log.info("当前无持仓,VaR 为 0")
return
security_list = list(positions.keys())
total_value = context.portfolio.total_value
# 计算当前各资产的权重
# weights 字典: {stock_code: weight}
weights = {}
for stock in security_list:
# 权重 = 个股持仓价值 / 组合总价值
weights[stock] = positions[stock].value / total_value
# 2. 获取历史价格数据
# 多取1天数据以计算收益率,因为pct_change会损失第一天数据
hist_data = history(g.lookback_days + 1, '1d', 'close', security_list)
# 3. 计算单资产历史日收益率
# df_returns 的列是股票代码,行是日期
df_returns = hist_data.pct_change().dropna()
if df_returns.empty:
log.warn("历史数据不足,无法计算 VaR")
return
# 4. 计算组合的历史模拟收益率序列
# 假设当前权重在历史上一直保持不变
# 公式:组合日收益 = sum(个股日收益 * 个股当前权重)
# 创建一个全为0的序列来存储组合收益
portfolio_hist_returns = pd.Series(0.0, index=df_returns.index)
for stock in security_list:
if stock in df_returns.columns:
w = weights[stock]
r = df_returns[stock]
# 累加加权收益
portfolio_hist_returns += r * w
# 5. 计算 VaR (分位数法)
# 如果置信度是 95%,我们需要找左尾 5% 的分位数
alpha = 1 - g.confidence_level
# 使用 numpy 的 percentile 函数计算分位数
# portfolio_hist_returns 是一系列收益率,如 [-0.02, 0.01, 0.03 ...]
# 我们寻找最差的那部分收益率
var_return = np.percentile(portfolio_hist_returns, alpha * 100)
# 计算金额 VaR
# VaR 通常表示为正数,代表最大潜在损失金额
var_value = abs(var_return) * total_value
# 打印结果
log.info("=" * 30)
log.info("【VaR计算报告 - 历史模拟法】")
log.info("回溯天数: %d 天" % g.lookback_days)
log.info("置信度: %.1f%%" % (g.confidence_level * 100))
log.info("当前组合总市值: %.2f" % total_value)
log.info("VaR (收益率形式): %.4f%%" % (var_return * 100))
log.info("VaR (金额形式): %.2f 元" % var_value)
log.info("这意味着在正常的市场条件下,")
log.info("明天该组合损失超过 %.2f 元的概率仅为 %.1f%%" % (var_value, alpha * 100))
log.info("=" * 30)
# 也可以使用 record 在回测图中画出 VaR 曲线
record(VaR_Value=var_value)
代码关键点解析
-
history函数的使用:- 我们使用
history(g.lookback_days + 1, '1d', 'close', security_list)获取数据。 - 注意:必须多取 1 天数据,因为计算收益率(
pct_change)时,第一天的数据会变成NaN并被丢弃。
- 我们使用
-
权重的处理:
- 历史模拟法的核心在于**“当前权重,历史收益”**。我们不是去回测历史上每一天真实的持仓权重,而是假设“如果我们过去一年一直持有现在的这个组合,每天会赚赔多少钱”。
- 代码中
weights[stock] = positions[stock].value / total_value计算了当前的静态权重。
-
组合收益率计算:
- 利用 Pandas 的向量化运算特性,
portfolio_hist_returns += r * w可以高效地计算出组合在历史每一天的假设收益率。
- 利用 Pandas 的向量化运算特性,
-
分位数计算 (
np.percentile):alpha = 1 - 0.95 = 0.05。np.percentile(data, 5)会找到数据中从小到大排序后位于 5% 位置的数值。因为是损失,这个值通常是负数(例如 -0.03 表示亏损 3%)。- 最终报告 VaR 时,通常取绝对值表示“最大潜在损失金额”。
Q&A
Q1: 为什么使用历史模拟法而不是方差-协方差法?
A1: 历史模拟法不需要假设收益率服从正态分布,能够更好地捕捉市场中的“肥尾”现象(即极端行情发生的概率比正态分布预测的要高)。此外,它不需要计算复杂的协方差矩阵,对于包含非线性资产(如期权)或非正态分布资产的组合更有效。
Q2: 回溯天数(Lookback Window)设置多少合适?
A2: 通常设置为 250 天(约一年)或 500 天(约两年)。天数太短(如 30 天)会导致样本量不足,分位数估计不准确;天数太长(如 10 年)则可能包含过时的市场结构信息,无法反映当前的市场波动特征。
Q3: 如何处理停牌股票?
A3: 在上述代码中,history 函数获取停牌股票的价格通常是前一交易日的收盘价,因此计算出的收益率为 0。这在一定程度上低估了风险(因为复牌后可能补跌)。更严谨的做法是使用行业指数收益率替代,或者在数据预处理阶段剔除停牌过久的标的。
Q4: 这个代码可以在回测和模拟交易中使用吗?
A4: 是的。该代码使用了 run_daily 和标准的 JoinQuant API,既可以在回测中验证风险指标的有效性,也可以在模拟交易中用于每日风险监控。