🚀 AI 一键生成 joinquant 策略代码
立即体验

聚宽多因子选股:如何用滚动 IC 动态调整因子权重?(附 Python 源码)

joinquant | 发布时间: 2026/8/7
以下内容由 EasyQuant 生成。

问题描述

聚宽多因子选股:如何用滚动 IC 动态调整因子权重?
如何在聚宽中结合 get_fundamentals、动量和波动率因子,并根据过去 12 个月的滚动 IC 表现动态调整因子权重?

解决方案

在多因子选股策略中,因子的表现往往会随着市场环境的变化而发生轮动。传统的固定权重法无法适应这种变化。通过计算各因子在过去一段时间(如12个月)的滚动IC(Information Coefficient,信息系数),并以IC值(或其IR值)作为权重分配的依据,可以实现因子权重的动态调整,从而提升策略的超额收益和稳定性。

以下是在聚宽(JoinQuant)平台上实现该策略的完整思路与核心代码。

一、 策略核心步骤

  1. 确定候选因子池
    • 基本面因子:使用 get_fundamentals 获取,例如 valuation.pe_ratio(市盈率)或 indicator.roe_ttm(净资产收益率)。
    • 动量因子:使用聚宽因子库中的 momentum(传统动量)或 Price3M(3个月股价变动)。
    • 波动率因子:使用聚宽因子库中的 Variance20(20日年化收益方差)或 residual_volatility(残差波动率)。
  2. 计算滚动 IC
    • 每月调仓期,获取过去12个月(约250个交易日)的历史因子值与未来1期的个股收益率。
    • 计算每个交易日因子值与下期收益率的 Rank IC(秩相关系数)。
    • 对过去12个月的 Rank IC 求均值,作为该因子的滚动 IC 表现。
  3. 动态调整权重
    • 采用 IC等比例加权IC/Std(IC)(即IR,信息比率) 作为因子的权重。
    • 动态权重 = 单个因子滚动IC / 所有因子滚动IC绝对值之和。
  4. 综合得分与选股调仓
    • 对个股的各因子值进行去极值(winsorize)和标准化(standardlize)。
    • 结合动态权重计算个股的综合得分,予以排序,买入得分最高的Top N只股票。

二、 聚宽 Python 策略源码

以下是一个简化但结构完整的聚宽策略代码示例,展示了如何获取数据、计算滚动IC并动态调仓:

import numpy as np
import pandas as pd
from jqdata import *
from jqfactor import get_factor_values, winsorize, standardlize

def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 策略参数设置
    g.factor_list = ['roe_ttm', 'momentum', 'Variance20'] # 候选因子:ROE、动量、20日方差
    g.holding_num = 20 # 持股数量
    g.rolling_months = 12 # 滚动IC计算窗口(月)
    
    # 每月第一个交易日开盘运行
    run_monthly(rebalance, monthday=1, time='09:30')

def rebalance(context):
    # 1. 获取股票池(以中证500成分股为例)
    stock_pool = get_index_stocks('000905.SH', date=context.previous_date)
    
    # 2. 计算滚动IC并获取动态权重
    weights = get_dynamic_weights(stock_pool, context.previous_date)
    log.info(f"当前调仓日因子动态权重: {weights}")
    
    # 3. 获取当前交易日的因子值
    current_date = context.previous_date
    factor_data = {}
    
    # 获取基本面因子 (ROE)
    q = query(valuation.code, indicator.roe_ttm).filter(valuation.code.in_(stock_pool))
    df_fundamental = get_fundamentals(q, date=current_date).set_index('code')
    factor_data['roe_ttm'] = df_fundamental['roe_ttm']
    
    # 获取量价因子 (动量与波动率)
    factor_val = get_factor_values(securities=stock_pool, factors=['momentum', 'Variance20'], end_date=current_date, count=1)
    factor_data['momentum'] = factor_val['momentum'].iloc[0]
    factor_data['Variance20'] = factor_val['Variance20'].iloc[0]
    
    # 4. 因子数据处理与综合打分
    score_series = pd.Series(0.0, index=stock_pool)
    for factor in g.factor_list:
        val = factor_data[factor].reindex(stock_pool).fillna(0.0)
        # 去极值与标准化
        clean_val = standardlize(winsorize(val, scale=3))
        
        # 负向因子处理(如方差/波动率通常为负向因子,IC计算后权重自然会调整正负,此处直接加权)
        score_series += clean_val * weights[factor]
        
    # 5. 排序并执行调仓
    buy_list = score_series.sort_values(ascending=False).head(g.holding_num).index.tolist()
    
    # 卖出不在买入列表中的持仓
    for stock in list(context.portfolio.positions.keys()):
        if stock not in buy_list:
            order_target(stock, 0)
            
    # 等权重买入目标持仓
    position_value = context.portfolio.total_value / g.holding_num
    for stock in buy_list:
        order_target_value(stock, position_value)

def get_dynamic_weights(stock_pool, end_date):
    """
    计算过去12个月的滚动IC,并返回归一化后的因子权重
    """
    # 获取过去12个月的历史月度交易日列表
    trade_days = get_trade_days(end_date=end_date, count=g.rolling_months * 21)
    monthly_dates = [trade_days[i] for i in range(0, len(trade_days), 21)][-g.rolling_months:]
    
    ic_history = {factor: [] for factor in g.factor_list}
    
    # 循环计算历史每个月的IC值
    for i in range(len(monthly_dates) - 1):
        t_date = monthly_dates[i]
        t_next_date = monthly_dates[i+1]
        
        # 获取t期的因子值
        # ROE
        q = query(valuation.code, indicator.roe_ttm).filter(valuation.code.in_(stock_pool))
        df_fundamental = get_fundamentals(q, date=t_date).set_index('code')
        
        # 动量与波动率
        factor_val = get_factor_values(securities=stock_pool, factors=['momentum', 'Variance20'], end_date=t_date, count=1)
        
        # 获取t期到t+1期的个股收益率
        prices = get_price(stock_pool, start_date=t_date, end_date=t_next_date, fields=['close'], fq='pre')['close']
        returns = (prices.iloc[-1] - prices.iloc[0]) / prices.iloc[0]
        
        # 计算各因子的Rank IC
        for factor in g.factor_list:
            if factor == 'roe_ttm':
                f_series = df_fundamental['roe_ttm'].reindex(stock_pool)
            else:
                f_series = factor_val[factor].iloc[0].reindex(stock_pool)
                
            f_series = f_series.fillna(f_series.median())
            # 计算秩相关系数
            rank_ic = f_series.corr(returns, method='spearman')
            if not np.isnan(rank_ic):
                ic_history[factor].append(rank_ic)
                
    # 计算滚动IC均值
    mean_ic = {}
    total_abs_ic = 0.0
    for factor in g.factor_list:
        mean_ic[factor] = np.mean(ic_history[factor]) if len(ic_history[factor]) > 0 else 0.01
        total_abs_ic += abs(mean_ic[factor])
        
    # 归一化权重
    weights = {}
    for factor in g.factor_list:
        weights[factor] = mean_ic[factor] / total_abs_ic if total_abs_ic > 0 else 1.0 / len(g.factor_list)
        
    return weights

三、 策略优化建议

  1. 引入IC_IR(信息比率)加权:不仅考虑IC的均值,还考虑IC的稳定性。权重公式可调整为:Weight = Mean(IC) / Std(IC),这样可以惩罚那些波动巨大、方向不稳定的因子。
  2. 行业与市值中性化:在计算IC之前,建议使用聚宽的 neutralize 函数对因子进行行业和市值中性化,避免选股策略过度暴露在某一特定行业或小市值风格上。
  3. 半衰期加权:在计算滚动IC均值时,可以对越接近当前调仓日的历史IC赋予更高的权重(如指数衰减加权),以更快地捕捉近期市场风格的切换。