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,核心就两步:

  1. 创建数据源对象
  2. 把数据源塞进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%的功夫在这里

数据拿到手,别急着跑回测。先洗一洗。

我总结了一套“三步清洗法”:

  1. 去空值——有些日子没交易,数据是NaN
  2. 去异常值——比如某天收盘价突然变成0
  3. 去重复值——同一个日期出现两次

看代码:

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 知识体系总览

说了这么多,咱们用一张图把整个数据处理的流程串起来:

数据源处理流程 数据源 格式检查 数据清洗 本地CSV 在线API(Yahoo等) 数据库 列名小写 日期格式 六件套完整性 去空值 去异常值 去重复值 Backtrader数据源

说白了,数据处理的本质就是:把各种来源的原始数据,统一成Backtrader能认的格式。格式对了,清洗干净了,后面的回测才能跑得稳。

💡 我的个人习惯:

我会在项目根目录建一个data/文件夹,里面按品种分:data/stock/data/futures/。每个品种的CSV文件命名统一为代码_周期.csv,比如000300_daily.csv。这样找数据一目了然。

好了,数据源这块就聊到这儿。记住一句话:数据质量决定回测质量。别偷懒,该洗的数据一定要洗。


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