第三章:数据获取与清洗——用Python搞定可转债行情数据

做可转债统计套利,第一步不是写策略,而是搞数据。

我见过太多人,策略逻辑写得天花乱坠,结果一跑回测,数据全是坑。缺失值、异常值、复权问题……随便一个就能让你的策略“看起来很美”。

这一章,咱们就聊聊怎么用Python把可转债行情数据拿下来,再把它洗干净。

3.1 数据源的选择——别把时间浪费在爬虫上

很多人一上来就想自己写爬虫,去东方财富、集思录上抓数据。我个人建议:除非你有大把时间,否则别这么干

为什么?

  • 网站反爬机制越来越严,今天能跑,明天可能就挂了
  • 数据格式不稳定,改个字段名你就得重写解析逻辑
  • 维护成本远高于你的想象

我推荐几个靠谱的数据源:

数据源 特点 推荐指数
akshare 开源免费,覆盖A股、可转债、期货等 ⭐⭐⭐⭐⭐
tushare 老牌金融数据接口,需注册获取token ⭐⭐⭐⭐
聚宽/米筐 量化平台自带数据,适合回测 ⭐⭐⭐⭐
东方财富API 数据全,但非官方接口,稳定性一般 ⭐⭐⭐

我个人最常用的是akshare。原因很简单:不用注册,pip install 就能用,而且数据质量还不错。

小提示:如果你做的是高频策略,建议用聚宽或米筐的本地数据。akshare 的数据有延迟,做日内交易可能不够用。

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())
避坑指南:我曾经遇到过一个问题——akshare 的历史数据接口有时会返回空值。后来发现是因为节假日或停牌导致的。所以获取数据后,一定要检查一下数据量是否合理。

3.3 数据清洗——把脏数据变成干净数据

数据拿到手,别急着用。先看看有没有“脏东西”。

我总结了一套清洗流程,你可以直接套用:

  1. 检查缺失值——看看哪些字段有空值
  2. 处理异常值——比如价格突然变成0或者负数
  3. 统一格式——日期、数值类型要一致
  4. 去重——避免重复数据影响统计

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 数据获取与清洗的核心流程

下面这张图,是我自己总结的数据处理流程,你可以参考:

可转债数据获取与清洗流程 数据获取 akshare / tushare 缺失值处理 前值填充 / 删除 异常值处理 3σ / 箱线图 格式统一 日期/数值类型 去重 按代码+日期去重 输出干净数据 用于策略回测 每一步都建议做数据质量检查,不要跳过 数据清洗不是一次性工作,每次获取新数据都要重新做

3.6 实战中的几个坑

最后,分享几个我在实战中踩过的坑:

  • 复权问题:可转债有转股、回售、强赎等事件,价格会跳空。如果你做的是历史回测,一定要用后复权数据。akshare 默认返回的是不复权数据,需要自己处理。
  • 停牌数据:可转债停牌期间,价格不变,但成交量是0。如果你用成交量做因子,记得把停牌日的数据标记出来。
  • 数据延迟:akshare 的实时数据大概有1-3秒的延迟。做高频交易的朋友,建议用券商提供的API。
我的习惯:每次获取数据后,我都会先画一张价格走势图,肉眼看一下有没有明显的异常。虽然土了点,但确实能发现一些代码检查不出来的问题。

好了,数据获取和清洗就聊到这里。记住一句话:脏数据进,脏策略出。花在数据清洗上的时间,永远值得。

蓝海数据掘金营,专注资料整理