问题描述
解决方案
在聚宽(JoinQuant)平台上构建以“收益最大化”为目标的投资组合,主要依赖于 jqlib.optimizer 库中的 portfolio_optimizer 函数以及 MaxProfit 优化目标。
核心逻辑说明
- 优化目标 (
target):使用MaxProfit(count=250)。该目标函数会寻找在过去一段时间(如250个交易日)内,组合预期收益率最高的权重配置。 - 约束条件 (
constraints):为了防止资金全部集中在某一只历史表现最好的股票上(导致风险过高),通常需要添加约束条件,例如:- 权重和限制:
WeightEqualConstraint(limit=1.0),保证满仓操作。 - 单标的权重边界 (
bounds):例如Bound(0.0, 0.1),限制单只股票持仓不超过总资产的10%,强制分散投资。
- 权重和限制:
- 股票池:通常选择一个指数成分股(如沪深300)作为待选股票池。
策略代码实现
以下是一个完整的策略代码。该策略每月调仓一次,在沪深300成分股中寻找历史收益最高的组合配置,同时限制单只股票权重不超过10%。
# -*- 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)
# 设置交易成本:印花税千分之一(卖出),佣金万分之三,最低5元
set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type='stock')
# 设定每月第一个交易日进行调仓
run_monthly(rebalance, monthday=1, time='09:30')
def rebalance(context):
"""
调仓函数:执行组合优化并调整持仓
"""
# 1. 获取待优化股票池:这里选择沪深300成分股
# 注意:为了演示速度和稳定性,实际使用中可以先进行基本面筛选
target_index = '000300.XSHG'
securities = get_index_stocks(target_index)
# 2. 设定优化参数
# 目标:最大化组合收益 (基于过去250个交易日的数据)
target = MaxProfit(count=250)
# 约束条件:
# (1) 组合总权重等于 1.0 (满仓)
# (2) 单只股票权重范围 0.0 ~ 0.1 (防止过度集中)
constraints = [
WeightEqualConstraint(limit=1.0)
]
bounds = [Bound(0.0, 0.1)]
# 3. 运行优化器
# 注意:date参数使用 context.previous_date 以避免未来函数
log.info("开始进行组合优化计算...")
optimized_weight = portfolio_optimizer(
date=context.previous_date,
securities=securities,
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("组合优化失败,本次不调整仓位。")
return
log.info("优化成功,开始调仓。")
# 获取当前总资产
total_value = context.portfolio.total_value
# 获取当前持仓的股票列表
current_positions = list(context.portfolio.positions.keys())
# 卖出不在优化结果中的股票,或者权重变为0的股票
for stock in current_positions:
if stock not in optimized_weight.index or optimized_weight[stock] == 0:
order_target_value(stock, 0)
# 买入/调整优化结果中的股票
# 遍历优化后的权重 Series
for stock, weight in optimized_weight.items():
if weight > 0:
target_value = total_value * weight
order_target_value(stock, target_value)
def handle_data(context, data):
"""
按分钟回测时每分钟调用,按天回测时每天调用
本策略为月度调仓,此处留空
"""
pass
代码关键点解析
from jqlib.optimizer import *: 必须导入此库才能使用优化器功能。MaxProfit(count=250): 这是核心。它告诉优化器,我们的目标是基于过去250天(约一年)的历史数据,找到收益率最高的组合。你可以根据需求修改count的值(例如60代表过去一个季度)。bounds=[Bound(0.0, 0.1)]: 如果不加这个边界限制,MaxProfit算法通常会倾向于将 100% 的资金分配给过去涨幅最大的那一只股票,这会带来巨大的个股风险。设置 0.1 的上限强制资金分散到至少 10 只股票上。portfolio_optimizer(...): 这是执行计算的函数。date=context.previous_date: 必须使用前一个交易日的数据进行计算,如果在开盘时使用当日数据会引入未来函数。return_none_if_fail=True: 优化是一个数学求解过程,可能会因为约束冲突或数据缺失而失败。设置此项可以防止程序报错崩溃,允许我们在失败时跳过调仓。
Q&A
Q: 为什么使用 MaxProfit 优化出来的组合风险很高?
A: MaxProfit 仅仅关注历史收益率的最大化。在没有其他约束的情况下,它倾向于选择波动率极大、历史涨幅极高的股票。为了控制风险,建议配合 bounds(单标的权重限制)使用,或者结合 RiskParity(风险平价)等混合目标,或在选股阶段先剔除高风险标的。
Q: portfolio_optimizer 运行速度慢怎么办?
A: 组合优化涉及复杂的数学规划求解。如果股票池很大(如全A股),计算会非常耗时甚至超时。建议:
- 缩小股票池(如仅使用沪深300或中证500)。
- 在调用优化器前,先通过因子筛选(如市值、PE等)过滤掉一部分股票,只对剩下的几十只股票进行权重优化。
- 适当调大
ftol参数(精度容忍度),例如设为1e-6。
Q: 如何在最大化收益的同时限制最大回撤?
A: 可以在 constraints 列表中添加 MaxDrawdownConstraint。例如:
constraints = [
WeightEqualConstraint(limit=1.0),
MaxDrawdownConstraint(limit=-0.2, count=250) # 限制组合过去一年的最大回撤不超过20%
]
注意:约束条件越多,优化失败(无解)的概率越高。