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

为什么 JoinQuant 策略中股票过滤函数会出现遗漏?(附正确写法)

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

问题描述

提问:为何股票过滤函数有遗漏?

如图,定义了股票过滤函数, 过滤ST及次新股。
对stocks使用filter_stocks过滤后得到stocks1,对stocks1再次使用filter_stocks过滤后得stocks2,理论上stocks1与stocks2应是一致的,为何会出现差别?
另外,经过检验,stocks1与stocks2相差的这些股票确实是上市不满一年的股票,为何第一次过滤有遗漏?

解决方案

问题解析:为什么股票过滤函数会产生遗漏?

在 JoinQuant(聚宽)编写量化策略时,对股票池进行过滤(如剔除 ST 股、停牌股、次新股)是非常常见的操作。如果您发现第一次过滤有遗漏,第二次过滤才剔除干净,这通常不是 JoinQuant API 的问题,而是触发了 Python 编程中一个非常经典的陷阱:在遍历列表的同时修改(删除)了列表元素

错误代码示例(导致遗漏的原因)

很多初学者在编写过滤函数时,会使用类似下面的代码:

def filter_stocks(stock_list):
    for stock in stock_list:
        # 假设 is_new_stock 是判断次新股的逻辑
        if is_new_stock(stock):
            stock_list.remove(stock) # 错误:在遍历时删除了元素
    return stock_list

原理解析:
当 Python 的 for 循环遍历列表时,它是基于索引进行的。假设列表为 [A, B, C, D]

  1. 循环到索引 1(元素 B),发现 B 是次新股,执行 remove(B)
  2. 此时列表变成了 [A, C, D]
  3. 下一次循环,Python 会去取索引 2 的元素。但在新列表中,索引 2 的元素变成了 D
  4. 结果就是:元素 C 被直接跳过了,根本没有经过条件判断!

这就是为什么您第一次过滤会有遗漏,而把漏掉的列表再过滤一次(第二次过滤)时,原本被跳过的元素才被正确剔除的原因。


正确的过滤写法

为了避免这个问题,我们绝不能在遍历列表的同时直接从中删除元素。以下是三种推荐的正确写法:

方法一:使用列表推导式(最推荐,Pythonic 且高效)

创建一个新的列表,只保留符合条件的股票,而不是去删除不符合条件的股票。

def filter_stocks(stock_list, context):
    # 获取当前时间
    current_date = context.current_dt.date()
    
    # 1. 过滤停牌和ST股
    current_data = get_current_data()
    
    # 使用列表推导式生成新列表
    valid_stocks = [
        stock for stock in stock_list 
        if not current_data[stock].paused  # 未停牌
        and not current_data[stock].is_st  # 非ST/*ST
    ]
    
    # 2. 过滤次新股(上市不满一年,即365天)
    final_stocks = []
    for stock in valid_stocks:
        stock_info = get_security_info(stock)
        if stock_info is None:
            continue
        # 计算上市天数
        days_public = (current_date - stock_info.start_date).days
        if days_public >= 365:
            final_stocks.append(stock)
            
    return final_stocks

方法二:遍历列表的副本(切片)

如果您一定要使用 remove,请遍历原列表的副本(使用 stock_list[:]),这样删除原列表的元素就不会影响遍历过程。

def filter_stocks(stock_list, context):
    current_date = context.current_dt.date()
    current_data = get_current_data()
    
    # 注意这里的 stock_list[:] 创建了一个浅拷贝
    for stock in stock_list[:]: 
        # 过滤停牌或ST
        if current_data[stock].paused or current_data[stock].is_st:
            stock_list.remove(stock)
            continue
            
        # 过滤次新股
        stock_info = get_security_info(stock)
        if stock_info is not None:
            days_public = (current_date - stock_info.start_date).days
            if days_public < 365:
                stock_list.remove(stock)
                
    return stock_list

方法三:使用内置的 filter 函数

def is_valid(stock, current_date, current_data):
    if current_data[stock].paused or current_data[stock].is_st:
        return False
    stock_info = get_security_info(stock)
    if stock_info and (current_date - stock_info.start_date).days < 365:
        return False
    return True

def filter_stocks(stock_list, context):
    current_date = context.current_dt.date()
    current_data = get_current_data()
    # filter 返回的是迭代器,需要转回 list
    return list(filter(lambda s: is_valid(s, current_date, current_data), stock_list))

总结

在 JoinQuant 编写策略时,永远不要在 for 循环中直接 remove 正在遍历的列表元素。改用列表推导式不仅能彻底解决“过滤遗漏”的 Bug,还能让代码运行速度更快、可读性更强。建议全面检查策略中所有的过滤逻辑(如过滤涨跌停、过滤特定行业等),确保使用了正确的列表操作方法。