🚀 AI 一键生成 joinquant 策略代码
立即体验

聚宽红利增强策略:解决 set 合并股票列表导致顺序随机与回测不稳定的方法

joinquant | 发布时间: 2026/8/10
以下内容由 EasyQuant 生成。

问题描述

JoinQuant 红利增强策略:如何避免 set 合并候选股导致排序随机?
聚宽中用 set 合并多个红利股票列表后顺序随机,如何重新按股息率排序以保证回测结果稳定并修复组合选股逻辑?

解决方案

在 JoinQuant(聚宽)量化平台上编写红利增强策略时,我们经常需要合并多个股票列表(例如:红利指数成分股、特定高股息行业股等)。为了去除重复的股票,很多开发者会使用 Python 的 set 集合进行合并。

然而,Python 中的 set 是无序的。当您将 set 转换回 list 时,股票的顺序是随机的。在回测或模拟盘中,如果您的资金不足以买入池中的所有股票,下单函数(如 orderorder_target)会按照列表的当前顺序依次下单。由于 set 导致的随机顺序,每次运行策略时买入的股票可能完全不同,从而导致回测结果不稳定、无法复现

要解决这个问题,必须在合并去重后,重新按照股息率(或市值等因子)进行显式排序。以下是完整的解决方案和修复代码。

解决方案:合并去重并按股息率重新排序

我们可以通过以下步骤修复该逻辑:

  1. 使用 set 合并多个股票列表并去重。
  2. 使用 get_fundamentals 查询这些合并后股票的最新股息率(或通过 get_factor_values 获取相关因子)。
  3. 对查询到的数据进行降序排序,获取排名前 N 的股票作为最终交易池。

修复后的 Python 源码示例

以下是一个完整的聚宽策略框架,展示了如何安全地合并股票池并按股息率重新排序:

import pandas as pd
from jqdata import *

def initialize(context):
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    
    # 每天开盘时运行
    run_daily(market_open, time='09:30')

def market_open(context):
    # 1. 获取不同的候选股票池
    # 假设列表 A:上证红利成分股
    list_a = get_index_stocks('000015.XSHG') 
    # 假设列表 B:银行和房地产行业的股票(高股息行业)
    list_b = get_industry_stocks('801780') + get_industry_stocks('801180') 
    
    # 2. 使用 set 合并并去重(此时顺序已随机)
    merged_set = set(list_a + list_b)
    merged_list = list(merged_set)
    
    # 3. 查询这些股票的股息率,用于重新排序,消除随机性
    # 股息率数据在 valuation 表中,字段为 dividend_liability(或通过财务数据计算)
    # 这里我们使用 get_fundamentals 获取市值表中的股息率/估值相关指标
    q = query(
        valuation.code,
        valuation.market_cap,
        # 也可以使用其他因子,此处以市值和PE作为演示,实际红利策略可查询具体股息率指标
        valuation.pe_ratio 
    ).filter(
        valuation.code.in_(merged_list)
    )
    
    df = get_fundamentals(q, date=context.previous_date)
    
    if not df.empty:
        # 4. 显式排序:按照特定指标(如 PE 升序,或市值降序)进行排序
        # 这样可以确保每次合并后的股票顺序完全一致,消除 set 的随机性
        df_sorted = df.sort_values(by='pe_ratio', ascending=True) 
        
        # 获取排序后的股票代码列表
        final_buy_list = list(df_sorted['code'])[:10] # 选择前10只
        
        log.info("今日确定的稳定股票池: %s" % final_buy_list)
        
        # 5. 执行调仓逻辑
        # 卖出不在新股票池中的持仓
        for stock in list(context.portfolio.positions.keys()):
            if stock not in final_buy_list:
                order_target(stock, 0)
                log.info("卖出不在红利池中的股票: %s" % stock)
                
        # 买入新股票池中的股票
        position_count = len(context.portfolio.positions)
        if position_count < 5:
            cash = context.portfolio.available_cash / (5 - position_count)
            for stock in final_buy_list:
                if stock not in context.portfolio.positions:
                    order_value(stock, cash)
                    log.info("买入红利股: %s" % stock)
    else:
        log.warn("未能获取到财务数据,跳过今日调仓")

核心要点总结

  1. 绝对不要直接对 list(set(stocks)) 进行循环下单:这会导致回测和实盘的下单顺序不一致,产生严重的幸存者偏差或随机交易误差。
  2. 始终进行显式排序:在将 set 转换回 list 后,必须调用 .sort() 或使用 Pandas 的 sort_values() 按照股票代码、股息率或市值进行二次排序,以确保数据流的确定性。
  3. 避免未来函数:在获取财务数据或因子值进行排序时,date 参数应传入 context.previous_date(前一交易日),防止引入未来数据。