🚀 AI 一键生成 joinquant 策略代码

JoinQuant 进阶教程:如何计算投资组合 Beta 并进行期货对冲?

joinquant | 发布时间: 2025/12/22
以下内容由 EasyQuant 生成。

问题描述

如何计算组合 Beta 并用股指期货做对冲?

解决方案

这是一个非常经典的量化对冲策略需求。在 JoinQuant(聚宽)平台上,实现这一策略主要分为两步:

  1. 计算组合 Beta:通过获取持仓股票的历史收益率与基准指数(如沪深300)的历史收益率,计算协方差和方差来得出 Beta 值。
  2. 期货对冲:根据组合总市值和 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)

代码关键点解析

  1. Beta 计算 (calculate_portfolio_beta):

    • 使用 attribute_history 获取过去 60 天(g.days)的收盘价。
    • 使用 pct_change() 计算日收益率。
    • 使用 np.cov(stock_ret, benchmark_ret) 计算协方差矩阵。Beta = 协方差 / 基准方差。
    • 加权求和:组合 Beta 等于个股 Beta 乘以其在组合中的市值权重之和。
  2. 主力合约切换 (get_dominant_future):

    • 期货合约会到期,必须使用 get_dominant_future('IF') 动态获取当前的主力合约。
    • 代码中包含了一个逻辑:如果主力合约发生变化,先平掉旧合约的仓位,再在新合约上开仓。
  3. 对冲数量计算:

    • 公式:合约数 = (股票总市值 * Beta) / (期货价格 * 合约乘数)
    • 沪深300股指期货(IF)的合约乘数是 300。如果是中证500(IC)则为 200。
  4. 下单函数 (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 最方便且不易出错。