backtrader概念与源码

参透了backtrader精华后,仰天长啸,壮怀激烈。


Backtrader 是目前用得比较多的一个回测框架,也能接入实盘交易。代码也简洁优美,值得学习借鉴。
Backtrader 是 2015 年开源的 Python 量化回测框架(支持实盘交易),功能丰富,操作方便灵活:

  • 品种多:股票、期货、期权、外汇、数字货币;
  • 周期全:Ticks 级、秒级、分钟级、日度、周度、月度、年度;
  • 速度快:pandas 矢量运算、多策略并行运算;
  • 组件多:内置 Ta-lib 技术指标库、PyFlio 分析模块、plot 绘图模块、参数优化等;
  • 超灵活:即可以随意搭配组件,又支持扩展自己开发的功能,想怎么玩就怎么玩;
  • 社区活跃、帮助文档齐全,官网:https://www.backtrader.com/。

backtrader源码核心逻辑

backtrader两个核心组件: DataFeed和Strategy,前者不断取行情数据,后者根据数据进行指标计算,然后发出买卖指令。cerebro作为指挥大脑,调动二者的运转。cerebro核心逻辑的伪代码:

def run():
	while True:
	   for data in self.datas:
	      data.load() ## 将调用_load(),每次获得一条行情数据(OHLC bar)
	   for strat in self.strategies:
	      strat.next()

对应到源码,看看cerebro中策略被执行的地方:

def run(self, **kwargs):
    ...
       for iterstrat in iterstrats:
    			runstrat = self.runstrategies(iterstrat)
    			...

在runstrategies()中,会执行每个策略的_next()方法:

    for strat in runstrats:
    	strat._next()

在_next()方法里,会根据当前数据长度和最小周期的长度判断是否进入next():

minperstatus = self._getminperstatus()
if minperstatus < 0:
    self.next()

策略里有个变量self._minperiods,记录着每支股票的最小周期。只有每支股票的数据长度大于最小周期,才会进入策略的next()方法。next()就会执行我们定义的策略的逻辑。

每个data对应一支股票,每个data里持有多个line,一个line是一个LineBuffer对象,里面包含一个array数组,对应时间序列上每点的value。

在这里插入图片描述
在live trade中,如果feed不断有数据过来,_runnext就无限循环。

LineBuffer对array的一个封装,实现了对下标,比较运算等的重载。要学会line[]和line()的含义。
下面代码帮你认识LineBuffer对象:

class MyDataSeries(bt.DataSeries):
    lines = ('line1','line2','line3',) ## 定义了三条LineBuffer
    def __init__(self):
        pass

def test_linebuffer():
    datas = MyDataSeries()
    for x in range(10):
        datas.line1.forward()
        datas.line1[0] = round(random.uniform(7, 10), 2)

    datas.line1.forward()
    datas.line1[0] = 100
    datas.line1[-1] = datas.line1[0]
    datas.line1.backwards()

    line = bt.LineBuffer()
    line.forward()
    line[0] = 1.21
    line.forward()
    line[0] = 1.22
    pass

上面我们创建了一个持有若干条LineBuffer的Data对象,然后填充LineBuffer。
我们完全可以基于LineBuffer、Pandas、talib库实现一个简易的实盘交易系统。

实盘

backtrader在实盘运行的时候是单核处理的,实际上,在回测的时候,也是单核处理的(参数优化除外)。既然是单核处理的,那么,很显然的一个问题就是,CPU的处理能力有限制,为了能在信号出来的时候尽快的下单,并把信号传递到交易所里面,降低交易的滑点,在策略里面进行的数据处理越少越好。

实盘交易中bar的合成不建议在策略里做,最好预先合成好。

为何进不了策略的next()? 请看函数:

def _getminperstatus(self):
    # check the min period status connected to datas
    dlens = map(operator.sub, self._minperiods, map(len, self.datas))
    self._minperstatus = minperstatus = max(dlens)
    return minperstatus

也就是最小周期需要小于数据的长度。

类图

在这里插入图片描述

Line

backtrader里很多数据都是LineBuffer类型,它代表时间序列上的一组数据,背后就是一个数组。采用Line这个概念容易直观地对数据做对比。每个时间点上的数据必须是数值型,不能是字符串或日期。,所以你会看到bt提供了num2dt和dt2num两个转换函数。
通过line[0]表示当前时间点的数据,line[-1]代表上一个时间点的数据。
line(-1)是延迟索引。通过延迟索引-1获取一个和line一样的line,但是值会向前移动1位。典型示例就是把要对比当前收盘价和上一天的收盘价时,就用data.close和data.close(-1)做对比。

在LineSeries中添加LineBuffer

默认的DataBase已经有六条基本的Line: ‘close’, ‘low’, ‘high’, ‘open’, ‘volume’, ‘openinterest’,有时我们想给自己的DataFeed新增几条Line,可以这么定义:

class MyDatabase(bt.DataBase):
    lines = (('haha'),('hoho'), )

上面新增两条line: haha和hoho

resampling

手上只有分钟级别的数据,而我们想要的是日线级别的数据,或者说手上是日线级别的数据,希望变成周线级别的数据。在backtrader中,有很好的的方法解决这样的问题。总而言之,就是timeframe转换的问题。
resampling字面意思看起来是“采样”,准确的来说,是上采样,从小的时间点变成大的时间点。方法很简单,就是在添加数据的时候,不在使用 cerebro.adddata(data),而是使用cerebro.resampledata(data, **kwargs)。后面的参数主要有两个,一个是timeframe,也就是你希望变成的timeframe是多少,day还是week;另外一个是compression,就是对bar进行压缩。

Lines的耦合(coupling)

backtrader对不同的lines进行适配处理,使他们建立对照关系。耦合主要用于将时间窗口不同的两个line建立关系。比如, 不同时间窗口的数据源具有不同的长度,indcator在使用这些数据的时候会复制这个长度。例如:

  • 股票的日线数据,每年大约250个(bar,对应250个工作日)
  • 股票的周线数据,每年大约52个(bar,对应52周)

现在如果我们要比对日线和周线的移动平均,这两个line一个长度为52个,一个250个,长度都不一样,如何比对呢?在backtrader中,使用一个空括号()来完成,如下示例:

class MyStrategy(bt.Strategy):
	params = dict(period=20)

	def __init__(self):
	
	 # data0 是日线数据
	sma0 = btind.SMA(self.data0, period=15) # 15 天的平均
	# data1 是周线数据
	sma1 = btind.SMA(self.data1, period=5) # 5 周的平均
	self.buysig = sma0 > sma1()
	
	def next(self):
	if self.buysig[0]:
	print('日均线大于周均线!')

Line运算

line之间做加减、比较等运算将会得到新的Line对象,我们以此作为买卖信号。

策略

在next()里接收当前行情,作出买卖判断,通过buy()和sell()函数下单。实际完成买卖是在broker里进行的。

class MyStrategy(bt.Strategy):
  # 先在 __init__ 中提前算好指标
    def __init__(self):
        self.sma1 = btind.SimpleMovingAverage(self.data)
        self.ema1 = btind.ExponentialMovingAverage()
        self.close_over_sma = self.data.close > self.sma1
        self.close_over_ema = self.data.close > self.ema1
        self.sma_ema_diff = self.sma1 - self.ema1
        # 生成交易信号
        self.buy_sig = bt.And(self.close_over_sma, self.close_over_ema, self.sma_ema_diff > 0)

    # 在 next 中直接调用计算好的指标
    def next(self):
        if self.buy_sig:
            self.buy()

信号

用来通知买和卖,有空头信号、多头信号、空多头信号之分。

Indicator

backtrader在指标计算这块是矢量化计算,不是利用循环来计算,这也比一些框架速度快。三种使用方式:

  • 使用bt.indicators包,也可以简写成bt.ind
  • 使用bt.talib模块
  • 直接使用talib
    你可以使用bt.indicators或talib进行指标计算,bt.indicators并不是基于talib实现的,它自己实现了一遍。基本计算函数在basicops.py中。
  • Indicators are always instantiated during __init__ in the Strategy
  • Indicator values (or values thereof derived) are used/checked during next
  • Any Indicator (or value thereof derived) declared during init will be precalculated before next is called。指示器的值每次调用next()之前都会计算
import talib
import backtrader as bt
## 三种调用方式
### 直接使用talib库
talib.SMA(self.data.close, timeperiod=5)
### 使用bt.talib,因为bt在一个单独的文件talib.py中导入了talib的所有函数
bt.talib.SMA(self.data.close, timeperiod=5)
### 使用bt自己的实现bt.indicators库
bt.indicators.SMA(self.data, period=5)
# 计算布林带
bt.talib.BBANDS(self.data, timeperiod=25)
bt.indicators.BollingerBands(self.data, period=25)

信号策略

也可以直接定义一个信号作为策略:

class TwoAvgLineSignalStrategy(bt.Indicator):
    name = '双均线信号策略'
    lines = ('signal',)  # 声明 signal 线,交易信号放在 signal line 上
    params = dict(
        short_period=5,
        long_period=20)

    def __init__(self):
        self.s_ma = bt.ind.SMA(period=self.p.short_period)
        self.l_ma = bt.ind.SMA(period=self.p.long_period)

        self.macd = btind.MACD()
        # 短期均线上穿长期均线,取值为1;反之,短期均线下穿长期均线,取值为-1
        # self.lines.signal = bt.ind.CrossOver(self.s_ma, self.l_ma)
        self.lines.signal= bt.ind.CrossOver(self.macd.macd, self.macd.signal)

此时,无需实现next()函数,因为我们定义了signal线,它大于0时买进,小于0就卖出

自定义指示器

例如计算N日内上涨天数的指示器可以如下编写:

import backtrader as bt

class NDaysUpIndicator(bt.Indicator):
    lines = ('ndaysup',)
    params = (('period', 5),)

    def __init__(self):
        self.addminperiod(self.params.period)

    def next(self):
        count = 0
        for i in range(self.params.period):
            if self.data.close[-i] > self.data.close[-i-1]:
                count += 1
        self.lines.ndaysup[0] = count

class TestStrategy(bt.Strategy):
    def __init__(self):
        self.ndaysup = NDaysUpIndicator(self.data)

    def next(self):
        print(f'Date: {self.data.datetime.date(0)}, NDaysUp: {self.ndaysup[0]}')

if __name__ == '__main__':
    cerebro = bt.Cerebro()

    data = bt.feeds.YahooFinanceData(dataname='AAPL', fromdate=datetime.datetime(2020, 1, 1), todate=datetime.datetime(2020, 12, 31))
    cerebro.adddata(data)

    cerebro.addstrategy(TestStrategy)

    cerebro.run()

多支股票

多支股票运行时,在初始化Indicator和调用sell()/buy()时,一定要记得传data这个参数。

for code in codes:
    cerebro.adddata(data, name=code)
## 策略里初始化:
# 移动平均线指标 
self.sma={d:bt.ind.SMA(d,period=self.p.period) for d in self.stocks}    
## 然后访问
if sma[code][1] > ...

def __init__(self):
        self.mas = dict()
        #遍历所有股票,计算20日均线
        for data in self.datas:
            self.mas[data._name] = bt.ind.SMA(data.close, period=self.p.period) 

def next(self):
   #计算截面收益率
	rate_list=[]
	for data in self.datas:
	    if len(data)>self.p.look_back_days:
	        p0=data.close[0]
	        pn=data.close[-self.p.look_back_days]
	        rate=(p0-pn)/pn
	        rate_list.append([data._name,rate])
	
	#股票池   
	long_list=[]
	sorted_rate=sorted(rate_list,key=lambda x:x[1],reverse=True)
	long_list=[i[0] for i in sorted_rate[:10]]
	
	# 得到当前的账户价值
	total_value = self.broker.getvalue()
	p_value = total_value*0.9/10
	for data in self.datas:
	    #获取仓位
	    pos = self.getposition(data).size
	    if not pos and data._name in long_list and \
	      self.mas[data._name][0]>data.close[0]:
	        size=int(p_value/100/data.close[0])*100
	        self.buy(data = data, size = size) 
	
	    if pos!=0 and data._name not in long_list or \
	      self.mas[data._name][0]<data.close[0]:
	        self.close(data = data)                        



多策略同时运行

一个cerebro里可以添加多个策略,那这么做是否有必要呢?个人认为完全有必要,因为我们不想把各种不同的逻辑混杂在一个策略里。那多个策略同时对多个股票进行买卖是否会存在冲突呢?如果我们是单核运行,基本上不会,我们可以在broker里进行订单管理,防止重复下单。

Cerebro

收盘作弊模式与开盘作弊模式

收盘作弊模式 (cheat_on_close) ,可以以今日收盘价成交。开盘作弊模式(cheat_on_open),相当于上帝模式(提前观察到开盘价),然后以开盘价成交。
当开启了这两种模式后,整个执行逻辑与默认模式很大不同,所以策略类中覆写的方法不再之前的 prenext、next等,backtrader提供了另一套队形的 xxx_open 方法供用户编写业务逻辑。如:next_open、nextstart_open、prenext_open、next_open等。
在这些 xxx_open 方法中:
1、各种技术指标都还未重新计算,还保留着昨天的结果。如 self.sma[0]是昨天的移动均值
2、经纪行broker还未评估未决订单。

订单

backtrader提供了Order相关的类,但Order的创建和处理流程是交给broker的,broker只需要返回一个Order对象给策略即可。
Order状态的处理也是broker自己完成。也就是说,如果自己要实现实盘交易,就需要自己处理Order的完整流程。

Order 包含 order.created 与 order.executed对象。created与executed的区别在于一个记录原始报单, 一个记录实际成交单, 即: created.price和created.size是不会改变的, executed.price和executed.size是随着实际成交结果而变化的。 size的值是可正可负的。
访问订单基本信息:

order.ref,  # 订单编号
order.executed.price,  # 成交价
order.executed.value,  # 成交额
order.executed.comm,  # 佣金
order.executed.size,  # 成交量
order.data._name,# 股票代码

可以在notify()函数中输出上述基本信息。
notify_order(order):每次订单状态改变会触发回调
notify_trade(trade):任何开仓/更新/平仓交易的通知
notify_cashvalue(cash, value) :通知当前现金和投资组合
notify_store(msg, *args, **kwargs):关于存储的通知
notify_data(self, data, status, *args, **kwargs)::关于数据的通知
notify_timer(self, timer, when, *args, **kwargs):定时器通知,定时器可以通过成员函数add_timer()添加

订单状态值,及流程如下:

  • Order.Created 创建
  • Order.Submitted 提交给broker
  • Order.Accepted broker已接收
  • Order.Partial 订单部分被执行 order.executed查看订单
  • Order.Complete 订单已完成平均价格
  • Order.Rejected 被broker拒绝
  • Order.Margin 保证金不足\没有足够的现金执行订单。
  • Order.Cancelled 用户取消
  • Order.Expired 过期

订单类型

  • Order.Market
    市价单,以当时市场价格成交的订单,不需要自己设定价格。市价单能被快速达成交易,防止踏空,尽快止损/止盈;
    按下一个 Bar (即生成订单的那个交易日的下一个交易日)的开盘价来执行成交;
    例:self.buy(exectype=bt.Order.Market)

  • Order.Close
    和 Order.Market 类似,也是市价单,只是成交价格不一样;
    按下一个 Bar 的收盘价来执行成交;
    例:self.buy(exectype=bt.Order.Close)

  • Order.Limit
    限价单,需要指定成交价格,只有达到指定价格(limit Price)或有更好价格时才会执行,即以指定价或低于指点价买入,以指点价或更高指定价卖出;
    在订单生成后,会通过比较 limit Price 与之后 Bar 的 open\high\low\close 行情数据来判断订单是否成交。
    如果下一个 Bar 的 open 触及到指定价格 limit Price,就以 open 价成交,订单在这个 Bar 的开始阶段就被执行完成;
    如果下一个 Bar 的 open 未触及到指定价格 limit Price,但是 limit Price 位于这个 bar 的价格区间内 (即 low ~ high),就以 limit Price 成交;
    例:self.buy(exectype=bt.Order.Limit, price=price, valid=valid)

  • Order.Stop
    止损单,需要指定止损价格(Stop Price),一旦股价突破止损价格,将会以市价单的方式成交;
    在订单生成后,也是通过比较 Stop Price 与之后 Bar 的 open\high\low\close 行情数据来判断订单是否成交。
    如果下一个 Bar 的 open 触及到指定价格 limit Price,就以 open 价成交;
    如果下一个 Bar 的 open 未触及到指定价格 Stop Price,但是 Stop Price 位于这个 bar 的价格区间内 (即 low ~ high),就以 Stop Price 成交;
    例:self.buy(exectype=bt.Order.Stop, price=price, valid=valid)

  • Order.StopLimit
    止损限价单,需要指定止损价格(Stop price)和限价(Limit Price),一旦股价达到设置的止损价格,将以限价单的方式下单;
    在下一个 Bar,按 Order.Stop 的逻辑触发订单,然后以 Order.Limit 的逻辑执行订单;
    例:self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid, plimit=plimit)

  • Order.StopTrail
    跟踪止损订单,是一种止损价格会自动调整的止损单,调整范围通过设置止损价格和市场价格之间的差价来确定。
    差价即可以用金额 trailamount 表示,也可以用市价的百分比 trailpercent 表示;
    如果是通过 buy 下达了买入指令,就会“卖出”一个跟踪止损单,在市场价格上升时,止损价格会随之上升;
    若股价触及止损价格时,会以市价单的形式执行订单;若市场价格下降或保持不变,止损价格会保持不变;
    如果是通过 sell 下达卖出指令,就会“买入”一个跟踪止损单,在市场价格下降时,止损价格会随之下降;
    若股价触及止损价格时,会以市价单的形式执行订单;但是当市场价格上升时,止损价格会保持不变;
    例:self.buy(exectype=bt.Order.StopTrail, price=xxx, trailamount=xxx)

  • Order.StopTrailLimit
    跟踪止损限价单,是一种止损价格会自动调整的止损限价单,订单中的限价 Limit Price 不会发生变动,
    止损价会发生变动,变动逻辑与上面介绍的跟踪止损订单一致;
    例:self.buy(exectype=bt.Order.StopTrailLimit, plimit=xxx, trailamount=xxx)

Trade

一个交易可能由多个订单组成。

  • 仓位从0变为正值(100),系统打开一个交易,会触发notify_trade方法
  • 仓位从100变为0,关闭交易,触发notify_trade
    不能做空的情况下,self.close()命令会一次性市价卖出全部仓位,关闭交易。在卖空操作中,self.close()默认会市价买回所有卖空仓位,关闭交易。

当前持仓

采用如下方式获得持仓信息:

print('当前可用资金', self.broker.getcash())
print('当前总资产', self.broker.getvalue())
print('当前持仓量', self.broker.getposition(self.data).size)
print('当前持仓成本', self.broker.getposition(self.data).price)
 # 也可以直接获取持仓
print('当前持仓量', self.getposition(self.data).size)
print('当前持仓成本', self.getposition(self.data).price)

扫地僧给力系列

他推出了一个概念:策略迭代表。我以为是什么新东西,看了他的解释后,哑然失笑了。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北极象

如果觉得对您有帮助,鼓励一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值