4. 数据准备:获取股票行情数据(日频收益率)、获取因子数据(以动量因子为例)、数据对齐与清洗

数据是量化分析的基石。在构建 Rank IC 监控系统之前,必须完成三部分核心工作:获取股票日频行情数据、计算因子数据(以动量因子为例)、数据对齐与清洗。本章将使用 Python 的 pandas、numpy 和 akshare(或 tushare)库,演示完整的数据准备流程。

数据源说明: 本课程使用 akshare 获取 A 股日频行情数据,该库免费且无需 Token。若使用 tushare pro,需自行申请 Token 并替换接口。

4.1 获取股票日频收益率数据

日频收益率是计算 Rank IC 的基础。我们以沪深 300 成分股为例,获取最近 1 年的日频行情数据,并计算对数收益率。

import akshare as ak
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# 1. 获取沪深300成分股列表
stock_300 = ak.index_stock_cons_csindex("000300")
stock_list = stock_300['成分券代码'].tolist()
print(f"沪深300成分股数量: {len(stock_list)}")

# 2. 设置时间范围
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')

# 3. 批量获取日频行情(示例取前5只,实际全量需循环)
def fetch_daily_data(stock_code, start, end):
    try:
        df = ak.stock_zh_a_hist(symbol=stock_code, period="daily",
                                start_date=start, end_date=end, adjust="qfq")
        df['stock_code'] = stock_code
        df['trade_date'] = pd.to_datetime(df['日期'])
        df['close'] = df['收盘'].astype(float)
        df['ret'] = np.log(df['close'] / df['close'].shift(1))
        return df[['trade_date', 'stock_code', 'close', 'ret']]
    except:
        return pd.DataFrame()

# 实际使用时建议用多线程或分批获取
sample_stocks = stock_list[:5]
all_data = []
for code in sample_stocks:
    data = fetch_daily_data(code, start_date, end_date)
    if not data.empty:
        all_data.append(data)

price_df = pd.concat(all_data, ignore_index=True)
price_df = price_df.dropna(subset=['ret'])
print(price_df.head())
关键字段说明: trade_date(交易日期)、stock_code(股票代码)、close(复权收盘价)、ret(日频对数收益率)。

4.2 计算动量因子(Momentum Factor)

动量因子是最经典的截面因子之一。我们定义 过去 20 个交易日的累计收益率 作为动量信号,即 MOM_20 = (close_t / close_{t-20}) - 1。

# 计算动量因子:过去20日累计收益率
def calc_momentum(df, window=20):
    df = df.sort_values(['stock_code', 'trade_date']).copy()
    df['mom_20'] = df.groupby('stock_code')['close'].transform(
        lambda x: x / x.shift(window) - 1
    )
    return df

price_df = calc_momentum(price_df, window=20)
factor_df = price_df[['trade_date', 'stock_code', 'mom_20']].dropna()
print(factor_df.head())
字段名称 含义 计算方式
trade_date 交易日 原始日期字段
stock_code 股票代码 6 位数字代码
mom_20 20 日动量因子 (当日收盘价 / 20 日前收盘价) - 1
动量因子陷阱: 计算时需注意 shift(window) 会导致前 window 行数据为 NaN,必须使用 dropna() 剔除。此外,停牌股票会导致收盘价连续多日不变,影响动量计算准确性,建议在清洗步骤中处理。

4.3 数据对齐与清洗

数据对齐是确保因子值和未来收益率在时间上严格匹配的关键步骤。我们需要将因子数据与下一交易日的收益率对齐,并清洗异常值。

4.3.1 时间对齐:因子 vs 未来收益率

Rank IC 衡量的是 t 日因子值 与 t+1 日收益率 的截面秩相关性。因此,必须将因子数据向前对齐到未来收益率。

# 对齐:将因子数据与未来1日收益率合并
def align_factor_with_future_ret(price_df, factor_df):
    # 先获取未来1日收益率
    ret_df = price_df[['trade_date', 'stock_code', 'ret']].copy()
    ret_df['future_date'] = ret_df['trade_date']  # 保留原日期用于对齐

    # 将收益率向前移动1天(即 t 日的因子对应 t+1 日的收益率)
    ret_df['trade_date'] = ret_df.groupby('stock_code')['trade_date'].shift(-1)

    # 合并因子与未来收益率
    merged = pd.merge(factor_df, ret_df, on=['trade_date', 'stock_code'], how='inner')
    merged = merged.dropna(subset=['mom_20', 'ret'])
    return merged

aligned_df = align_factor_with_future_ret(price_df, factor_df)
print(f"对齐后数据量: {len(aligned_df)} 行")
print(aligned_df.head())

4.3.2 清洗规则

清洗是保证因子质量的重要环节。我们执行以下标准清洗流程:

  • 缺失值处理: 删除因子或收益率为 NaN 的行。
  • 极端值处理: 对因子值进行 MAD(中位数绝对偏差) 去极值,剔除超过 5 倍 MAD 的样本。
  • 停牌股处理: 剔除当日收益率为 0 且前一日收盘价未变的股票(连续停牌)。
  • ST 股处理: 剔除股票代码中包含 ST 或 *ST 的股票。
def clean_data(df):
    # 1. 删除缺失值
    df = df.dropna(subset=['mom_20', 'ret']).copy()

    # 2. MAD去极值(按日期分组)
    def mad_clip(group):
        median = group['mom_20'].median()
        mad = (group['mom_20'] - median).abs().median()
        upper = median + 5 * mad
        lower = median - 5 * mad
        group['mom_20'] = group['mom_20'].clip(lower, upper)
        return group

    df = df.groupby('trade_date', group_keys=False).apply(mad_clip)

    # 3. 剔除停牌股(当日ret为0且close未变)
    df = df[~((df['ret'] == 0) & (df['close'] == df['close'].shift(1)))]

    # 4. 剔除ST股(假设stock_code为纯数字,ST股代码含字母)
    df = df[df['stock_code'].str.match(r'^\d{6}$')]

    return df.reset_index(drop=True)

cleaned_df = clean_data(aligned_df)
print(f"清洗后数据量: {len(cleaned_df)} 行")
清洗效果: 经过上述清洗,数据量通常减少 5%~15%,但因子与收益率的截面相关性(Rank IC)的稳定性会显著提升。建议在清洗前后分别计算一次 Rank IC 均值,对比效果。

4.4 数据准备流程总览

下图展示了从原始行情到可用于 Rank IC 计算的完整数据流水线:

数据准备流程总览 数据准备流程总览 获取日频行情 计算对数收益率 计算动量因子 因子与收益率对齐 数据清洗 清洗后因子数据 清洗规则: • 删除缺失值 • MAD 5倍去极值 • 剔除停牌股、ST股 对齐方式: t 日因子值 → t+1 日收益率 使用 shift(-1) 实现

4.5 数据质量检查清单

在进入 Rank IC 计算之前,建议执行以下检查,确保数据质量:

检查项 预期结果 检查方法
缺失值比例 < 5% df.isnull().sum() / len(df)
因子分布 近似正态,无极端离群值 绘制直方图或计算偏度/峰度
时间跨度 覆盖完整回测区间 df['trade_date'].min() / max()
截面样本数 每日至少 30 只股票 df.groupby('trade_date').size().min()
因子与收益率对齐 无未来函数 检查 t 日因子是否对应 t+1 日收益率
最佳实践: 将清洗后的数据保存为 Parquet 格式,以便后续章节快速加载。使用 cleaned_df.to_parquet('factor_data_cleaned.parquet') 即可。Parquet 格式比 CSV 快 5~10 倍,且自动保留数据类型。

至此,我们已经完成了从原始行情到清洗后因子数据的全部准备工作。下一阶段将基于这些数据,正式计算 Rank IC 序列并评估其稳定性。