问题描述
解决方案
在 JoinQuant 聚宽平台上计算投资组合的 VaR (Value at Risk,在险价值),最常用且适合回测环境的方法是历史模拟法 (Historical Simulation)。
这种方法的核心逻辑是:假设当前持仓的权重在过去一段时间内保持不变,计算该组合在历史上的每日收益率分布,然后根据置信度(如 95% 或 99%)找到对应的分位数(Quantile),从而估算未来一日的最大潜在损失。
核心步骤
- 获取持仓信息:从
context.portfolio.positions获取当前持有的标的和对应的市值权重。 - 获取历史行情:使用
history或attribute_history获取持仓标的过去 N 天(如 250 天)的收盘价。 - 计算历史收益率:计算每只股票的日收益率。
- 构建组合历史收益率:利用当前权重和个股历史收益率,合成“假设当前组合在过去表现”的收益率序列。
- 计算 VaR:使用
numpy.percentile计算指定置信度下的分位数,并乘以当前总资产。
策略代码示例
以下是一个完整的策略代码,包含了一个简单的买入逻辑(用于建立仓位)和一个专门计算 VaR 的函数 calculate_portfolio_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)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 定义全局变量:VaR计算的历史窗口长度(例如过去250个交易日)
g.lookback_days = 250
# 定义全局变量:置信度(例如95%)
g.confidence_level = 0.95
# 每天收盘后计算当天的VaR
run_daily(calculate_portfolio_var, 'after_trading_end')
# 每天开盘时进行简单的交易(为了演示持仓)
run_daily(trade_func, 'open')
def trade_func(context):
# 简单的示例交易逻辑:买入几只股票构建组合
target_stocks = ['000001.XSHE', '600000.XSHG', '000002.XSHE']
for stock in target_stocks:
# 均仓买入
order_target_value(stock, context.portfolio.total_value / len(target_stocks))
def calculate_portfolio_var(context):
"""
计算投资组合的 VaR (历史模拟法)
"""
# 1. 获取当前持仓的标的列表
positions = context.portfolio.positions
security_list = list(positions.keys())
# 如果没有持仓,VaR 为 0
if len(security_list) == 0:
log.info("当前无持仓,VaR 为 0")
return
# 2. 获取持仓标的的历史收盘价数据
# 获取过去 g.lookback_days + 1 天的数据,以计算 g.lookback_days 个收益率
hist_data = history(g.lookback_days + 1, '1d', 'close', security_list, df=True)
# 3. 计算个股的日收益率
# pct_change() 计算 (今天-昨天)/昨天,dropna() 去除第一行空值
stock_returns = hist_data.pct_change().dropna()
# 4. 计算当前各标的的权重
# 注意:这里假设计算的是“当前持仓”在“历史市场环境”下的风险
total_value = context.portfolio.total_value
weights = []
valid_securities = []
for stock in security_list:
# 确保该股票在历史数据中有收益率记录(防止停牌或新股导致数据缺失)
if stock in stock_returns.columns:
# 权重 = 个股持仓市值 / 总资产
weight = positions[stock].value / total_value
weights.append(weight)
valid_securities.append(stock)
# 转换为 numpy 数组方便计算
weights = np.array(weights)
# 仅保留有效股票的收益率数据
stock_returns = stock_returns[valid_securities]
# 5. 计算组合的历史模拟收益率序列
# 矩阵乘法:(T x N) dot (N x 1) = (T x 1)
# 得到的是假设每天都持有当前权重的组合,在过去每一天的收益率
portfolio_hist_returns = stock_returns.dot(weights)
# 6. 计算 VaR (百分比形式)
# 如果置信度是 95%,我们需要找左尾 5% 的分位数
alpha = 1 - g.confidence_level
var_percent = np.percentile(portfolio_hist_returns, alpha * 100)
# 7. 计算 VaR (金额形式)
# VaR 通常表示为正数,代表潜在损失金额
var_value = abs(var_percent * total_value)
# 打印和记录结果
log.info("=" * 30)
log.info(f"日期: {context.previous_date}")
log.info(f"置信度: {g.confidence_level*100}%")
log.info(f"历史窗口: {g.lookback_days} 天")
log.info(f"组合 VaR (百分比): {var_percent:.4%}")
log.info(f"组合 VaR (金额): {var_value:.2f} 元")
log.info("=" * 30)
# 可以在回测图表中画出 VaR 曲线
record(VaR=var_value)
代码逻辑详解
-
数据获取 (
history):
我们使用history函数获取持仓股票过去 N 天的收盘价。这里需要获取g.lookback_days + 1天的数据,因为计算收益率会损失一天的数据。 -
权重计算:
VaR 的核心在于评估当前持仓面临的风险。因此,我们使用context.portfolio.positions[stock].value / context.portfolio.total_value来计算当前每只股票占总资产的比例。 -
矩阵运算 (
dot):
stock_returns是一个 DataFrame,每一列是一只股票的历史收益率序列。weights是一个向量。通过点积运算 (dot),我们可以快速算出组合在历史上每一天的假设收益率。- 公式:$R_{portfolio, t} = \sum_{i=1}^{N} (w_i \times R_{i, t})$
-
分位数计算 (
np.percentile):
历史模拟法的关键在于找到收益率分布的左尾。- 对于 95% 的置信度,意味着有 5% 的概率损失会超过 VaR。
- 因此我们计算历史收益率序列的第 5 百分位数(5th percentile)。
-
结果解读:
如果计算出的var_percent是 -0.03 (即 -3%),且当前总资产为 1,000,000 元,则var_value为 30,000 元。这意味着在正常的市场波动下,我们有 95% 的把握认为明天的损失不会超过 30,000 元。
Q&A
Q: 为什么使用历史模拟法而不是方差-协方差法?
A: 在量化回测平台中,历史模拟法实现最简单且不需要假设收益率服从正态分布(金融数据通常具有尖峰肥尾特性,不符合正态分布)。方差-协方差法需要计算协方差矩阵,对于持仓较多的组合计算量大且对分布假设敏感。
Q: 如何提高 VaR 计算的准确性?
A:
- 增加历史窗口:将
g.lookback_days从 250 增加到 500 或更多,可以包含更多的极端行情样本。 - 使用指数加权:给近期的历史数据更高的权重(EWMA),因为近期的波动率更能反映当前市场状态。
- 蒙特卡洛模拟:如果包含期权等非线性资产,历史模拟法可能失效,需要使用蒙特卡洛模拟,但在 JoinQuant 普通策略中计算成本较高。
Q: record(VaR=var_value) 有什么用?
A: record 函数会将计算出的 VaR 数值绘制在回测结果的收益率曲线下方。这样你可以直观地看到随着持仓变化和市场波动,你的风险敞口是如何变化的。