问题描述
解决方案
在聚宽(JoinQuant)平台上,构建一个满足特定约束(如行业占比、换手率限制)并追求最小化风险的投资组合,可以使用 jqlib.optimizer 库中的 投资组合优化器 (portfolio_optimizer) 来实现。
核心思路
- 优化目标:使用
MinVariance(最小化组合方差)来实现风险最小化。 - 行业约束:使用
IndustryConstraint来限制金融行业的权重在 10% 到 20% 之间。我们需要知道金融行业的代码(聚宽一级行业中金融指数代码通常为'HY007')。 - 换手率约束:使用
TurnoverConstraint来限制当次调仓相对于当前持仓的换手率不超过 30%。 - 权重约束:通常还需要一个
WeightConstraint来保证总仓位在合理区间(例如 95%~100%)。
策略代码实现
以下是一个完整的策略示例。该策略以沪深300成分股为股票池,每月调仓一次,在满足上述约束的前提下最小化风险。
# -*- coding: utf-8 -*-
# 导入函数库
from jqdata import *
from jqlib.optimizer import *
import pandas as pd
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定交易费率
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 设定按月运行,每月第一个交易日
run_monthly(market_open, monthday=1, time='09:30')
def market_open(context):
# 1. 确定股票池:这里选取沪深300成分股
stock_pool = get_index_stocks('000300.XSHG')
# 2. 获取当前持仓权重,用于计算换手率约束
# 如果是空仓,current_portfolio 为 None 或空 Series
current_positions = context.portfolio.positions
if len(current_positions) > 0:
total_value = context.portfolio.total_value
current_weights = pd.Series({code: pos.value / total_value for code, pos in current_positions.items()})
else:
current_weights = None
# 3. 设定优化目标和约束条件
# 目标:最小化组合方差(风险最小化)
# count=250 表示使用过去250个交易日的数据来计算协方差矩阵
target = MinVariance(count=250)
constraints = []
# 约束1:总权重限制在 95% 到 100% 之间(保持高仓位)
constraints.append(WeightConstraint(low=0.95, high=1.0))
# 约束2:金融行业持仓比例在 10% 到 20% 之间
# 'HY007' 是聚宽一级行业中的金融指数代码
constraints.append(IndustryConstraint(industry_code='HY007', low=0.1, high=0.2))
# 约束3:整体换手率不能超过 30%
# 注意:如果是第一次调仓(current_weights为None),此约束可能不生效或被视为从0仓位建仓
if current_weights is not None:
constraints.append(TurnoverConstraint(limit=0.3, current_portfolio=current_weights))
# 4. 运行优化器
# 注意:date应使用前一个交易日,避免未来函数
try:
optimized_weights = portfolio_optimizer(
date=context.previous_date,
securities=stock_pool,
target=target,
constraints=constraints,
# 单只股票权重上限设为10%,防止过度集中
bounds=[Bound(0.0, 0.1)],
ftol=1e-09,
return_none_if_fail=True
)
except Exception as e:
log.error("优化器运行异常: %s" % e)
optimized_weights = None
# 5. 执行调仓
if optimized_weights is None:
log.warn("组合优化失败,本次不调仓")
return
log.info("优化成功,开始调仓")
# 获取当前总资产
total_value = context.portfolio.total_value
# 先卖出不在优化结果中的股票
for stock in context.portfolio.positions:
if stock not in optimized_weights.index:
order_target_value(stock, 0)
# 根据优化后的权重买入/调整股票
for stock, weight in optimized_weights.items():
target_value = total_value * weight
# 只有当目标价值大于0时才下单
if target_value > 0:
order_target_value(stock, target_value)
代码关键点解析
-
MinVariance(count=250):- 这是核心目标函数。它会根据过去 250 个交易日的历史数据计算协方差矩阵,寻找波动率(方差)最小的组合。
-
IndustryConstraint(industry_code='HY007', low=0.1, high=0.2):- 这是行业约束。
'HY007'是聚宽一级行业分类中的“金融指数”。 - 该行代码强制优化器选出的组合中,属于金融行业的股票权重之和必须落在 0.1 (10%) 到 0.2 (20%) 之间。
- 提示:您可以在聚宽的研究环境中通过
get_industries(name='jq_l1')查看所有行业代码。
- 这是行业约束。
-
TurnoverConstraint(limit=0.3, current_portfolio=current_weights):- 这是换手率约束。它需要传入当前的持仓权重 (
current_portfolio)。 - 优化器会计算
sum(abs(新权重 - 旧权重)),并确保这个值(双边换手率的一半,即单边换手)不超过limit。这在资金量大或为了控制交易成本时非常有用。
- 这是换手率约束。它需要传入当前的持仓权重 (
-
portfolio_optimizer:- 这是执行优化的主函数。如果因为约束条件过于苛刻(例如市场剧烈波动导致无法在换手率限制内满足行业占比),优化可能会失败并返回
None。代码中加入了if optimized_weights is None的判断来处理这种情况,避免程序报错。
- 这是执行优化的主函数。如果因为约束条件过于苛刻(例如市场剧烈波动导致无法在换手率限制内满足行业占比),优化可能会失败并返回
Q&A
Q: 如何查找其他行业的代码?
A: 您可以在聚宽的研究模块中运行以下代码来查看聚宽一级行业列表:
from jqdata import *
df = get_industries(name='jq_l1')
print(df)
找到对应的行业代码(如医药、科技等)替换代码中的 'HY007' 即可。
Q: 为什么优化器有时候会返回 None?
A: 当约束条件之间存在冲突,或者在给定的约束下无法找到可行解时(例如要求换手率极低,但为了满足行业占比必须大幅调仓),优化器会失败。建议在 portfolio_optimizer 中设置 return_none_if_fail=True 并做好异常处理。
Q: MinVariance 是如何衡量风险的?
A: 它使用的是历史波动率。默认参数 count=250 表示使用过去一年的日收益率数据来计算协方差矩阵。如果您希望对近期波动更敏感,可以减小 count 的值(例如 60)。