第三章:数据获取与清洗——用Python搞定可转债行情数据
做可转债统计套利,第一步不是写策略,而是搞数据。
我见过太多人,策略逻辑写得天花乱坠,结果一跑回测,数据全是坑。缺失值、异常值、复权问题……随便一个就能让你的策略“看起来很美”。
这一章,咱们就聊聊怎么用Python把可转债行情数据拿下来,再把它洗干净。
3.1 数据源的选择——别把时间浪费在爬虫上
很多人一上来就想自己写爬虫,去东方财富、集思录上抓数据。我个人建议:除非你有大把时间,否则别这么干。
为什么?
- 网站反爬机制越来越严,今天能跑,明天可能就挂了
- 数据格式不稳定,改个字段名你就得重写解析逻辑
- 维护成本远高于你的想象
我推荐几个靠谱的数据源:
| 数据源 | 特点 | 推荐指数 |
|---|---|---|
| akshare | 开源免费,覆盖A股、可转债、期货等 | ⭐⭐⭐⭐⭐ |
| tushare | 老牌金融数据接口,需注册获取token | ⭐⭐⭐⭐ |
| 聚宽/米筐 | 量化平台自带数据,适合回测 | ⭐⭐⭐⭐ |
| 东方财富API | 数据全,但非官方接口,稳定性一般 | ⭐⭐⭐ |
我个人最常用的是akshare。原因很简单:不用注册,pip install 就能用,而且数据质量还不错。
3.2 用akshare获取可转债行情
先安装:
pip install akshare
然后获取全市场可转债的实时行情:
import akshare as ak
# 获取所有可转债的实时行情
bond_zh = ak.bond_zh_hs_cov()
# 看看前几行
print(bond_zh.head())
跑完之后,你会得到一个DataFrame,里面包含代码、名称、现价、涨跌幅、转股价值、溢价率等字段。
嗯,这里要注意:akshare 返回的字段名可能会随着版本更新而变化。我建议每次用之前先打印一下列名,确认一下。
print(bond_zh.columns.tolist())
如果你需要历史日线数据,可以这样:
# 获取某只可转债的历史日线数据
# 参数:symbol="sh123456" 或 "sz123456"
df_history = ak.bond_zh_hs_cov_daily(symbol="sh123456")
print(df_history.tail())
3.3 数据清洗——把脏数据变成干净数据
数据拿到手,别急着用。先看看有没有“脏东西”。
我总结了一套清洗流程,你可以直接套用:
- 检查缺失值——看看哪些字段有空值
- 处理异常值——比如价格突然变成0或者负数
- 统一格式——日期、数值类型要一致
- 去重——避免重复数据影响统计
3.3.1 缺失值处理
先看看缺失情况:
# 检查缺失值
print(df.isnull().sum())
常见的处理方式有三种:
- 删除——如果缺失比例很小,直接删掉
- 填充——用前值、均值或中位数填充
- 插值——用前后值做线性插值
我个人习惯是:对于价格类数据,用前值填充。因为可转债停牌时,价格不变,用前值最合理。
# 用前值填充缺失
df.fillna(method='ffill', inplace=True)
3.3.2 异常值处理
异常值怎么找?最简单的方法是用3σ原则或箱线图。
举个例子,检查价格异常:
# 计算价格的均值和标准差
mean_price = df['现价'].mean()
std_price = df['现价'].std()
# 找出超出3σ范围的数据
outliers = df[(df['现价'] < mean_price - 3 * std_price) |
(df['现价'] > mean_price + 3 * std_price)]
print(f"发现 {len(outliers)} 个异常值")
但说实话,3σ原则在可转债上不一定好用。为什么?因为可转债的价格波动范围很大,有的债价格只有80块,有的能到200块。用全局的均值和标准差,容易把正常数据误判为异常。
我建议的做法是:按价格区间分组,分别做异常检测。
# 按价格区间分组
df['价格区间'] = pd.cut(df['现价'], bins=[0, 100, 130, 200, 500])
# 分组检测异常
def detect_outliers(group):
q1 = group['现价'].quantile(0.25)
q3 = group['现价'].quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
return group[(group['现价'] < lower) | (group['现价'] > upper)]
outliers_by_group = df.groupby('价格区间').apply(detect_outliers)
3.4 数据清洗的完整流程
把上面的步骤串起来,就是一个完整的清洗流程。我把它封装成一个函数:
def clean_bond_data(df):
"""
清洗可转债行情数据
"""
# 1. 复制数据,避免修改原数据
df_clean = df.copy()
# 2. 删除全空的行
df_clean.dropna(how='all', inplace=True)
# 3. 填充缺失值(价格类用前值)
price_cols = ['现价', '转股价值', '溢价率']
for col in price_cols:
if col in df_clean.columns:
df_clean[col].fillna(method='ffill', inplace=True)
# 4. 处理异常值(价格不能为0或负数)
df_clean = df_clean[df_clean['现价'] > 0]
# 5. 去重
df_clean.drop_duplicates(subset=['代码', '日期'], inplace=True)
# 6. 重置索引
df_clean.reset_index(drop=True, inplace=True)
return df_clean
# 使用
df_clean = clean_bond_data(df)
这个函数我用了两年多,基本没出过问题。你可以根据自己的需求调整。
3.5 数据获取与清洗的核心流程
下面这张图,是我自己总结的数据处理流程,你可以参考:
3.6 实战中的几个坑
最后,分享几个我在实战中踩过的坑:
- 复权问题:可转债有转股、回售、强赎等事件,价格会跳空。如果你做的是历史回测,一定要用后复权数据。akshare 默认返回的是不复权数据,需要自己处理。
- 停牌数据:可转债停牌期间,价格不变,但成交量是0。如果你用成交量做因子,记得把停牌日的数据标记出来。
- 数据延迟:akshare 的实时数据大概有1-3秒的延迟。做高频交易的朋友,建议用券商提供的API。
好了,数据获取和清洗就聊到这里。记住一句话:脏数据进,脏策略出。花在数据清洗上的时间,永远值得。