3.4 股票池筛选与流动性管理 (Universe Selection)¶
在量化交易中,你交易什么往往和如何交易同样重要。 一个包含大量非流动性"垃圾股"的股票池,即使因子 IC 再高,实盘也无法按信号执行。 本节介绍机构构建股票池(Trading Universe)的标准流程。
这一节讲什么?
股票池定义决定哪些证券进入研究样本。本节介绍流动性过滤、市值筛选和行业集中度约束,并说明幸存者偏差与时点可得性。
学习目标¶
- 理解流动性(Liquidity)在量化交易中的核心地位
- 掌握平均日成交量(ADV)和市值(Market Cap)过滤标准
- 学会评估股票池的行业集中度风险
- 使用换手率指标评估股票可交易性
1. 这节究竟解决什么实际问题?¶
3.4 股票池筛选与流动性管理 (Universe Selection) 的具体任务是:用流动性和可交易性筛出策略真正能覆盖的股票池。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
理解收益率、滚动窗口和基础 DataFrame 操作。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
使用固定日频价格样本;高低开收和成交量如为教学代理,会在代码中明确说明。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 3.4 股票池筛选与流动性管理 (Universe Selection) 的输入、计算步骤、输出和局限;尤其能说明:用流动性和可交易性筛出策略真正能覆盖的股票池。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
技术指标将历史价格或成交量压缩为统计量。指标本身不包含交易方向、执行时点和风险约束。
滚动指标只使用窗口结束日及以前的数据;交易仓位必须至少滞后一个可交易周期。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
公共指标函数减少重复代码;首次学习仍应读懂每个滚动、平滑和标准化步骤。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“用流动性和可交易性筛出策略真正能覆盖的股票池。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
窗口不足时误读 NaN、同日用收盘信号按收盘成交、忽略除零、把指标阈值当普适规律。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
测试大量窗口后只保留样本内表现最高的参数会产生数据窥探偏差;任何信号都应和简单基线比较。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/03_indicators/04_universe_selection_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/03_indicators/04_universe_selection_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Technical Analysis of the Financial Markets(Murphy)。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 05_cointegration_correlation。本节的概念会成为理解它的输入,而不是孤立的名词。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
np.random.seed(42)
# 模拟一个包含 500 只股票的"全市场"数据集
n_stocks = 500
# 模拟关键属性
market_caps = np.random.lognormal(mean=20, sigma=2, size=n_stocks) # 市值(万元)
adv = market_caps * np.random.uniform(0.001, 0.008, n_stocks) # 日均成交额(正相关市值)
price = np.random.lognormal(mean=3, sigma=1, size=n_stocks) # 股价
sectors = np.random.choice(['金融', '科技', '消费', '医药', '工业', '能源', '材料', '公用事业'], n_stocks)
universe_df = pd.DataFrame({
'stock_id': [f'STK{i:04d}' for i in range(n_stocks)],
'market_cap_m': market_caps / 1e6, # 亿元
'adv_m': adv / 1e4, # 万元
'price': price,
'sector': sectors
})
print(f"全市场股票数量: {len(universe_df)}")
print(universe_df.describe().round(2))
全市场股票数量: 500
market_cap_m adv_m price
count 500.00 500.00 500.00
mean 5327.60 2026.15 34.64
std 49759.29 13521.93 39.19
min 0.74 0.53 1.11
25% 119.57 47.66 10.95
50% 497.74 202.45 21.55
75% 1733.86 717.03 40.41
max 1077282.71 244919.89 264.99
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
1. 流动性过滤:平均日成交量(ADV)¶
流动性是量化策略的隐性成本放大器。
如果你的策略信号每天要买入 1000 万元某股票,但这只股票日均成交量只有 200 万元, 该买单相当于样本日均成交量的 500%。在这一规模下,成交容量假设不成立,市场冲击和无法成交的风险都需要单独建模。
机构通常要求每笔委托不超过当日成交量的 5%~10%。
# ADV 流动性过滤
strategy_daily_turnover = 1000 # 假设策略每天需要交易 1000万元
# 流动性过滤:只保留 ADV 足够大的股票(让策略占比 < 10%)
min_adv = strategy_daily_turnover / 0.10 # ADV 至少 10000万元 = 1亿元
liquid_universe = universe_df[universe_df['adv_m'] >= min_adv / 10000 * 100]
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 全市场 ADV 分布
axes[0].hist(universe_df['adv_m'], bins=50, color='lightcoral', alpha=0.7, edgecolor='white')
axes[0].axvline(min_adv / 10000 * 100, color='red', linestyle='--', linewidth=2, label=f'最低 ADV 阈值')
axes[0].set_title('全市场:日均成交额分布')
axes[0].set_xlabel('日均成交额(百万元)')
axes[0].legend()
# 过滤后的股票池分布
axes[1].hist(liquid_universe['adv_m'], bins=30, color='steelblue', alpha=0.7, edgecolor='white')
axes[1].set_title(f'过滤后股票池({len(liquid_universe)} 只):日均成交额分布')
axes[1].set_xlabel('日均成交额(百万元)')
plt.suptitle('流动性过滤前后对比', fontsize=13)
plt.tight_layout()
plt.show()
print(f"全市场股票数: {len(universe_df)}")
print(f"流动性过滤后: {len(liquid_universe)} 股 ({len(liquid_universe)/len(universe_df):.1%})")
全市场股票数: 500 流动性过滤后: 311 股 (62.2%)
2. 市值过滤:聚焦大中型股¶
小市值股票(微盘股)通常:
- 流动性更差
- 信息不透明,基本面分析更难
- 受到更多算法超额关注(被"挤兑")
机构通常要求股票市值达到一定门槛(如 A 股中证 800 成份股的门槛)。
# 市值过滤:只保留市值前 60% 的股票
market_cap_threshold = universe_df['market_cap_m'].quantile(0.40) # 排除后40%小市值
large_mid_cap = liquid_universe[liquid_universe['market_cap_m'] >= market_cap_threshold]
print(f"ADV 过滤后: {len(liquid_universe)} 只")
print(f"+ 市值过滤后(最终股票池): {len(large_mid_cap)} 只")
# 行业分布检验
sector_dist = large_mid_cap['sector'].value_counts()
colors = plt.cm.Set3(range(len(sector_dist)))
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
sector_dist.plot(kind='bar', ax=ax1, color=colors, edgecolor='white')
ax1.set_title('最终股票池行业分布')
ax1.set_xlabel('行业')
ax1.set_ylabel('股票数量')
ax1.tick_params(axis='x', rotation=30)
ax2.pie(sector_dist.values, labels=sector_dist.index, autopct='%1.1f%%', colors=colors)
ax2.set_title('行业占比(饼图)')
plt.suptitle('股票池行业集中度分析', fontsize=13)
plt.tight_layout()
plt.show()
# Herfindahl-Hirschman 集中度指数(HHI,越靠近0越分散)
weights = sector_dist / sector_dist.sum()
hhi = (weights**2).sum()
print(f"\nHHI 行业集中度指数: {hhi:.4f}")
print(f"(0 = 完全分散,1 = 完全集中)")
print(f"评级: {'分散' if hhi < 0.15 else '中等' if hhi < 0.25 else '高度集中'}")
ADV 过滤后: 311 只 + 市值过滤后(最终股票池): 286 只
HHI 行业集中度指数: 0.1265 (0 = 完全分散,1 = 完全集中) 评级: 分散
练习¶
- 更改 ADV 阈值为 5% 参与率而不是 10%,观察股票池大小如何变化。
- 在阿股 A 股数据(可用 adata 库)中,实现一个完整的 Universe Selection 流程,剔除 ST 和 *ST 股票。
- 对过滤后的股票池计算'行业分散度'得分,思考如何在选股时额外限制单一行业最大持仓比例。
下一节 → 05_cointegration_correlation.ipynb