🚀 AI 一键生成 joinquant 策略代码

JoinQuant 组合优化:构建 MaxScore(打分最大化)投资组合

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

问题描述

如何根据股票打分构建最大化分数的投资组合?

解决方案

在聚宽(JoinQuant)平台中,要根据股票打分构建最大化分数的投资组合,核心是使用 jqlib.optimizer 库中的 portfolio_optimizer 函数,并将优化目标(target)设置为 MaxScore

核心步骤

  1. 获取股票池与数据:确定待选股票(如沪深300成分股),并获取用于打分的数据(如财务指标、因子值等)。
  2. 构建打分 Series:将数据处理为 pandas.Series 格式,索引为股票代码,值为对应的分数。
  3. 设置优化器
    • Target: 使用 MaxScore(scores=...),传入打分数据。
    • Constraints: 设置约束条件,例如总仓位限制(WeightConstraint)。
    • Bounds: 设置单只股票的权重边界(Bound),防止持仓过于集中。
  4. 执行调仓:根据优化器返回的权重进行买卖操作。

策略代码示例

以下是一个完整的策略示例。该策略每月调仓一次,以沪深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("调仓完成")

代码关键点解析

  1. MaxScore(scores): 这是策略的核心。scores 必须是一个 pandas.Series,其中索引是股票代码,数值是该股票的得分。优化器会尝试分配权重,使得组合的加权平均得分最大化。
  2. constraints (约束):
    • WeightConstraint(low=0.99, high=1.0): 强制要求优化器给出的权重之和接近 1.0(即满仓操作)。如果不设置这个,优化器可能会为了规避风险而选择空仓。
  3. bounds (边界):
    • Bound(0, 0.1): 这是一个非常重要的风控参数。如果没有这个边界,MaxScore 逻辑会倾向于将 100% 的资金全部买入分数最高的那一只股票。设置 0.1 表示单只股票最大持仓不能超过 10%,从而强制分散投资。
  4. 数据获取: 示例中使用了 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%。但这通常需要结合风险模型数据。