问题描述
解决方案
在聚宽(JoinQuant)平台中,要根据股票打分构建最大化分数的投资组合,核心是使用 jqlib.optimizer 库中的 portfolio_optimizer 函数,并将优化目标(target)设置为 MaxScore。
核心步骤
- 获取股票池与数据:确定待选股票(如沪深300成分股),并获取用于打分的数据(如财务指标、因子值等)。
- 构建打分 Series:将数据处理为
pandas.Series格式,索引为股票代码,值为对应的分数。 - 设置优化器:
- Target: 使用
MaxScore(scores=...),传入打分数据。 - Constraints: 设置约束条件,例如总仓位限制(
WeightConstraint)。 - Bounds: 设置单只股票的权重边界(
Bound),防止持仓过于集中。
- Target: 使用
- 执行调仓:根据优化器返回的权重进行买卖操作。
策略代码示例
以下是一个完整的策略示例。该策略每月调仓一次,以沪深300成分股为股票池,使用 ROE(净资产收益率) 作为打分依据(假设ROE越高越好),构建最大化该分数的投资组合。同时限制单只股票最大权重为 10%。
# -*- coding: utf-8 -*-
from jqdata import *
from jqlib.optimizer import *
import pandas as pd
def initialize(context):
"""
初始化函数
"""
# 设定基准
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(rebalance, monthday=1, time='09:30')
def rebalance(context):
"""
调仓函数
"""
print(f"开始调仓: {context.current_dt.date()}")
# 1. 确定股票池:沪深300成分股
stock_pool = get_index_stocks('000300.XSHG')
# 2. 获取数据并构建打分 Series
# 这里以 ROE (净资产收益率) 作为分数的代理,ROE越高,分数越高
# 注意:get_fundamentals 在回测中只能获取前一个交易日的数据
q = query(
valuation.code,
indicator.roe
).filter(
valuation.code.in_(stock_pool)
)
df = get_fundamentals(q, date=context.previous_date)
# 数据清洗:去除空值
df = df.dropna()
# 构建 scores Series: index为股票代码, value为分数
# 如果有更复杂的打分逻辑(如多因子合成),请在此处处理
scores = df.set_index('code')['roe']
# 3. 运行投资组合优化器
# 目标:最大化分数 (MaxScore)
# 约束:
# - WeightConstraint: 组合总权重在 99% 到 100% 之间 (满仓)
# - Bound: 单只股票权重上限为 10% (0.1)
try:
optimized_weights = portfolio_optimizer(
date=context.previous_date, # 优化日期
securities=list(scores.index), # 参与优化的股票列表
target=MaxScore(scores=scores), # 【核心】设置目标为最大化分数
constraints=[
WeightConstraint(low=0.99, high=1.0) # 总仓位约束
],
bounds=[Bound(0, 0.1)], # 单标的权重约束:0% ~ 10%
default_port_weight_range=[0.0, 1.0],
ftol=1e-09,
return_none_if_fail=True
)
except Exception as e:
print(f"优化器运行异常: {e}")
return
# 4. 执行交易
if optimized_weights is None:
print("警告:组合优化失败,未返回权重。")
return
# 获取当前总资产
total_value = context.portfolio.total_value
# 获取当前持仓
current_positions = context.portfolio.positions
# 先卖出:不在优化结果中的股票,或者权重变为0的股票
for stock in current_positions:
if stock not in optimized_weights.index or optimized_weights[stock] == 0:
order_target_value(stock, 0)
# 再买入/调整:根据优化后的权重调整持仓
for stock, weight in optimized_weights.items():
if weight > 0:
target_value = total_value * weight
order_target_value(stock, target_value)
print("调仓完成")
代码关键点解析
MaxScore(scores): 这是策略的核心。scores必须是一个pandas.Series,其中索引是股票代码,数值是该股票的得分。优化器会尝试分配权重,使得组合的加权平均得分最大化。constraints(约束):WeightConstraint(low=0.99, high=1.0): 强制要求优化器给出的权重之和接近 1.0(即满仓操作)。如果不设置这个,优化器可能会为了规避风险而选择空仓。
bounds(边界):Bound(0, 0.1): 这是一个非常重要的风控参数。如果没有这个边界,MaxScore逻辑会倾向于将 100% 的资金全部买入分数最高的那一只股票。设置 0.1 表示单只股票最大持仓不能超过 10%,从而强制分散投资。
- 数据获取: 示例中使用了
get_fundamentals获取 ROE。在实际应用中,您可以替换为您自己的多因子打分模型、机器学习预测值或技术指标评分。
Q&A
Q1: 如果我想最小化分数(例如寻找估值最低的股票),应该怎么做?
A1: 您可以使用 MinScore(scores) 作为 target 参数。用法与 MaxScore 完全一致,优化器会寻找加权平均分最低的组合。
Q2: 为什么优化器有时候返回 None?
A2: 这通常是因为约束条件(Constraints)之间存在冲突,或者在给定的边界(Bounds)内无法找到满足所有约束的解。例如,如果股票池只有 5 只股票,但设置了单只股票最大权重为 10% (Bound(0, 0.1)),那么总仓位最多只能达到 50%,如果同时设置了 WeightConstraint(low=0.9, high=1.0),优化器就会因为无法满足总仓位要求而失败。
Q3: scores 数据中包含 NaN 会怎么样?
A3: 如果 scores 中包含 NaN (空值),优化器通常会报错或无法处理。务必在传入优化器之前使用 .dropna() 或 .fillna() 对数据进行清洗。
Q4: 如何结合风险控制(如限制组合波动率)?
A4: 您可以在 constraints 列表中添加风险相关的约束。例如,添加 AnnualStdConstraint(limit=0.2) 可以限制组合的预期年化波动率不超过 20%。但这通常需要结合风险模型数据。