第三章:数据获取与预处理
做量化研究,尤其是可转债,有个铁律:数据质量决定模型上限。模型再牛,喂进去的是垃圾,出来的还是垃圾。我见过太多人花80%时间调参,却不愿意花20%时间把数据洗干净。嗯,这章我们就来聊聊怎么把数据搞到手,再把它收拾利索。
3.1 数据源的选择:akshare vs tushare
国内可转债数据源,我主要用两个:akshare 和 tushare。说白了,它们都是Python接口,帮你省去爬虫的麻烦。
| 特性 | akshare | tushare |
|---|---|---|
| 上手难度 | 极低,无需注册 | 需要注册+积分 |
| 数据频率 | 日频为主,分钟级有限 | 日频、分钟级、Tick级 |
| 历史长度 | 约3-5年 | 可回溯至2010年 |
| 稳定性 | 偶尔断流 | 付费后较稳定 |
我个人习惯:快速验证用akshare,正式回测用tushare。为什么?akshare免费,拿来写demo、做探索性分析特别爽。但如果你要跑三年以上的回测,tushare的历史数据更全,复权也更规范。
3.2 用akshare快速拉取转债数据
先看个最简单的例子。获取所有可转债的实时行情:
import akshare as ak
# 获取可转债实时行情
bond_zh = ak.bond_zh_cov()
print(bond_zh.head())
你会得到一个DataFrame,包含代码、名称、现价、转股价值、溢价率等字段。嗯,这里要注意:akshare返回的字段名是中文的,后续处理时记得统一成英文,不然容易乱。
获取历史日线数据呢?也很简单:
# 获取某只转债的历史日线
df = ak.bond_zh_hs_cov_daily(symbol="sz123456")
print(df.tail())
symbol参数格式是「交易所代码+转债代码」。深交所是sz,上交所是sh。我在项目中遇到过一个问题:有些老转债的代码会变,比如退市后重新上市。这时候用旧代码拉数据会报错。我的解决办法是维护一个「转债代码映射表」,定期更新。
3.3 用tushare获取更完整的数据
如果你需要更长的历史数据,或者更细的字段(比如转股价变动记录),tushare是更好的选择。
import tushare as ts
# 设置token(需要注册获取)
ts.set_token('your_token_here')
pro = ts.pro_api()
# 获取可转债列表
cb_list = pro.cb_basic()
print(cb_list.head())
tushare的字段是英文的,比如bond_id、stk_code、conv_price。我个人觉得这样反而更干净,少了很多编码问题。
获取历史行情:
# 获取某只转债的日线数据
df = pro.cb_daily(
ts_code='123456.SZ',
start_date='20200101',
end_date='20231231'
)
print(df.head())
注意:tushare的ts_code格式是「代码.交易所」,交易所用SZ、SH。这个和akshare不一样,别搞混了。
3.4 数据清洗:那些坑我替你踩过了
数据拉下来,别急着用。先看看有没有脏数据。我总结了几类常见问题:
- 缺失值:停牌日、节假日、数据源漏采
- 异常值:价格突然跳空、成交量异常放大
- 重复值:同一日期的数据出现多次
- 类型错误:本该是数字的字段变成了字符串
先处理缺失值。我的做法是:
# 检查缺失值
print(df.isnull().sum())
# 处理方式:向前填充(用上一个交易日的数据)
df.fillna(method='ffill', inplace=True)
# 如果连续缺失超过5天,直接删除
df = df[df.isnull().sum(axis=1) < 5]
为什么会用向前填充?因为可转债的转股价、溢价率等指标,在停牌期间其实没变化。用上一个交易日的数据填充,逻辑上说得通。
再处理异常值。我习惯用「3倍标准差法」:
# 剔除价格异常值
mean = df['close'].mean()
std = df['close'].std()
df = df[(df['close'] > mean - 3*std) & (df['close'] < mean + 3*std)]
嗯,这里要注意:可转债的价格波动比股票小,但极端情况下(比如强赎公告后)也会出现大幅波动。3倍标准差可能会误删有效数据。我后来改用「分位数法」,只剔除上下0.5%的极端值。
3.5 复权处理:别让分红拆细骗了你
可转债的复权和股票不太一样。股票复权主要考虑分红、送股、配股。可转债呢?主要考虑转股价调整。
当正股分红或送股时,转股价会相应下调。这时候转债的历史价格也需要调整,否则回测结果会失真。
tushare提供了复权因子:
# 获取复权因子
adj_factor = pro.cb_adj_factor(ts_code='123456.SZ')
print(adj_factor.head())
拿到复权因子后,对历史价格做调整:
# 后复权:调整历史价格
df['adj_close'] = df['close'] * df['adj_factor']
df['adj_open'] = df['open'] * df['adj_factor']
akshare没有直接提供复权因子。我的做法是:用tushare拉复权数据,用akshare做实时监控。两个库互补,各取所长。
3.6 数据预处理流程总结
我把整个流程画了张图,方便你理解:
这张图展示了从数据获取到最终可用的完整链路。你可能会问:为什么数据清洗后还要检查质量?因为有些数据源会突然抽风,比如某天返回的全是空值。加一道检查,能避免把脏数据喂给模型。
3.7 实战:构建一个可转债数据管道
最后,我分享一个我自己在用的数据管道框架。它把上面所有步骤串起来了:
class ConvertibleBondDataPipeline:
def __init__(self, source='akshare'):
self.source = source
self.data = None
def fetch_data(self, codes, start_date, end_date):
"""获取原始数据"""
if self.source == 'akshare':
# 用akshare拉数据
pass
elif self.source == 'tushare':
# 用tushare拉数据
pass
def clean_data(self):
"""清洗数据"""
# 处理缺失值
self.data.fillna(method='ffill', inplace=True)
# 处理异常值
self.data = self._remove_outliers(self.data)
# 去重
self.data.drop_duplicates(inplace=True)
def adjust_price(self):
"""复权处理"""
# 用复权因子调整价格
pass
def run(self, codes, start_date, end_date):
self.fetch_data(codes, start_date, end_date)
self.clean_data()
self.adjust_price()
return self.data
这个框架的好处是:换数据源只需要改fetch_data方法,其他逻辑不用动。我建议你也这样设计,因为数据源说换就换,但清洗和复权的逻辑是通用的。
好了,数据获取和预处理就聊到这儿。记住:数据是量化研究的基石。花时间把数据搞干净,后面的因子挖掘和模型训练才能事半功倍。