Backtrader从0到1------Data Feeds【入门篇】
- [0. 前言](#0. 前言)
- [1. 数据源访问](#1. 数据源访问)
- [2. 添加CSV格式数据](#2. 添加CSV格式数据)
- [3. 参数持久化](#3. 参数持久化)
- [4. 添加Pandas数据](#4. 添加Pandas数据)
- [5. 总结](#5. 总结)
0. 前言
所谓Data Feeds,翻译过来就是数据推送。
笔者在学习Data Feeds模块时,发现官网上很多部分的代码都过分冗余,并且真正有实际应用意义的内容也不多。本文将着重介绍CSV格式文件的加载方式,以及pandas数据的加载方式。
笔者在加载这些数据时,已经形成了一套自己的体系,包括CSV文件内容格式,如何读取,以及自定义CSV类型参数持久化,都有自己的一套规范。希望大家在学习时,也能形成一套自己的使用体系。
学习完本文,将保证你能熟练的将CSV数据加载进Backtrader,处理不同级别时间的数据也将不是问题。
有一些提升部分,如低级别数据的聚合,本文将不会涉及,如有需要,也可以出一篇高级篇,专门介绍一下。这部分内容可用于搭建更复杂的交易系统(如多级别联动),但是对于单周期级别上的策略,就有点画蛇添足了。
话不多说,让我们进入Data Feeds的世界!!!
1. 数据源访问
通过Backtrader从0到1------第一个回测策略的学习,我们已经能将数据源lines
添加进Cerebro
实例中了。在策略中访问数据源有如下几种方式:
1. 访问lines
self.datas[i]
表示访问插入的第i
个lines
。self.data
等价于self.data0
等价于self.datas[0]
,表示访问插入的第一个lines
,是一种便捷访问方式。
2. 访问lines中的特定line,也是特定列(以访问第一个lines中的line为例)
self.datas[0].列名
,就可以拿到对应的列。一个列就是一个bar
,一个line
,它的index
是Backtrader
中特有的(上篇文章讲过)。
2. 添加CSV格式数据
1. 准备CSV数据
- 这里选择从
akshare
中拉取豆油的30分钟k线数据。并保存为CSV格式。
python
import akshare as ak
# 获取螺纹钢主力合约 1 分钟数据(代码格式:交易所_品种主力,如 "SHFE.rb主力")
df = ak.futures_zh_minute_sina(
symbol="Y0", # 合约代码
period="30", # 1/5/15/30/60 分钟
)
print(df.head())
df.to_csv('SoybeanOil.csv', index=False)
- 查看CSV文件:

- 可以看到拿到的数据一共有7列,其中
hold
是持仓量,还有一个翻译是openinterest
。 pd.to_csv
时,切记带上index=True
选项,不然你将看到8列数据,带一列自增索引。- 今后我们的CSV文件格式,就按图片上来,需要有一行标题行,一行中数据以逗号隔开。第一列是
datetime
,第二列是open
,以此类推。(当然,文件中到底有没有标题行,以什么为分割符,都可以在读取时通过参数来控制,这里我统一认为文件要带标题行,以,
为分割符,即是为了规范,也是为了好看)。
2. 加载CSV文件进Cerebro
- 使用
bt.feeds.GenericCSVData()
方法,加载数据,保证加载的列和CSV文件中的列一一对应(这里只是为了规范一点,好看一点,不是语法要求):
python
data = bt.feeds.GenericCSVData(dataname='SoybeanOil.csv',
nullvalue=0.0, # 空值替换为0.0
dtformat=('%Y-%m-%d %H:%M:%S'), # 和csv文件中的日期格式适配
datetime=0, # 指定为第0列
open=1, # 指定为第1列
high=2, # 以此类推
low=3,
close=4,
volume=5,
openinterest=6,
timeframe=bt.TimeFrame.Minutes, # 分钟级数据
)
- 参数详解:
dataname
:CSV文件路径或或类似文件的对象;datetime
:(默认值0)包含日期(或日期时间)字段的列;open
(默认值1),high
(默认值2),low
(默认值3),close
(默认值4),volume
(默认值5),openinterest
(默认值6);- 拿
open=1
为例,表示指定索引(Python索引)为1的列,列名为open
。 - 如果传递负值(例如:-1),则表示该字段在CSV数据中不存在。
- 拿
nullvalue
:(默认值:float('NaN')
)将空值替换为一个值,例如nullvalue=0.0
就是将空值替换为0.0;dtformat
:(默认值:%Y-%m-%d %H:%M:%S
)和CSV文件中日期的格式适配;timeframe
:(默认值:TimeFrame.Days
)可选项有:Ticks, Seconds, Minutes, Days, Weeks, Months and Years
。例子中我们加载的是30分钟级别数据,所以填bt.TimeFrame.Minutes
;
- 没有用到的参数:
headers
:(默认值:True
)指示传递的数据是否有初始标题行;separator
:(默认: ",
")指定分隔符。time
:(默认值:-1)如果与日期时间字段分开,如:date,time
,则需要用该参数指定时间所在列;tmformat
:(默认值:%H:%M:%S
)令time
列的格式和CSV文件中时间的格式适配。compression
:(默认值:1)每根柱状图的实际小柱状图数量。仅在数据重采样/回放时用的上。
如果没用用到的参数不理解,特别是
compression
,没有关系,因为很复杂的场景下(如多级别联动),才会用到。
3. 代码测试,看看数据是否被成功载入了
- 这里添加的策略很简单,就是将加载的信息,遍历一遍:
python
import backtrader as bt
class MyStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
# 注意要和datetime类型适配,使用datetime(0)表示当前时间
dt = dt or self.datas[0].datetime.datetime(0)
# dt.isoformat(sep=" ")格式化输出,以空格为date和time的分隔符
print('%s, %s' % (dt.isoformat(sep=" "), txt))
def __init__(self):
# 为每一列,创建一个bar
self.dataopen = self.datas[0].open # 开盘价bar
self.datahigh = self.datas[0].high # 最高价bar
self.datalow = self.datas[0].low # 最低价bar
self.dataclose = self.datas[0].close # 收盘价bar
self.datavolume = self.datas[0].volume # 成交量bar
self.dataopeninterest = self.datas[0].openinterest # 持仓量bar
def next(self):
# 打印该行全部信息
self.log('open:%.2f, high:%.2f, low:%.2f, close:%.2f, volume:%.2f, openinterest:%.2f'
% (self.dataopen[0], self.datahigh[0], self.datalow[0], self.dataclose[0], self.datavolume[0], self.dataopeninterest[0]))
if __name__ == "__main__":
data = bt.feeds.GenericCSVData(dataname='SoybeanOil.csv',
nullvalue=0.0, # 空值替换为0.0
dtformat=('%Y-%m-%d %H:%M:%S'), # 和csv文件中的日期格式适配
datetime=0, # 指定为第0列
open=1, # 指定为第1列
high=2, # 以此类推
low=3,
close=4,
volume=5,
openinterest=6,
timeframe=bt.TimeFrame.Minutes, # 分钟级数据
)
cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(0.0002)
cerebro.addstrategy(MyStrategy)
cerebro.run()
- 输出结果:
0
2024-12-02 22:00:00, open:8058.00, high:8070.00, low:8050.00, close:8070.00, volume:63069.00, openinterest:340406.00
2024-12-02 22:30:00, open:8070.00, high:8070.00, low:8040.00, close:8052.00, volume:53939.00, openinterest:340349.00
2024-12-02 23:00:00, open:8050.00, high:8064.00, low:8042.00, close:8062.00, volume:60184.00, openinterest:338767.00
2024-12-03 09:30:00, open:8062.00, high:8116.00, low:8062.00, close:8086.00, volume:107417.00, openinterest:333335.00
2024-12-03 10:00:00, open:8086.00, high:8096.00, low:8060.00, close:8066.00, volume:54452.00, openinterest:331981.00
2024-12-03 10:45:00, open:8066.00, high:8086.00, low:8056.00, close:8058.00, volume:63507.00, openinterest:331925.00
2024-12-03 11:15:00, open:8058.00, high:8070.00, low:8002.00, close:8010.00, volume:95367.00, openinterest:334204.00
2024-12-03 13:45:00, open:8010.00, high:8038.00, low:8004.00, close:8014.00, volume:42692.00, openinterest:333286.00
...
- 可以看到,数据被成功载入了。
今后这段策略代码就可以拿来做测试,看看数据的加载情况。
3. 参数持久化
不难发现,有很多参数都是固定的,或者说我们希望,即使不传参,这个参数也应该有我们规定的默认值。这就要用到参数持久化了。
1. 通过子类化,实现参数持久化
- 该子类继承
bt.feeds.GenericCSVData
:
python
import backtrader as bt
class MyDatatype(bt.feeds.GenericCSVData):
params = (
('nullvalue', 0.0),
('dtformat', ('%Y-%m-%d %H:%M:%S')),
('datetime', 0),
('open', 1),
('high', 2),
('low', 3),
('close', 4),
('volume', 5),
('openinterest', 6),
('timeframe', bt.TimeFrame.Days), # 默认是Day级别k线数据
)
- 这样一来,我们只需要传很少的参数就可以了:
python
data = MyDatatype(dataname="SoybeanOil.csv", timeframe=bt.TimeFrame.Minutes)
2. 完整代码测试
python
import backtrader as bt
from Datatype import MyDatatype
class MyStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
# 注意要和datetime类型适配,使用datetime(0)表示当前时间
dt = dt or self.datas[0].datetime.datetime(0)
# dt.isoformat(sep=" ")格式化输出,以空格为date和time的分隔符
print('%s, %s' % (dt.isoformat(sep=" "), txt))
def __init__(self):
# 为每一列,创建一个bar
self.dataopen = self.datas[0].open # 开盘价bar
self.datahigh = self.datas[0].high # 最高价bar
self.datalow = self.datas[0].low # 最低价bar
self.dataclose = self.datas[0].close # 收盘价bar
self.datavolume = self.datas[0].volume # 成交量bar
self.dataopeninterest = self.datas[0].openinterest # 持仓量bar
def next(self):
# 打印该行全部信息
self.log('open:%.2f, high:%.2f, low:%.2f, close:%.2f, volume:%.2f, openinterest:%.2f'
% (self.dataopen[0], self.datahigh[0], self.datalow[0], self.dataclose[0], self.datavolume[0], self.dataopeninterest[0]))
if __name__ == "__main__":
# 这里用到了参数持久化
data = MyDatatype(dataname="SoybeanOil.csv", timeframe=bt.TimeFrame.Minutes)
cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(0.0002)
cerebro.addstrategy(MyStrategy)
cerebro.run()
- 效果和之前一模一样,但是参数变简洁了,非常Nice!!!
4. 添加Pandas数据
1. 准备数据
python
import pandas as pd
pd.read_csv("SoybeanOil.csv", index_col=0, date_format="'%Y-%m-%d %H:%M:%S'")
- 看一眼数据的样子:

- 对于Pandas数据,尽量保证索引是时间类型,这是Backtrader的要求。
2. 看看部分源码
python
class PandasData(feed.DataBase):
'''
The ``dataname`` parameter inherited from ``feed.DataBase`` is the pandas
DataFrame
'''
params = (
# datetime 的可能值(必须已经存在)
# None : datetime 是Pandas的索引
# -1 : 自动检测位置,匹配名字相同的列
# >= 0 : 从0开始,不包括索引,匹配Pandas的第几列
# string : 手动传入列名称,进行匹配
('datetime', None),
# 可能值:
# None : 该列不存在
# -1 : 自动检测位置,匹配名字相同的列
# >= 0 : numeric index to the colum in the pandas dataframe
# string : 手动传入列名称,进行匹配
('open', -1),
('high', -1),
('low', -1),
('close', -1),
('volume', -1),
('openinterest', -1),
)
- Backtrader支持的Pandas数据需要通过
PandasData
类构造,之后add
的也是这个类的实例化对象。 - 参数的解释已经写在代码注释中了,这里说一下一般的传参方式:
datetime
参数一般是不传的,让他为默认值None
,因为上面准备的Pandas数据索引已经是时间类型了;- 剩余六个基本参数,如果该列在Pandas数据中存在,就传它的列索引(不带
datetime
列,索引从0开始),或者列名;如果不存在就传None
。(毕竟都用Pandas了,还是传列名吧,可读性强,可移植性高)。
- 不同于加载CSV数据,这里不需要再指定
timeframe
了,因为该信息已经包含在Pandas的datetime
索引中了。
3. 完整代码演示
python
import backtrader as bt
import pandas as pd
class MyStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
# 注意要和datetime类型适配,使用datetime(0)表示当前时间
dt = dt or self.datas[0].datetime.datetime(0)
# dt.isoformat(sep=" ")格式化输出,以空格为date和time的分隔符
print('%s, %s' % (dt.isoformat(sep=" "), txt))
def __init__(self):
# 为每一列,创建一个bar
self.dataopen = self.datas[0].open # 开盘价bar
self.datahigh = self.datas[0].high # 最高价bar
self.datalow = self.datas[0].low # 最低价bar
self.dataclose = self.datas[0].close # 收盘价bar
self.datavolume = self.datas[0].volume # 成交量bar
self.dataopeninterest = self.datas[0].openinterest # 持仓量bar
def next(self):
# 打印该行全部信息
self.log('open:%.2f, high:%.2f, low:%.2f, close:%.2f, volume:%.2f, openinterest:%.2f'
% (self.dataopen[0], self.datahigh[0], self.datalow[0], self.dataclose[0], self.datavolume[0], self.dataopeninterest[0]))
if __name__ == "__main__":
data = pd.read_csv("SoybeanOil.csv", index_col=0, date_format='%Y-%m-%d %H:%M:%S')
# 这里Pandas数据中,成交量列名是hold,不是openinterest,需要显示传入,让参数适配
data = bt.feeds.PandasData(dataname=data, openinterest='hold')
cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(0.0002)
cerebro.addstrategy(MyStrategy)
cerebro.run()
- 输出:
0
2024-12-02 22:00:00, open:8058.00, high:8070.00, low:8050.00, close:8070.00, volume:63069.00, openinterest:340406.00
2024-12-02 22:30:00, open:8070.00, high:8070.00, low:8040.00, close:8052.00, volume:53939.00, openinterest:340349.00
2024-12-02 23:00:00, open:8050.00, high:8064.00, low:8042.00, close:8062.00, volume:60184.00, openinterest:338767.00
2024-12-03 09:30:00, open:8062.00, high:8116.00, low:8062.00, close:8086.00, volume:107417.00, openinterest:333335.00
2024-12-03 10:00:00, open:8086.00, high:8096.00, low:8060.00, close:8066.00, volume:54452.00, openinterest:331981.00
2024-12-03 10:45:00, open:8066.00, high:8086.00, low:8056.00, close:8058.00, volume:63507.00, openinterest:331925.00
2024-12-03 11:15:00, open:8058.00, high:8070.00, low:8002.00, close:8010.00, volume:95367.00, openinterest:334204.00
2024-12-03 13:45:00, open:8010.00, high:8038.00, low:8004.00, close:8014.00, volume:42692.00, openinterest:333286.00
...
5. 总结
至此,绝大部分的数据加载问题已经难不住我们了。对比这两种数据加载方式,Pandas加载显然比CSV的加载更加灵活,但是CSV加载则更加规范。
本人习惯使用更为规范的CSV加载,即使有现成的Pandas数据,也可以先pd.to_csv()
保存成CSV格式,预处理一下,再加载进Backtrader。