4. 数据获取与预处理:使用Python获取股票/ETF数据,清洗与标准化

做量化交易,数据就是你的弹药库。弹药不行,枪法再好也白搭。

我见过太多人,策略逻辑写得漂漂亮亮,回测曲线美如画,一上实盘就崩。为什么?数据源有问题,或者预处理没做好。说白了,垃圾进,垃圾出。

这一章,我们就来聊聊怎么用Python把数据搞到手,再把它收拾得服服帖帖。

4.1 数据源的选择:别在源头就输了

我个人习惯,数据源至少要备两个。一个主用,一个备用。为什么?因为任何数据服务商都可能抽风。我曾经在周五晚上跑回测,结果某数据源直接挂了,整个周末的计划全泡汤。

目前国内常用的数据源,我列个表给你看:

数据源 类型 优点 缺点
Tushare 免费/付费 数据全,社区活跃 免费版有积分限制
AkShare 免费 开源,接口丰富 依赖网络爬虫,稳定性一般
Baostock 免费 无需注册,上手快 数据更新有延迟
JoinQuant / RiceQuant 平台内置 本地化好,回测方便 数据导出有限制

对于双低策略,我们主要需要两类数据:可转债数据正股/ETF数据。可转债数据包括价格、转股价值、溢价率等;正股数据就是常规的日线行情。

我的建议:刚开始学,用AkShare或Baostock就够了。免费,不用折腾。等你要做高频或者更精细的策略,再考虑付费数据源。

4.2 用Python获取数据:代码走起

我们以AkShare为例,演示怎么获取可转债和ETF的数据。AkShare的接口设计得挺人性化,你想想看,一行代码就能拉数据。

先安装库:

pip install akshare pandas numpy

获取可转债实时行情:

import akshare as ak
import pandas as pd

# 获取所有可转债的实时数据
bond_zh_cov_df = ak.bond_zh_cov()
print(bond_zh_cov_df.head())

获取ETF日线数据(以沪深300ETF为例):

# 获取沪深300ETF的历史日线数据
# 代码:510300.SH
etf_df = ak.fund_etf_hist_em(symbol="510300", period="daily", 
                              start_date="20230101", end_date="20231231", 
                              adjust="qfq")
print(etf_df.head())
注意:adjust参数控制复权方式。"qfq"是前复权,"hfq"是后复权。做双低策略,我建议用前复权,这样价格走势更连贯。

获取可转债的转股价值、溢价率等关键字段:

# 获取可转债的详细数据,包含转股价值、溢价率等
bond_cov_df = ak.bond_cov_all()
# 筛选我们需要的字段
bond_cov_df = bond_cov_df[['债券代码', '债券名称', '现价', '转股价', 
                            '正股价', '转股价值', '溢价率']]
print(bond_cov_df.head())

4.3 数据清洗:把脏活累活干漂亮

数据拿到手,别急着用。先看看有没有坑。

我遇到过最离谱的一次,某只可转债的转股价居然是0。你想想看,转股价为0意味着什么?意味着可以零成本转股,这明显是数据错误。如果不处理,你的双低评分直接崩掉。

常见的清洗步骤:

  1. 处理缺失值:用前向填充或删除
  2. 处理异常值:价格不能为负,溢价率不能太离谱
  3. 去重:同一只债券在同一天出现多次
  4. 数据类型转换:把字符串转成数值

代码示例:

# 1. 检查缺失值
print(bond_cov_df.isnull().sum())

# 2. 删除转股价为0或缺失的行
bond_cov_df = bond_cov_df[bond_cov_df['转股价'] > 0]

# 3. 删除现价异常的行(比如现价小于0.1元)
bond_cov_df = bond_cov_df[bond_cov_df['现价'] > 0.1]

# 4. 重置索引
bond_cov_df = bond_cov_df.reset_index(drop=True)

# 5. 确保数值列是float类型
bond_cov_df['现价'] = pd.to_numeric(bond_cov_df['现价'], errors='coerce')
bond_cov_df['转股价'] = pd.to_numeric(bond_cov_df['转股价'], errors='coerce')
避坑指南:我曾经因为没做数据类型转换,直接用字符串做数学运算,结果回测出来的收益率高得离谱。后来一查,原来是字符串拼接导致的bug。所以,一定要用pd.to_numeric()做类型转换

4.4 数据标准化:让不同品种站在同一起跑线

双低策略的核心是「双低」——低价格、低溢价率。但这两个指标的数值范围差很多。价格可能是80到200,溢价率可能是-5%到50%。直接相加,价格会主导评分。

所以,我们需要做标准化。

常用的标准化方法:

  • Min-Max标准化:缩放到[0,1]区间
  • Z-score标准化:均值为0,标准差为1
  • 排名标准化:用百分位排名代替原始值

我个人偏好排名标准化。为什么?因为它对异常值不敏感。你想想看,如果某只可转债价格突然飙到500,Min-Max会把其他所有债券的价格都压到接近0,这显然不合理。

代码实现:

# 排名标准化函数
def rank_normalize(series):
    return series.rank(pct=True)

# 对现价和溢价率做排名标准化
bond_cov_df['价格排名'] = rank_normalize(bond_cov_df['现价'])
bond_cov_df['溢价率排名'] = rank_normalize(bond_cov_df['溢价率'])

# 计算双低得分(等权)
bond_cov_df['双低得分'] = bond_cov_df['价格排名'] + bond_cov_df['溢价率排名']

# 按得分排序,得分越低越好
bond_cov_df = bond_cov_df.sort_values('双低得分')
print(bond_cov_df[['债券代码', '债券名称', '现价', '溢价率', '双低得分']].head(10))
一个小技巧:如果你觉得价格和溢价率的重要性不同,可以调整权重。比如,价格权重0.6,溢价率权重0.4。公式就是:双低得分 = 0.6 * 价格排名 + 0.4 * 溢价率排名。这个权重怎么调?嗯,我一般会做参数扫描,找历史回测最优值。

4.5 数据合并与对齐:别让时间错位

双低策略需要同时用到可转债数据和正股数据。这两类数据的更新频率可能不一样。可转债数据可能每天更新一次,正股数据也是每天更新。但万一哪天停牌了呢?或者数据源漏了一天?

所以,合并数据时一定要做时间对齐。

# 假设我们有可转债日线数据和正股日线数据
# 以日期为索引,做inner join
merged_df = pd.merge(bond_daily_df, stock_daily_df, 
                     on='date', how='inner', suffixes=('_bond', '_stock'))

# 检查合并后的数据是否有缺失
print(merged_df.isnull().sum())

# 如果某一天正股数据缺失,可以考虑用前一天的收盘价填充
merged_df = merged_df.fillna(method='ffill')
注意:用ffill填充要小心。如果连续停牌好几天,用前几天的价格可能失真。我一般会设置一个最大填充天数,比如3天。超过3天就删除那天的数据。

4.6 本章小结:数据准备的核心逻辑

数据获取与预处理,说白了就是三个字:准、全、稳

  • :数据来源可靠,清洗到位
  • :字段齐全,时间跨度够长
  • :流程自动化,可重复执行

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

双低策略数据预处理流程图 数据获取 Tushare/AkShare 数据清洗 去空/去重/去异常 数据标准化 排名/Min-Max/Z-score 数据合并 时间对齐/字段拼接 特征计算 双低得分/排名 输出数据 DataFrame/CSV 每日/每周自动运行 虚线表示自动化流程,可定时执行

嗯,数据准备这块,说难不难,说简单也不简单。关键是要养成好习惯——每次拿到数据,先做一遍完整的清洗和标准化流程。别偷懒,偷懒的代价就是回测结果不可信。

记住一句话:数据质量决定了策略的上限。模型再牛,数据不行,一切都是白搭。


公众号:蓝海资料掘金营,微信deep3321