4、数据源处理:本地CSV数据加载、在线数据获取(Yahoo Finance等)、数据格式要求、数据清洗与预处理
做量化回测,第一步就是搞定数据。
数据搞不定,后面模型再漂亮也是白搭。我见过太多人花80%的时间调参,却不愿意花20%的时间把数据洗干净。结果呢?回测曲线漂亮得像假的一样,实盘一跑就露馅。
今天咱们就把数据源这块彻底讲透。
4.1 数据格式要求——Backtrader的“脾气”你得摸清
Backtrader对数据格式有自己的一套规矩。说白了,它认的是“六件套”:
| 字段 | 字段名(默认) | 说明 | 必须? |
|---|---|---|---|
| 日期时间 | datetime | 时间索引,格式为YYYY-MM-DD或时间戳 | 是 |
| 开盘价 | open | 当日第一笔成交价 | 是 |
| 最高价 | high | 当日最高成交价 | 是 |
| 最低价 | low | 当日最低成交价 | 是 |
| 收盘价 | close | 当日最后一笔成交价 | 是 |
| 成交量 | volume | 当日成交数量 | 是 |
| 持仓量 | openinterest | 期货等衍生品用,股票可填0 | 否 |
嗯,这里要注意:列名必须小写。我之前有个同事,把列名写成“Open”、“Close”,结果数据死活加载不进去,查了半天才发现是大小写的问题。
我曾经在项目中遇到过一个坑——CSV文件里日期格式是"2024/1/5",Backtrader默认不认这种格式。你得显式指定:dtformat='%Y/%m/%d'。否则它会报错,而且报错信息特别隐晦。
4.2 本地CSV数据加载——最稳的方式
我个人习惯先用CSV做本地测试。为啥?因为快,而且不受网络影响。
Backtrader加载CSV,核心就两步:
- 创建数据源对象
- 把数据源塞进Cerebro引擎
看代码:
import backtrader as bt
# 第一步:创建Cerebro引擎
cerebro = bt.Cerebro()
# 第二步:加载CSV数据
data = bt.feeds.GenericCSVData(
dataname='./data/000300.csv', # 文件路径
dtformat='%Y-%m-%d', # 日期格式
timeframe=bt.TimeFrame.Days, # 时间周期
compression=1, # 压缩倍数,1表示不压缩
# 列映射——告诉Backtrader你的CSV里哪列对应什么
datetime=0, # 第0列是日期
open=1, # 第1列是开盘价
high=2, # 第2列是最高价
low=3, # 第3列是最低价
close=4, # 第4列是收盘价
volume=5, # 第5列是成交量
openinterest=-1 # -1表示没有持仓量列
)
# 第三步:添加数据
cerebro.adddata(data)
如果你的CSV文件第一行是列名,记得加参数header=True。如果没列名,就设header=False,然后手动指定列索引。
你想想看,如果每次都要手动写这些映射,是不是很烦?我一般会封装一个函数:
def load_csv_data(filepath, fromdate=None, todate=None):
"""加载CSV数据,返回数据源对象"""
data = bt.feeds.GenericCSVData(
dataname=filepath,
fromdate=fromdate,
todate=todate,
dtformat='%Y-%m-%d',
timeframe=bt.TimeFrame.Days,
compression=1,
datetime=0, open=1, high=2, low=3, close=4, volume=5,
openinterest=-1,
header=True
)
return data
这样每次调用就一行代码:data = load_csv_data('000300.csv')。清爽多了。
4.3 在线数据获取——Yahoo Finance
本地数据毕竟有限。想测美股?想测港股?在线获取就派上用场了。
Backtrader原生支持Yahoo Finance的数据接口。不过要注意,Yahoo的API这几年改了好几次,老版本的接口已经废了。
我建议用yfinance库来获取数据,然后转成Backtrader能吃的格式:
import yfinance as yf
import pandas as pd
import backtrader as bt
# 下载数据
df = yf.download('AAPL', start='2020-01-01', end='2023-12-31')
# 重置索引,把日期变成一列
df.reset_index(inplace=True)
# 重命名列——Backtrader要求小写
df.columns = ['datetime', 'open', 'high', 'low', 'close',
'adj_close', 'volume']
# 保存为CSV(可选)
df.to_csv('AAPL.csv', index=False)
# 加载到Backtrader
data = bt.feeds.GenericCSVData(
dataname='AAPL.csv',
dtformat='%Y-%m-%d',
timeframe=bt.TimeFrame.Days,
compression=1,
datetime=0, open=1, high=2, low=3, close=4, volume=6,
openinterest=-1,
header=True
)
Yahoo Finance返回的数据里有个“Adj Close”(复权收盘价),这个在回测中很重要。如果你用普通收盘价做回测,遇到分红送股时,历史数据会出现跳空,回测结果会失真。我个人习惯用复权价。
4.4 数据清洗与预处理——80%的功夫在这里
数据拿到手,别急着跑回测。先洗一洗。
我总结了一套“三步清洗法”:
- 去空值——有些日子没交易,数据是NaN
- 去异常值——比如某天收盘价突然变成0
- 去重复值——同一个日期出现两次
看代码:
def clean_data(df):
"""数据清洗三步走"""
# 第一步:去空值
df.dropna(inplace=True)
# 第二步:去异常值——价格不能为0或负数
df = df[(df['close'] > 0) & (df['volume'] >= 0)]
# 第三步:去重复值——保留第一条,删除后续重复
df.drop_duplicates(subset=['datetime'], keep='first', inplace=True)
# 排序——按时间升序
df.sort_values('datetime', inplace=True)
return df
我曾经在回测沪深300时,发现某一天成交量突然变成0。一开始以为是停牌,后来查了才发现是数据源的问题——那天其实有交易,但数据采集时漏了。如果不处理,回测时策略会以为那天没交易,导致持仓计算错误。所以,成交量异常值一定要检查。
4.5 知识体系总览
说了这么多,咱们用一张图把整个数据处理的流程串起来:
说白了,数据处理的本质就是:把各种来源的原始数据,统一成Backtrader能认的格式。格式对了,清洗干净了,后面的回测才能跑得稳。
我会在项目根目录建一个data/文件夹,里面按品种分:data/stock/、data/futures/。每个品种的CSV文件命名统一为代码_周期.csv,比如000300_daily.csv。这样找数据一目了然。
好了,数据源这块就聊到这儿。记住一句话:数据质量决定回测质量。别偷懒,该洗的数据一定要洗。
公众号:蓝海资料掘金营,微信deep3321