问题描述
解决方案
在聚宽(JoinQuant)的投资组合优化器(portfolio_optimizer)中,可以通过设置 constraints 参数列表来实现对行业权重和换手率的限制。
具体使用的约束类如下:
- 限制单个行业的持仓权重:使用
IndustriesConstraint(针对行业分类限制)或IndustryConstraint(针对特定行业限制)。通常为了限制所有行业的最大权重(例如每个申万一级行业不超过 10%),使用IndustriesConstraint更为方便。 - 限制整体换手率:使用
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)
关键点说明
-
IndustriesConstraintvsIndustryConstraint:IndustriesConstraint(industry_code='sw_l1', ...):这是批量设置。它表示该分类标准(如申万一级)下的每一个行业,其权重都必须满足low和high的限制。IndustryConstraint(industry_code='801780', ...):这是单独设置。它只限制特定的行业代码(如银行I)的权重。- 通常为了风控,使用
IndustriesConstraint限制所有行业(如不超过 30%)是最常用的方法。
-
TurnoverConstraint的current_portfolio:- 优化器需要知道你现在手里有什么,才能计算为了达到新目标需要买卖多少(即换手率)。
- 必须构建一个
pandas.Series,Index 是股票代码,Value 是权重(持仓市值 / 总资产)。 - 如果策略刚开始运行(空仓),
current_portfolio可以传None,此时换手率约束实际上限制的是开仓量(通常此时不应设置过小的换手率限制,否则可能无法建仓)。
-
优化失败处理:
- 当约束条件过多或相互冲突时(例如:要求换手率极低,但又要求行业权重剧烈变化),优化器可能无法找到可行解。
- 设置
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,意味着你买入+卖出的调整量相对于总资产的比例受到限制,防止剧烈调仓产生高额手续费。