🚀 AI 一键生成 joinquant 策略代码

JoinQuant 组合优化器:如何设置行业、换手率与风险因子暴露约束?

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

问题描述

在使用投资组合优化器时,如何限制单个行业的持仓权重和整体换手率?

解决方案

在聚宽(JoinQuant)的投资组合优化器(portfolio_optimizer)中,可以通过设置 constraints 参数列表来实现对行业权重和换手率的限制。

具体使用的约束类如下:

  1. 限制单个行业的持仓权重:使用 IndustriesConstraint(针对行业分类限制)或 IndustryConstraint(针对特定行业限制)。通常为了限制所有行业的最大权重(例如每个申万一级行业不超过 10%),使用 IndustriesConstraint 更为方便。
  2. 限制整体换手率:使用 TurnoverConstraint。需要注意的是,使用此约束时必须传入当前的持仓权重(current_portfolio),否则优化器无法计算换手率。

核心代码逻辑

  • 行业限制IndustriesConstraint(industry_code='sw_l1', low=0.0, high=0.1) 表示限制申万一级行业分类下,每个行业的权重在 0% 到 10% 之间。
  • 换手率限制TurnoverConstraint(limit=0.2, current_portfolio=current_weights) 表示相对于当前持仓,优化后的组合换手率不超过 20%。

完整策略代码示例

以下是一个完整的策略示例,展示了如何在每日调仓时应用这两个约束。

# -*- coding: utf-8 -*-
from jqdata import *
from jqlib.optimizer import *
import pandas as pd
import numpy as np

def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # 设定股票池:以上证50为例
    g.stock_pool = get_index_stocks('000016.XSHG')
    
    # 每天开盘运行
    run_daily(market_open, time='09:30')

def market_open(context):
    # 1. 获取当前持仓权重,用于计算换手率
    # 如果是空仓,current_portfolio 为 None
    current_weights = None
    if context.portfolio.total_value > 0:
        # 获取当前持仓的标的和对应的市值
        positions = context.portfolio.positions
        if len(positions) > 0:
            codes = []
            weights = []
            total_value = context.portfolio.total_value
            for code, pos in positions.items():
                codes.append(code)
                # 计算个股占总资产的权重
                weights.append(pos.value / total_value)
            
            # 构造 pandas.Series,索引为股票代码,值为权重
            current_weights = pd.Series(data=weights, index=codes)

    # 2. 设定优化目标和约束条件
    # 目标:最小化组合方差 (MinVariance)
    target = MinVariance(count=250)
    
    # 约束条件列表
    constraints = []
    
    # --- 约束 A: 权重约束 ---
    # 限制组合中股票总仓位在 95% 到 100% 之间
    constraints.append(WeightConstraint(low=0.95, high=1.0))
    
    # --- 约束 B: 行业权重限制 ---
    # 使用 IndustriesConstraint 限制 'sw_l1' (申万一级行业)
    # 限制每个行业的配置比例不超过 20% (high=0.2)
    constraints.append(IndustriesConstraint(industry_code='sw_l1', low=0.0, high=0.2))
    
    # --- 约束 C: 换手率限制 ---
    # 限制单次调仓的换手率不超过 20% (limit=0.2)
    # 注意:必须传入 current_portfolio
    if current_weights is not None:
        constraints.append(TurnoverConstraint(limit=0.2, current_portfolio=current_weights))
    
    # --- 约束 D: 个股边界 (可选) ---
    # 限制单只股票权重不超过 10%
    bounds = [Bound(0.0, 0.1)]

    # 3. 运行优化器
    # 注意:date应使用上一交易日,避免未来函数
    optimized_weight = portfolio_optimizer(
        date=context.previous_date,
        securities=g.stock_pool,
        target=target,
        constraints=constraints,
        bounds=bounds,
        default_port_weight_range=[0.0, 1.0],
        ftol=1e-09,
        return_none_if_fail=True
    )

    # 4. 执行调仓
    if optimized_weight is None:
        log.warn("组合优化失败,保持当前持仓")
    else:
        log.info("优化成功,开始调仓")
        # 获取当前总资产
        total_value = context.portfolio.total_value
        
        # 先卖出不在优化结果中的股票
        for stock in context.portfolio.positions.keys():
            if stock not in optimized_weight.index:
                order_target_value(stock, 0)
        
        # 根据优化后的权重买入/调整
        for stock in optimized_weight.index:
            target_value = total_value * optimized_weight[stock]
            order_target_value(stock, target_value)

关键点说明

  1. IndustriesConstraint vs IndustryConstraint:

    • IndustriesConstraint(industry_code='sw_l1', ...):这是批量设置。它表示该分类标准(如申万一级)下的每一个行业,其权重都必须满足 lowhigh 的限制。
    • IndustryConstraint(industry_code='801780', ...):这是单独设置。它只限制特定的行业代码(如银行I)的权重。
    • 通常为了风控,使用 IndustriesConstraint 限制所有行业(如不超过 30%)是最常用的方法。
  2. TurnoverConstraintcurrent_portfolio:

    • 优化器需要知道你现在手里有什么,才能计算为了达到新目标需要买卖多少(即换手率)。
    • 必须构建一个 pandas.Series,Index 是股票代码,Value 是权重(持仓市值 / 总资产)。
    • 如果策略刚开始运行(空仓),current_portfolio 可以传 None,此时换手率约束实际上限制的是开仓量(通常此时不应设置过小的换手率限制,否则可能无法建仓)。
  3. 优化失败处理:

    • 当约束条件过多或相互冲突时(例如:要求换手率极低,但又要求行业权重剧烈变化),优化器可能无法找到可行解。
    • 设置 return_none_if_fail=True,并检查返回值是否为 None,以防止程序报错中断。

Q&A

Q1: industry_code 支持哪些分类标准?
A1: 支持 'sw_l1' (申万一级), 'sw_l2' (申万二级), 'sw_l3' (申万三级), 'jq_l1' (聚宽一级), 'jq_l2' (聚宽二级), 'zjw' (证监会行业)。

Q2: 如果优化器返回 None,通常是什么原因?
A2: 通常是因为约束条件太严格导致无解。例如,股票池中只有银行股,但你限制银行行业权重为 0;或者限制换手率为 1%,但目标组合与当前持仓差异巨大。建议适当放宽 ftol 精度或放宽约束条件。

Q3: TurnoverConstraint 限制的是单边还是双边换手率?
A3: 它限制的是调整权重的绝对值之和的一半(通常定义)。简单理解为,如果 limit=0.2,意味着你买入+卖出的调整量相对于总资产的比例受到限制,防止剧烈调仓产生高额手续费。