第三章:数据获取与清洗——数据源选择、清洗流程、处理缺失值与异常值、数据对齐与重采样
做轮动模型,最怕什么?
不是策略亏钱,而是数据出问题。
我见过太多人,策略逻辑写得漂漂亮亮,回测曲线也好看,结果一上实盘就崩。查到最后,发现是数据源有坑——前复权算错了、停牌日没处理、不同股票的数据长度不一样。嗯,这些坑我都踩过。
所以这一章,咱们把数据这块地基打牢。说白了,数据质量决定了你的回测可信度。数据脏,结果就是垃圾进垃圾出。
3.1 数据源选择:Tushare、AKShare、Yahoo Finance
我个人习惯,根据场景选数据源。做A股轮动,首选Tushare和AKShare。做全球资产配置,Yahoo Finance更方便。下面我逐个说说。
3.1.1 Tushare
国内老牌金融数据接口,数据质量高,覆盖全面。但需要注册获取token,部分接口有积分限制。
优点:数据准确、字段丰富、有复权因子
缺点:需要注册、有调用频率限制、高级接口收费
import tushare as ts
# 设置token(需提前注册获取)
ts.set_token('your_token_here')
pro = ts.pro_api()
# 获取日线数据
df = pro.daily(ts_code='000001.SZ', start_date='20200101', end_date='20231231')
print(df.head())
3.1.2 AKShare
开源免费,接口丰富,更新快。我最近两年用得比较多。它直接爬取公开网站数据,所以不需要注册。
我的经验:AKShare的接口名经常变,建议固定版本号。我曾经因为升级版本导致回测脚本全崩,排查了半天。
import akshare as ak
# 获取股票日线数据
df = ak.stock_zh_a_hist(symbol="000001", period="daily",
start_date="20200101", end_date="20231231", adjust="qfq")
print(df.head())
3.1.3 Yahoo Finance
做全球ETF轮动、美股轮动,Yahoo Finance是首选。用yfinance库就能直接拉数据,非常方便。
import yfinance as yf
# 获取SPY和QQQ数据
tickers = ['SPY', 'QQQ']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')
print(data['Adj Close'].head())
注意:Yahoo Finance在国内访问不稳定,建议搭配代理使用。另外,它返回的数据是未复权的,需要自己处理复权。
3.2 数据清洗流程
数据拿到手,别急着用。先走一遍清洗流程。我总结了一个四步法:
- 格式统一——日期格式、列名、数据类型
- 缺失值处理——填充或删除
- 异常值检测——涨跌幅异常、停牌日处理
- 数据对齐——多标的统一时间轴
你想想看,如果不同数据源的日期格式不一样,一个用'2020-01-01',一个用'20200101',合并的时候直接报错。这种低级错误我犯过不止一次。
# 标准清洗流程示例
import pandas as pd
def clean_data(df):
# 1. 统一日期格式
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
# 2. 设置索引
df = df.set_index('date')
# 3. 只保留需要的列
df = df[['open', 'high', 'low', 'close', 'volume']]
# 4. 处理缺失值
df = df.fillna(method='ffill') # 前向填充
# 5. 处理异常值(涨跌幅超过20%视为异常)
df['return'] = df['close'].pct_change()
df.loc[df['return'].abs() > 0.2, 'close'] = None
df['close'] = df['close'].fillna(method='ffill')
return df
3.3 处理缺失值与异常值
缺失值怎么处理?说白了就三种思路:
- 删除——缺失比例小,直接dropna()
- 填充——用前值、后值、均值填充
- 插值——线性插值、时间插值
我个人习惯,金融时间序列用前向填充(ffill)最多。为什么?因为停牌日的数据,用前一天的价格填充最合理。你想想看,股票停牌了,价格不变,用ffill正好。
避坑指南:我曾经在回测中用了均值填充,结果把停牌日的数据填成了均价,导致回测收益虚高。后来发现,停牌日应该用前一天的收盘价,而不是均值。
# 缺失值处理实战
# 场景:某股票停牌5天,数据缺失
df = pd.DataFrame({
'date': pd.date_range('2023-01-01', periods=10, freq='D'),
'close': [10, 10.5, None, None, None, None, None, 11, 11.2, 11.5]
})
# 正确做法:前向填充
df['close_ffill'] = df['close'].fillna(method='ffill')
print(df)
异常值处理,我常用两种方法:
- 涨跌幅阈值法——超过±20%视为异常(A股涨跌停限制)
- Z-score法——超过3倍标准差视为异常
# 异常值检测
def detect_outliers(df, column='close', method='zscore', threshold=3):
if method == 'zscore':
z_scores = (df[column] - df[column].mean()) / df[column].std()
return df[z_scores.abs() > threshold]
elif method == 'pct_change':
returns = df[column].pct_change()
return df[returns.abs() > 0.2]
3.4 数据对齐与重采样
做轮动模型,最核心的一步就是数据对齐。不同股票的交易日期不一样,有的节假日停牌,有的临时停牌。如果不做对齐,回测结果就是错的。
我的经验:我习惯用沪深300的交易日期作为基准,把所有股票对齐到同一时间轴。这样能保证每个交易日都有数据。
# 多标的数据对齐
import pandas as pd
# 假设有三只股票的数据
stock_a = pd.DataFrame({'date': pd.date_range('2023-01-01', periods=5, freq='D'),
'close': [10, 10.5, 11, 11.2, 11.8]})
stock_b = pd.DataFrame({'date': pd.date_range('2023-01-01', periods=5, freq='D'),
'close': [20, 20.3, None, 20.8, 21]})
stock_c = pd.DataFrame({'date': pd.date_range('2023-01-03', periods=3, freq='D'),
'close': [30, 30.5, 31]})
# 合并并对齐
all_data = pd.merge(stock_a, stock_b, on='date', how='outer', suffixes=('_a', '_b'))
all_data = pd.merge(all_data, stock_c, on='date', how='outer')
all_data = all_data.set_index('date').sort_index()
# 前向填充缺失值
all_data = all_data.fillna(method='ffill')
print(all_data)
重采样也很重要。做周频轮动,需要把日线数据重采样成周线。做月频轮动,就重采样成月线。
# 日线重采样为周线
df_weekly = df.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
3.5 本章知识体系
下面这张图,是我做数据清洗时的完整流程。你可以保存下来,每次做数据预处理时对照着走一遍。
嗯,这一章的内容就到这里。数据清洗看起来琐碎,但它是整个轮动模型的地基。地基不稳,上面盖的房子再漂亮也没用。下一章,咱们开始讲技术指标的计算,到时候这些干净数据就能派上用场了。
公众号:蓝海资料掘金营,微信deep3321