跳到主内容
研究主线市场摩擦与证券池设计 · 第27–29讲3/3模块导览 →
讲义

第29讲:证券池选择 (Universe Selection)

从行业、流动性、价格和数据可得性构建证券池,设计动态再平衡,并说明幸存者偏差和前视风险。

完成状态保存在当前浏览器,可随时取消。

一、什么是股票池?#

1.1 基本概念#

股票池(Universe)是量化策略交易的标的资产集合。就像厨师需要选择优质食材一样,量化交易者需要精心挑选股票池。常见的股票池类型包括:

  • 行业特定池(如科技股)

  • 规模导向池(如沪深300成分股)

  • 因子驱动池(如高ROE股票)

1.2 重要性说明#

(演进过程示例):

2008年金融危机期间,仅包含金融股的策略普遍失效,而全市场策略表现更好。这说明:

策略收益=策略逻辑收益+股票池选择收益策略收益 = 策略逻辑收益 + 股票池选择收益

1.3 数据获取实践#

code
import yfinance as yf
import pandas as pd

# 获取多只股票数据示例
tickers = ['AAPL', 'MSFT', 'GOOG']
start_date = '2020-01-01'
end_date = '2023-01-01'

data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']
print(data.head())

# 计算日收益率
returns = data.pct_change().dropna()
print(returns.describe())

小练习:尝试添加TSLA股票数据,比较其收益率波动性

二、构建股票池的关键要素#

2.1 行业分布分析#

使用yfinance获取行业信息(示例):

code
# 获取单只股票信息
aapl = yf.Ticker("AAPL")
print(aapl.info['sector'])  # 输出行业分类

# 行业分布可视化
sector_counts = pd.Series({
    'Technology': 150,
    'Financials': 120,
    'Healthcare': 80
})
sector_counts.plot(kind='pie', autopct='%1.1f%%')

2.2 流动性筛选#

常用流动性指标计算公式:

日流动金额=收盘价×成交量日流动金额 = 收盘价 \times 成交量

20日移动平均流动性筛选:

code
def liquidity_screen(data, window=20, threshold=1e6):
    volume = data['Volume']
    close = data['Close']
    dollar_volume = (volume * close).rolling(window).mean()
    return dollar_volume > threshold

2.3 换手率控制#

换手率计算公式:

日换手率=新入选股票数+被剔除股票数股票池总数量×100%日换手率 = \frac{新入选股票数 + 被剔除股票数}{股票池总数量} \times 100\%

对比实验

code
# 原始股票池与平滑后对比
original_turnover = [...]  # 原始换手率数据
smoothed_turnover = [...]   # 使用移动平均后的数据

pd.DataFrame({
    '原始': original_turnover,
    '平滑': smoothed_turnover
}).plot(title='换手率对比')

三、高级筛选技巧#

3.1 多条件复合筛选#

code
def build_universe(data):
    # 条件1:市值大于100亿
    market_cap = data['MarketCap'] > 10e9

    # 条件2:过去20日平均成交量大于100万股
    volume_avg = data['Volume'].rolling(20).mean() > 1e6

    # 条件3:股价高于5美元
    price_condition = data['Close'] > 5

    return market_cap & volume_avg & price_condition

3.2 动态调整机制#

使用3个月再平衡周期示例:

code
from dateutil.relativedelta import relativedelta

current_date = pd.to_datetime('2022-01-01')
rebalance_dates = [current_date + relativedelta(months=3*i) for i in range(4)]
print(rebalance_dates)

小练习:尝试修改再平衡周期为1个月,观察股票池变动频率

四、最佳实践案例:QTradableStocksUS#

4.1 筛选标准#

指标阈值作用
市值>5亿美元排除小盘股风险
日均成交额>250万美元(200日)确保流动性
股价>5美元降低交易摩擦成本

4.2 实现逻辑#

code
def QTradableStocksUS(data):
    # 市值筛选
    market_cap = data['MarketCap'] > 5e8

    # 流动性筛选
    dollar_volume = (data['Volume'] * data['Close']).rolling(200).median() > 2.5e6

    # 价格筛选
    price_filter = data['Close'] > 5

    # 其他条件(示例)
    primary_share = data['IsPrimary']  # 假设有是否为优先股字段
    no_adr = ~data['IsADR']            # 排除美国存托凭证

    return market_cap & dollar_volume & price_filter & primary_share & no_adr

五、常见误区与解决方案#

5.1 过拟合问题#

错误做法:在历史数据上不断调整参数直到表现完美

正确方法

code
# 使用时间序列交叉验证
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(data):
    train_data = data.iloc[train_idx]
    test_data = data.iloc[test_idx]
    # 执行回测...

5.2 幸存者偏差#

解决方案:使用历史时点数据进行回测,包括已退市股票

示例数据集结构

code
Date       | Ticker | Price | Exists
2020-01-02 | ABC    | 50.2  | True
2021-05-15 | ABC    | NaN   | False  # 标记退市

六、课后练习#

  1. 使用yfinance构建一个包含10只科技股的股票池

  2. 计算该股票池过去一年的行业分布变化

  3. 实现一个简单的换手率控制模块(阈值设为5%)

  4. 对比不同市值门槛(50亿 vs 100亿)对股票池数量的影响

code
# 练习模板代码
def tech_sector_universe():
    tech_tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META']
    data = yf.download(tech_tickers, period='1y')
    # 添加你的筛选逻辑...
    return selected_stocks

(注:本教程示例代码需要配合实际市场数据使用,部分筛选条件需要根据具体数据字段调整实现)

七、历史股票池审查清单#

  • 股票池必须按历史时点重建,并区分成分变更的公告日与生效日;今天从 yfinance 获取的代码列表只能用于演示,不能证明历史回测有效

  • 保留已退市、并购、破产和更名证券,并按当时可得的公司行动数据调整价格与标识符

  • 筛选流动性、市值和基本面数据时加入真实发布延迟,不能让当期尚未披露的字段进入信号

  • 在每个再平衡日检查停牌、涨跌停、借券、最小成交额和容量约束,并记录无法成交的证券

  • 分别报告“固定当前成分”与“point-in-time 成分”结果;两者差距本身就是幸存者偏差诊断