数据获取:沪深300成分股的历史数据清洗与对齐
做量化投资,数据就是你的弹药库。弹药不行,枪法再好也白搭。
我个人习惯,在开始任何策略研究之前,先花70%的时间把数据搞定。为什么?因为我在项目中遇到过太多次「策略回测漂亮,实盘一塌糊涂」的情况,最后查下来,十有八九是数据出了问题。
这一章,我们就来搞定沪深300成分股的历史数据。包括价格、成交量,还有财务数据。嗯,这里要注意,财务数据是很多新手容易忽略的「暗坑」。
数据获取的三大来源
先说说数据从哪来。我常用的有三个渠道:
- Tushare Pro:国内比较成熟的金融数据接口,数据全,但需要积分。我个人觉得,对于个人研究来说,性价比不错。
- AKShare:开源免费,接口丰富。缺点是偶尔会因网站改版而失效,需要及时更新。
- Wind / 聚宽:机构级数据,准确度高,但收费不菲。实盘阶段可以考虑。
这一章,我们用Tushare Pro来演示。你想想看,如果连数据都拿不到手,后面的策略分析就是空中楼阁。
核心思路:先拿成分股列表,再逐只股票拉取日线行情,最后对齐财务数据。三步走,缺一不可。
第一步:获取沪深300成分股列表
沪深300的成分股不是一成不变的。每年6月和12月,中证指数公司会调整一次。所以,我们拿到的列表,一定要带上时间戳。
我曾经犯过一个错误:用2020年的成分股列表,去回测2018年的策略。结果可想而知,很多股票当时根本不在指数里。
import tushare as ts
import pandas as pd
# 初始化API
pro = ts.pro_api('你的token')
# 获取沪深300成分股
df_member = pro.index_weight(index_code='000300.SH', start_date='20230101', end_date='20231231')
print(df_member.head())
这段代码会返回一个DataFrame,包含成分股代码、纳入日期和权重。注意,index_code要写对,沪深300是000300.SH。
第二步:拉取日线行情数据
有了成分股列表,接下来就是拉取每只股票的历史日线数据。包括:开盘价、收盘价、最高价、最低价、成交量、成交额。
这里有个小技巧:不要一次性拉取所有股票的全部历史数据。Tushare有频率限制,你一下子拉太多,会被封IP。我建议分批拉取,每批50只,中间sleep一下。
import time
from tqdm import tqdm
stock_list = df_member['con_code'].unique().tolist()
all_data = []
for stock in tqdm(stock_list[:50]): # 先试50只
try:
df = pro.daily(ts_code=stock, start_date='20180101', end_date='20231231')
all_data.append(df)
time.sleep(0.5) # 礼貌一点,别把服务器搞崩了
except Exception as e:
print(f'{stock} 获取失败: {e}')
df_price = pd.concat(all_data, ignore_index=True)
print(f'共获取 {len(df_price)} 条记录')
避坑指南:我曾经因为没加try-except,结果某只股票停牌导致整个循环中断,白白跑了两个小时的数据全废了。所以,异常处理一定要加。
第三步:财务数据的对齐难题
财务数据比价格数据麻烦得多。为什么?因为财报的发布时间和报告期不一致。
比如,2023年的一季报,最晚要在4月30日前发布。但你在4月15日看到的「最新」财报,其实是2022年年报。如果你直接用最新发布的财报数据去计算估值指标,就会产生「未来函数」——用未来的数据预测过去,回测结果当然漂亮,实盘就惨了。
正确的做法是:对齐「报告期」和「实际可用日期」。
# 获取财务数据,并标记实际可用日期
def get_financial_data(ts_code, start_date):
df_fin = pro.fina_indicator(ts_code=ts_code, start_date=start_date)
# 假设季报在报告期结束后45天可用,年报在120天后可用
df_fin['available_date'] = df_fin['end_date'].apply(
lambda x: (pd.to_datetime(x) + pd.DateOffset(days=45)) if x.month in [3,6,9]
else (pd.to_datetime(x) + pd.DateOffset(days=120))
)
return df_fin
嗯,这里要注意,不同交易所对财报披露时限有细微差别,但45天和120天是业界常用的近似值。我个人习惯再保守一点,多留5天缓冲。
第四步:数据清洗——脏活累活
数据拿到手,别急着用。先洗一洗。我总结了三步清洗法:
- 去重:同一只股票同一天出现两条记录,保留后一条(可能是修正数据)。
- 填充缺失值:成交量缺失,用前值填充;财务数据缺失,用行业均值填充。
- 异常值处理:涨跌幅超过±20%的,检查是否复权问题;市盈率为负的,单独标记。
# 去重
df_price = df_price.drop_duplicates(subset=['ts_code', 'trade_date'], keep='last')
# 填充成交量缺失
df_price['vol'] = df_price.groupby('ts_code')['vol'].fillna(method='ffill')
# 标记异常涨跌幅
df_price['pct_chg'] = df_price['pct_chg'].clip(-20, 20) # 粗暴但有效
个人经验:对于财务数据的异常值,我建议不要直接删除。比如某公司市盈率突然变成1000倍,可能是因为利润极低。这时候,用「中位数填充」比「均值填充」更稳健。
第五步:数据对齐——把拼图拼起来
价格数据是日频的,财务数据是季频的。怎么对齐?
说白了,就是把财务数据「向下填充」到每一天。比如,2023年一季报在4月30日可用,那么从5月1日到8月30日,每天的财务数据都用这一份。
# 对齐:将财务数据按可用日期合并到日线数据
df_price['trade_date'] = pd.to_datetime(df_price['trade_date'])
df_fin['available_date'] = pd.to_datetime(df_fin['available_date'])
# 使用merge_asof进行时间点对齐
df_merged = pd.merge_asof(
df_price.sort_values('trade_date'),
df_fin.sort_values('available_date'),
left_on='trade_date',
right_on='available_date',
by='ts_code',
direction='backward' # 取最近可用的财务数据
)
这里用merge_asof而不是普通的merge,就是因为财务数据不是每天都有。这个函数会自动找到「最接近且早于当前交易日」的财务数据。
知识体系总览
为了让你更直观地理解整个流程,我画了一张图:
几个容易踩的坑
- 复权问题:前复权还是后复权?我个人习惯用后复权做回测,因为前复权会导致历史价格变成负数(极端情况)。
- 停牌处理:停牌期间的数据是缺失的。不要用前值填充,而是直接跳过。策略交易时也不会在停牌日交易。
- 财报发布日期:不同公司发布时间不同。我建议统一用「报告期结束后第45天」作为可用日期,虽然不精确,但至少避免了未来函数。
总结一下:数据获取不是简单的「调个接口就完事」。你需要考虑成分股变化、财报发布时间差、复权方式、异常值处理。每一步都马虎不得。
数据准备好了,下一件事就是构建因子。但那是后面章节的内容了。先把数据搞干净,这是所有策略的基石。