问题描述
解决方案
这是一个非常经典的量化对冲策略需求。在 JoinQuant(聚宽)平台上,实现这一策略主要分为两步:
- 计算组合 Beta:通过获取持仓股票的历史收益率与基准指数(如沪深300)的历史收益率,计算协方差和方差来得出 Beta 值。
- 期货对冲:根据组合总市值和 Beta 值,计算需要做空的期货合约数量,从而对冲市场风险(Alpha 策略)。
核心逻辑
-
Beta ($\beta$) 计算公式:
$$ \beta_p = \sum (w_i \times \beta_i) $$
其中 $w_i$ 是股票 $i$ 在组合中的权重,$\beta_i$ 是股票 $i$ 相对于基准的 Beta。
单个股票 Beta 计算:$\beta_i = \frac{Cov(r_i, r_m)}{Var(r_m)}$
($r_i$ 为股票收益率,$r_m$ 为基准收益率) -
对冲所需期货合约数:
$$ N = \frac{TotalValue_{stock} \times \beta_p}{Price_{future} \times Multiplier} $$
其中 $Multiplier$ 是合约乘数(沪深300 IF 为 300)。
策略代码实现
以下是一个完整的策略代码。该策略会构建一个简单的股票组合(例如买入低估值股票),并每日计算 Beta,使用沪深300股指期货(IF)进行动态对冲。
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
from jqdata import *
def initialize(context):
# 1. 设定基准为沪深300
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 2. 设定手续费
# 股票:买入万3,卖出万3加千1印花税
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 期货:无印花税,买卖万0.23 (示例值,具体视交易所标准)
set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.000023, close_commission=0.000023, close_today_commission=0.0023, min_commission=0), type='index_futures')
# 3. 设定全局变量
g.stock_pool = [] # 股票池
g.future_code = None # 当前主力合约
g.days = 60 # 计算Beta使用的历史天数
# 4. 运行计划
# 每周一开盘调整股票仓位
run_weekly(adjust_stock_position, weekday=1, time='09:30')
# 每天开盘后调整对冲仓位 (股票调仓后立即对冲)
run_daily(adjust_hedge_position, time='09:40')
# 每天收盘后打印当日Beta
run_daily(print_beta, time='15:00')
def adjust_stock_position(context):
"""
选股与调仓逻辑(示例:简单的低PE选股)
"""
# 获取沪深300成分股
stocks = get_index_stocks('000300.XSHG')
# 查询PE数据
q = query(valuation.code, valuation.pe_ratio).filter(
valuation.code.in_(stocks),
valuation.pe_ratio > 0
).order_by(valuation.pe_ratio.asc()).limit(10) # 选PE最小的10只
df = get_fundamentals(q)
g.stock_pool = list(df['code'])
# 卖出不在股票池的股票
for stock in context.portfolio.positions:
if context.portfolio.positions[stock].total_amount > 0:
if stock not in g.stock_pool:
order_target_value(stock, 0)
# 买入股票池中的股票(等权分配)
if len(g.stock_pool) > 0:
# 预留一部分资金给期货保证金,假设使用90%资金买股票
available_cash = context.portfolio.total_value * 0.9
target_value = available_cash / len(g.stock_pool)
for stock in g.stock_pool:
order_target_value(stock, target_value)
def calculate_portfolio_beta(context):
"""
计算持仓组合的Beta值
"""
# 获取基准(沪深300)的历史数据
benchmark_data = attribute_history('000300.XSHG', g.days + 1, '1d', ['close'])
# 计算基准收益率
benchmark_ret = benchmark_data['close'].pct_change().dropna()
# 如果没有持仓,Beta为0
if len(context.portfolio.positions) == 0:
return 0.0
total_value = context.portfolio.positions_value
if total_value == 0:
return 0.0
portfolio_beta = 0.0
# 遍历持仓计算加权Beta
for stock, position in context.portfolio.positions.items():
if position.total_amount == 0:
continue
# 获取个股历史数据
stock_data = attribute_history(stock, g.days + 1, '1d', ['close'])
stock_ret = stock_data['close'].pct_change().dropna()
# 确保数据长度一致
min_len = min(len(benchmark_ret), len(stock_ret))
if min_len < 10: # 数据太少不计算
continue
rb = benchmark_ret.iloc[-min_len:]
rs = stock_ret.iloc[-min_len:]
# 计算协方差矩阵 [[Var(s), Cov(s,b)], [Cov(b,s), Var(b)]]
cov_mat = np.cov(rs, rb)
beta_i = cov_mat[0, 1] / cov_mat[1, 1]
# 计算权重
weight_i = position.value / total_value
# 累加组合Beta
portfolio_beta += beta_i * weight_i
return portfolio_beta
def adjust_hedge_position(context):
"""
调整期货对冲仓位
"""
# 1. 获取当前主力合约
# IF: 沪深300, IC: 中证500, IH: 上证50
# 这里我们用沪深300期货对冲
new_future = get_dominant_future('IF')
# 如果主力合约切换,平掉旧合约
if g.future_code and g.future_code != new_future:
# 平旧合约空单
if context.portfolio.short_positions[g.future_code].total_amount > 0:
order_target(g.future_code, 0, side='short')
g.future_code = new_future
# 2. 计算组合Beta
beta = calculate_portfolio_beta(context)
# 3. 计算股票总市值
stock_value = context.portfolio.positions_value
# 4. 计算需要对冲的价值 (Hedge Value = Stock Value * Beta)
hedge_value = stock_value * beta
# 5. 获取期货当前价格和合约乘数
future_price = get_current_data()[g.future_code].last_price
multiplier = 300.0 # IF合约乘数为300
# 6. 计算所需合约张数 (四舍五入取整)
if future_price > 0:
required_contracts = int(round(hedge_value / (future_price * multiplier)))
else:
required_contracts = 0
# 7. 执行交易(调整空单仓位)
# 注意:order_target 的 side='short' 表示调整空单的目标数量
# 如果计算出的Beta为负或0,则不需要对冲(或者应该做多,这里简化为不持仓)
if required_contracts < 0:
required_contracts = 0
log.info("当前股票市值: %.2f, 组合Beta: %.4f, 需对冲价值: %.2f, 目标空单手数: %d" % (stock_value, beta, hedge_value, required_contracts))
order_target(g.future_code, required_contracts, side='short')
def print_beta(context):
beta = calculate_portfolio_beta(context)
record(Beta=beta)
代码关键点解析
-
Beta 计算 (
calculate_portfolio_beta):- 使用
attribute_history获取过去 60 天(g.days)的收盘价。 - 使用
pct_change()计算日收益率。 - 使用
np.cov(stock_ret, benchmark_ret)计算协方差矩阵。Beta = 协方差 / 基准方差。 - 加权求和:组合 Beta 等于个股 Beta 乘以其在组合中的市值权重之和。
- 使用
-
主力合约切换 (
get_dominant_future):- 期货合约会到期,必须使用
get_dominant_future('IF')动态获取当前的主力合约。 - 代码中包含了一个逻辑:如果主力合约发生变化,先平掉旧合约的仓位,再在新合约上开仓。
- 期货合约会到期,必须使用
-
对冲数量计算:
- 公式:
合约数 = (股票总市值 * Beta) / (期货价格 * 合约乘数)。 - 沪深300股指期货(IF)的合约乘数是 300。如果是中证500(IC)则为 200。
- 公式:
-
下单函数 (
order_target):- 期货做空对冲,需要指定
side='short'。 order_target(security, amount, side='short')的意思是将空单的持仓调整到amount手。如果当前空单少于目标,系统会自动开空;如果多于目标,系统会自动平空。
- 期货做空对冲,需要指定
常见问题与优化建议
- Beta 的不稳定性:历史 Beta 不一定代表未来 Beta。可以使用更复杂的风险模型(如 Barra 模型)或对 Beta 进行平滑处理(如贝叶斯收缩)。
- 基准错配:如果你的股票池主要是中小盘股(如中证500成分股),用沪深300期货(IF)对冲效果会很差,应该使用中证500期货(IC)。
- 资金占用:期货需要保证金(通常 10%-15%)。代码中
available_cash = context.portfolio.total_value * 0.9预留了 10% 的资金防止爆仓,实际操作中可能需要更多现金缓冲。 - 未来函数:在计算 Beta 时,使用的是过去的数据,这是合法的。但在选股时要注意不要使用未来数据。
Q&A
Q: 为什么计算 Beta 时要用收益率而不是价格直接计算相关性?
A: 金融时间序列通常是非平稳的(价格有趋势),直接用价格计算协方差会导致伪回归。使用收益率可以将序列转化为平稳序列,统计结果才具有实际意义。
Q: 如果我的股票组合 Beta 是负的怎么办?
A: 如果 Beta 为负,意味着你的组合与大盘走势相反。理论上你应该做多期货来对冲风险。但在 A 股市场,长期来看 Beta 极少为负,通常是在 0.8 到 1.2 之间。代码中加入了 if required_contracts < 0: required_contracts = 0 的保护逻辑。
Q: 为什么使用 order_target 而不是 order?
A: order_target 是目标仓位管理,它会自动计算当前持仓与目标持仓的差额进行买卖。在对冲策略中,我们每天计算的是“应该持有多少手空单”,而不是“今天新增多少手”,所以用 order_target 最方便且不易出错。