问题描述
如图,定义了股票过滤函数, 过滤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(元素B),发现B是次新股,执行remove(B)。 - 此时列表变成了
[A, C, D]。 - 下一次循环,Python 会去取索引
2的元素。但在新列表中,索引2的元素变成了D! - 结果就是:元素
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,还能让代码运行速度更快、可读性更强。建议全面检查策略中所有的过滤逻辑(如过滤涨跌停、过滤特定行业等),确保使用了正确的列表操作方法。