Backtrader 数据篇

Backtrader 数据篇

上一篇已经学习了整个框架的大概划分模块,对于 Backtrader 这框架有了一个大概的认识,这篇将学习这框架基石--数据流程。

下面将会一一解答关于这框架 数据方面的疑问
怎么将数据填充到框架种?
数据组织形式是怎么样?
怎么访问数据?
在回测的过程种,数据流是以怎么形式传递?
怎么扩展其他回测指标?

数据填充

在简介篇,已经知道所有的模块有大脑统一控制管理,数据模块也不例外,所以首先要实例化 Cerebro ,然后往这 Cerebro 塞数据,再统一调度管理。

下面数据是以 dataframe 数据格式为例的介绍,当然 backtrader 也支持其他数据规范的格式,都差不多。

backtrader 把规范的数据包装成了 Data Feeds ,其实就是一个回测数据集合管理类。数据可以添加一个或者多个股票数据。

如何填充数据?

只需要指定数据源,回测时间段,即可添加多个股票数据源。

参数描述
dataname 数据源
fromdate 回测开始时间
todate 回测结束时间

示例代码


# 股票查询开始和结束时间
start_query = '2022-01-01'
end_query = '2022-09-01'
# 回测开始和结束时间
start_date = datetime.datetime(2022, 7, 10)
end_date = datetime.datetime(2022, 8, 10)
cerebro = bt.Cerebro()
# 添加几个股票数据
codes = [
    '300015',
    '300347',
    '300760',
    '603127',
    '600438'
]
# 添加多个股票回测数据
for code in codes:
    data = sdb.stock_daily(code, start_query, end_query)
    data.index.names = ['datetime']
    date_feed = bt.feeds.PandasData(dataname=data, fromdate=start_date, todate=end_date)
    cerebro.adddata(date_feed, name=code)
    print('添加股票数据:code: %s' % code)

注意:查询的数据时间段与回测时间段是分开的,最终数据流是以回测时间段测试数据。

参数 name 是这张表的表名。 self.datas[0]._name 可以打印表名。

存储方式

把每个股票数据看作为一张表,其实就是一张指标维度和时间维度的表,self.datas 就是集合了多个股票的数据集,形成了一个三维数据源,分别是: 数据表维度、时间维度、指标维度。

如上图所示,Data Feeds 种的 self.datas 数据类型是 list. 每个元素是一张包含时间维度和指标维度的股票数据表。

数据表维度

该维度其实就是 list 集合,集合了所有添加进来了股票数据,每个股票数据都有时间维度和指标维度构成的数据表。
self.datas[N]

指标维度

该维度是回测时使用的指标,除了常用指标还可以自定义指标。
self.datas[N].lines.xxx[M]

参数

字段类型描述
datetime float 日期,如果打印日期,
用datetime.date[0]
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价
volume float 成交量
openinterest float 持仓量,一般没使用
其他扩展指标 其他 自定义或扩展指标,列如 pe 、pb

所有的指标

self.data.lines.getlinealiases()

控制台
>> lines ('close', 'low', 'high', 'open', 'volume', 'openinterest', 'datetime')

时间维度

时间维度其实是回测时间段,fromdate ~ todate 之间。
self.data[N].lines.datetime.date(M)

数据索引

self.datas 数据类型是 list

  1. 下标索引:self.datas[N],其中 N 为 0 到 N-1 时,是正向,N 为 -1 到 -N 时为反向
  2. 缩写索引,self.dataN ,注意不是 datas 。N 为 0 到 N-1。
  3. 表名索引,self.getdatabyname('name'),其中 name 为导入数据时 adddata(date_feed, name=code) 时设置的表名。
  4. 第一个数据集索引,self.datas[0] 等价 self.data0 等价 self.data

注意:self.datas 和 self.data 的区别,self.datas 是数据集合 list. self.data 是第 0 个数据。self.dataN 的访问是没有 s 的。

self.datas[N].lines

索引方式
日期:self.datas[N].lines.datetime.date(N) 其他:self.datas[N].lines.其他字段名(open、high、low、close、volume)[N]

注意:datetime 是以 float 数据类型存储,访问是需要借助 xxx.date(N) 函数进行转换。

示例

# 访问第一个数据表的 close 指标的索引 0
self.data[0].lines.close[0]

切片方式

self.data1.lines.close.get(ago=N, size=M))
参数描述
ago 索引开始位置
size 切片大小

返回值:array 数组 [close[N-(M-1)],...,close[N-1],close[N]]
如果 N - (M - 1) <1,返回空数组 []。
列如:N = 3 ,M = 4,返回 [].

策略中的数据流

回测长度:N = self.data.buflen()

索引下标 0 的含义

索引下标 0 在 init() 函数和 next() 函数是不一样的,在 init() 函数中索引 0 是代表回测时间 todate ,在 next() 函数中 索引 0 是代表当前回测的时间。

init()函数中的数据流

在 init() 函数中,索引 0 是 todate ,索引 1 是 fromdate. 支持 正向和反向访问的两种方式。

  • 正向索引的索引下标为 1、2 ... N
  • 反向索引下标为 0 、-1、-2 ... -(N-1).

其中,对应下标值是相同

正向索引反向索引
0 N
1 -(N-1)
2 -(N-2)
... ...
N-1 -1
N 0

next()函数中的数据流

在 next() 函数中,索引 0 永远是当前的时间节点,索引 0 随着以时间维度的循环,不停的移动。backtrader 是已经回测过的了,forward 是还没有回测到的。

自定义读取数据

如果你觉得每次都要设置这么多参数来告知指标位置很麻烦,那你也可以重新自定义数据读取函数,自定义的方式就是继承数据加载类 GenericCSVData、PandasData 再构建一个新的类,然后在新的类里统一设置参数:

class My_PandasData(bt.feeds.PandasData):
    params = (
        ('fromdate', datetime.datetime(2019, 1, 2)),
        ('todate', datetime.datetime(2021, 1, 28)),
        ('nullvalue', 0.0),
        ('dtformat', ('%Y-%m-%d')),
        ('datetime', 0),
        ('time', -1),
        ('high', 3),
        ('low', 4),
        ('open', 2),
        ('close', 5),
        ('volume', 6),
        ('openinterest', -1)
    )

新增指标

backtrader 除了提供基本的指标外('close', 'low', 'high', 'open', 'volume', 'openinterest', 'datetime'),还提供给用户自定义扩展。例如:macd,pe,pb等等指标。

如何扩展?

  1. 继承 bt.feeds.PandasData 类,定义新指标。
  2. 数据源扩展新指标对应的列,并初始化新指标值。

示例

# 继承
class PandasData_more(bt.feeds.PandasData):
    lines = ('pe', 'pb',)  # 要添加的线
    # 设置 line 在数据源上的列位置
    params = (
        ('pe', -1),
        ('pb', -1),
    )

# 数据源新增列
# 股票查询开始和结束时间
start_query = '2022-01-01'
end_query = '2022-09-01'
# 回测开始和结束时间
start_date = datetime.datetime(2022, 7, 11)
end_date = datetime.datetime(2022, 8, 10)
cerebro = bt.Cerebro()
# 添加几个股票数据
codes = [
    '300015',
    '300347',
    '300760',
    '603127',
    '600438'
]
# 添加多个股票回测数据
for code in codes:
    data = sdb.stock_daily(code, start_query, end_query)
    data.index.names = ['datetime']
    # 新增指标列
    data['pe'] = 3
    data['pb'] = 4
    date_feed = PandasData_more(dataname=data, fromdate=start_date, todate=end_date)
    cerebro.adddata(date_feed, name=code)
    print('添加股票数据:code: %s' % code)

cerebro.addstrategy(TestStrategy)
cerebro.run()

常用函数手册

函数描述
self.data.buflen() 回测总长度
len(self.data) 已经处理的数据位置
self.data.lines.getlinealiases() 指标列表
bt.Strategy.init() 一次调用,策略初始化函数
计算指标值,买卖信号等耗时操作
bt.Strategy.next() 多次调用,策略回测调用函数
循环所有的回测时间段
self.datas[N].lines.datetime.date(N) 日期索引
self.datas[N].lines.close.date(N) 指标close索引
self.getdatabyname('name') 表名索引,表名建议以code命名
self.dataT.lines.close.get(ago=N, size=M)) 切片函数
bt.num2date() 时间 datatime 格式将其转为“xxxx-xx-xx xx:xx:xx”这种形式

测试示例

import datetime

import backtrader as bt

import stock_db as sdb


class PandasData_more(bt.feeds.PandasData):
    lines = ('pe', 'pb',)  # 要添加的线
    # 设置 line 在数据源上的列位置
    params = (
        ('pe', -1),
        ('pb', -1),
    )


class My_PandasData(bt.feeds.PandasData):
    params = (
        ('fromdate', datetime.datetime(2019, 1, 2)),
        ('todate', datetime.datetime(2021, 1, 28)),
        ('nullvalue', 0.0),
        ('dtformat', ('%Y-%m-%d')),
        ('datetime', 0),
        ('time', -1),
        ('high', 3),
        ('low', 4),
        ('open', 2),
        ('close', 5),
        ('volume', 6),
        ('openinterest', -1)
    )


class TestStrategy(bt.Strategy):
    # 可选,设置回测的可变参数:如移动均线的周期
    # params = (
    #     (..., ...),  # 最后一个“,”最好别删!
    # )

    def log(self, txt, dt=None):
        """
        可选,构建策略打印日志的函数:可用于打印订单记录或交易记录等
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datas[0].datetime.date(0)
        print('%s,%s' % (dt.isoformat(), txt))

    def __init__(self):
        """
        必选,初始化属性、计算指标等
        """

        print(type(self.datas[0].lines.datetime[0]))
        print(type(self.datas[0].lines.volume[0]))
        self.count = 0
        print("------------- init 中的索引位置-------------")
        print('lines', self.data1.lines.getlinealiases())
        print('datas type', type(self.datas))
        print("0 索引:", 'datetime', self.datas[1].lines.datetime.date(0), 'close', self.data1.lines.close[0])
        print('-1 索引:', 'datetime', self.data1.lines.datetime.date(-1), 'close', self.data1.lines.close[-1])
        print('-2 索引:', 'datetime', self.data1.lines.datetime.date(-2), 'close', self.data1.lines.close[-2])
        print('1 索引: ', 'datetime', self.data1.lines.datetime.date(1), 'close', self.data1.lines.close[1])
        print('2 索引: ', 'datetime', self.data1.lines.datetime.date(2), 'close', self.data1.lines.close[2])
        print('从 0 开始往前取3天的收盘价:', self.data1.lines.close.get(ago=0, size=3))
        print("从-1开始往前取3天的收盘价:", self.data1.lines.close.get(ago=3, size=3))
        print("从-2开始往前取3天的收盘价:", self.data1.lines.close.get(ago=-2, size=3))
        print("回测的总长度:", self.data.buflen())
        pass

    def notify_order(self, order):
        """
        可选,打印订单信息
        :param order:
        :return:
        """
        pass

    def notify_trade(self, trade):
        """
        可选,打印交易信息
        :param trade:
        :return:
        """
        pass

    def next(self):
        """
        必选,编写交易策略逻辑
        :return:
        """
        print(f"------------- next 的第{self.count + 1}次循环 --------------")
        print('当前节点(今日): ', 'datetime', self.data1.lines.datetime.date(0), 'close', self.data1.lines.close[0])
        print("往前推1天(昨日):", 'datetime', self.data1.lines.datetime.date(-1), 'close', self.data1.lines.close[-1])
        print("往前推2天(前日)", 'datetime', self.data1.lines.datetime.date(-2), 'close', self.data1.lines.close[-2])
        print("前日、昨日、今日的收盘价:", self.data1.lines.close.get(ago=0, size=3))
        # print("往后推1天(明日):", 'datetime', self.data1.lines.datetime.date(1), 'close', self.data1.lines.close[1])
        # print("往后推2天(明后日)", 'datetime', self.data1.lines.datetime.date(2), 'close', self.data1.lines.close[2])
        print("已处理的数据点:", len(self.data))
        print("回测的总长度:", self.data.buflen())
        self.count += 1
        pass


# 股票查询开始和结束时间
start_query = '2022-01-01'
end_query = '2022-09-01'
# 回测开始和结束时间
start_date = datetime.datetime(2022, 7, 11)
end_date = datetime.datetime(2022, 8, 10)
cerebro = bt.Cerebro()
# 添加几个股票数据
codes = [
    '300015',
    '300347',
    '300760',
    '603127',
    '600438'
]
# 添加多个股票回测数据
for code in codes:
    data = sdb.stock_daily(code, start_query, end_query)
    data.index.names = ['datetime']
    # 新增指标列
    data['pe'] = 3
    data['pb'] = 4
    date_feed = PandasData_more(dataname=data, fromdate=start_date, todate=end_date)
    cerebro.adddata(date_feed, name=code)
    print('添加股票数据:code: %s' % code)

cerebro.addstrategy(TestStrategy)
cerebro.run()
# cerebro.plot()

if __name__ == '__main__':
    pass

写于 2022 年 10 月 05 日 14:24:59

 
posted @ 2022-10-05 14:45  醉卧梦星河  阅读(655)  评论(1编辑  收藏  举报