第三章:数据获取与预处理

做量化研究,尤其是可转债,有个铁律:数据质量决定模型上限。模型再牛,喂进去的是垃圾,出来的还是垃圾。我见过太多人花80%时间调参,却不愿意花20%时间把数据洗干净。嗯,这章我们就来聊聊怎么把数据搞到手,再把它收拾利索。

3.1 数据源的选择:akshare vs tushare

国内可转债数据源,我主要用两个:aksharetushare。说白了,它们都是Python接口,帮你省去爬虫的麻烦。

特性 akshare tushare
上手难度 极低,无需注册 需要注册+积分
数据频率 日频为主,分钟级有限 日频、分钟级、Tick级
历史长度 约3-5年 可回溯至2010年
稳定性 偶尔断流 付费后较稳定

我个人习惯:快速验证用akshare,正式回测用tushare。为什么?akshare免费,拿来写demo、做探索性分析特别爽。但如果你要跑三年以上的回测,tushare的历史数据更全,复权也更规范。

小技巧: 我建议你两个库都装上。万一akshare某天接口挂了,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_idstk_codeconv_price。我个人觉得这样反而更干净,少了很多编码问题。

获取历史行情:

# 获取某只转债的日线数据
df = pro.cb_daily(
    ts_code='123456.SZ',
    start_date='20200101',
    end_date='20231231'
)
print(df.head())

注意:tushare的ts_code格式是「代码.交易所」,交易所用SZ、SH。这个和akshare不一样,别搞混了。

避坑指南: 我曾经因为tushare积分不够,拉不到某些字段。后来发现,基础日线数据只需要200积分,但转股价变动、回售条款等需要更高积分。建议先评估你的需求,再决定是否充值。

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做实时监控。两个库互补,各取所长。

核心观点: 复权处理不是可选项,是必选项。不做复权,你的回测结果可能偏差10%以上。尤其是那些频繁调整转股价的转债,误差更大。

3.6 数据预处理流程总结

我把整个流程画了张图,方便你理解:

数据获取与预处理流程图 数据获取 akshare / tushare 数据清洗 缺失值 / 异常值 / 重复值 复权处理 转股价调整 / 复权因子 数据质量 是否合格? 不合格 干净数据 → 因子计算

这张图展示了从数据获取到最终可用的完整链路。你可能会问:为什么数据清洗后还要检查质量?因为有些数据源会突然抽风,比如某天返回的全是空值。加一道检查,能避免把脏数据喂给模型。

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方法,其他逻辑不用动。我建议你也这样设计,因为数据源说换就换,但清洗和复权的逻辑是通用的。

个人经验: 我刚开始做可转债研究时,数据管道写得特别糙,每次换数据源都要重写一遍。后来花了半天时间重构,用工厂模式把数据源抽象出来。嗯,这半天时间,后来帮我省了至少几十个小时。

好了,数据获取和预处理就聊到这儿。记住:数据是量化研究的基石。花时间把数据搞干净,后面的因子挖掘和模型训练才能事半功倍。


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