在 backtrader 内部运行的策略主要处理数据 和 指针。
数据被添加到Cerebro 实例中,并最终成为策略输入的一部分(解析并用作实例的属性),而指针由策略本身声明和管理。
到目前为止,所有 backtrader 样图都有3件事似乎是理所当然的,因为它们没有在任何地方声明:
-
现金和价值(经纪人中的钱发生了什么)
-
交易(又名运营)
-
买入/卖出订单
它们存在于Observers
子模块 backtrader.observers
中。它们之所以存在,是因为 Cerebro 支持一个参数来自动添加(或不添加)它们到策略中:
stdstats
(默认值: True)
如果遵循默认值,Cerebro将运行以下等效的用户代码:
import backtrader as bt ... cerebro = bt.Cerebro() # default kwarg: stdstats=True cerebro.addobserver(backtrader.observers.Broker) cerebro.addobserver(backtrader.observers.Trades) cerebro.addobserver(backtrader.observers.BuySell)
让我们看看通常的图表,其中包含这3个默认 observers (即使没有发出订单,因此没有交易发生,现金和投资组合价值也没有变化)
from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt import backtrader.feeds as btfeeds if __name__ == '__main__': cerebro = bt.Cerebro(stdstats=False) cerebro.addstrategy(bt.Strategy) data = bt.feeds.BacktraderCSVData(dataname='../datas/2006-day-001.txt') cerebro.adddata(data) cerebro.run() cerebro.plot()
现在,让我们在创建Cerebro实例时将 False
的值stdstats
更改为 (也可以在调用run
时完成):
cerebro = bt.Cerebro(stdstats=False)
现在的图表不同了。
访问 Observers
如上所述, Observers 在默认情况下已经存在,并且收集可用于统计目的的信息,这就是为什么可以通过策略的一个属性来完成 observers 的原因,称为:
stats
它只是一个占比特。如果我们还记得上面列出的添加缺省Observers 之一:
... cerebro.addobserver(backtrader.observers.Broker) ...
显而易见的问题是如何访问Broker
observer。例如,如何从策略的方法完成next
此操作:
class MyStrategy(bt.Strategy): def next(self): if self.stats.broker.value[0] < 1000.0: print('WHITE FLAG ... I LOST TOO MUCH') elif self.stats.broker.value[0] > 10000000.0: print('TIME FOR THE VIRGIN ISLANDS ....!!!')
Broker
observer就像数据一样,指针和策略本身也是一个Lines
对象。在本例中,Broker
有2个lines:
-
cash
-
value
Observer 实施
实现与指针非常相似:
class Broker(Observer): alias = ('CashValue',) lines = ('cash', 'value') plotinfo = dict(plot=True, subplot=True) def next(self): self.lines.cash[0] = self._owner.broker.getcash() self.lines.value[0] = value = self._owner.broker.getvalue()
步骤:
-
派生自
Observer
(而不是从Indicator
) -
根据需要声明 lines 和参数(
Broker
有 2 个 lines 但没有参数) -
将有一个自动属性
_owner
,即持有 observer
Observers 行动:
-
在计算完所有指针后
-
在运行策略
next
方法后 -
这意味着:在周期结束时...他们观察 发生了什么
在这种情况下,Broker
它只是盲目地记录经纪人在每个时间点的现金和投资组合价值。
向策略添加Observers
如上所述,Cerebro 正在使用该 stdstats
参数来决定是否添加3个缺省 Observers,从而减轻最终用户的工作。
在混合物中添加其他 Observers 是可能的,无论是沿着stdstats
还是删除它们。
让我们采用通常的策略,当价格高于aSimpleMovingAverage
时close
买入,如果相反true,则卖出。
通过一个「添加」:
- DrawDown是生态系统中
backtrader
已经存在observer
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import os.path import time import sys import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class MyStrategy(bt.Strategy): params = (('smaperiod', 15),) def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self):
视觉输出显示回撤的演变
与部分文本输出:
... 2006-12-14T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-15T23:59:59+00:00, DrawDown: 0.22 2006-12-15T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-18T23:59:59+00:00, DrawDown: 0.00 2006-12-18T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-19T23:59:59+00:00, DrawDown: 0.00 2006-12-19T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-20T23:59:59+00:00, DrawDown: 0.10 2006-12-20T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-21T23:59:59+00:00, DrawDown: 0.39 2006-12-21T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-22T23:59:59+00:00, DrawDown: 0.21 2006-12-22T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-27T23:59:59+00:00, DrawDown: 0.28 2006-12-27T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-28T23:59:59+00:00, DrawDown: 0.65 2006-12-28T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-29T23:59:59+00:00, DrawDown: 0.06 2006-12-29T23:59:59+00:00, MaxDrawDown: 2.62
注意
从文本输出和代码中可以看出,DrawDown
observer实际上有2个lines:
-
drawdown
-
maxdrawdown
选择不是绘制maxdrawdown
line,而是使其仍然可供用户使用。
实际上, last 值maxdrawdown
也可以在直接属性(不是 line)中使用,其名称为 maxdd
开发 Observers
observer的Broker
实施情况如上所示。要生成有意义的observer,实现可以使用以下信息:
-
self._owner
是当前正在运行的策略因此,策略中的任何内容都可供 observer
-
原则中可用的缺省内部内容可能很有用:
broker
->属性,用于访问策略创建订单的代理实例
如 所示
Broker
,现金和投资组合价值是通过调用方法getcash
和getvalue
_orderspending
->列出由策略创建的订单,并且经纪人已向策略通知事件。
BuySell
observer遍历清单,查找已运行(全部或部分)的订单,以创建给定时间点(索引 0)的平均运行价格_tradespending
->交易清单(一组已完成的买入/卖出或卖出/买入对),该列表是根据买入/卖出订单编译的
Observer显然可以通过路径访问其他observersself._owner.stats
。
自订订单观察器
标准BuySell
observer只关心已运行的操作。我们可以创建一个observer显示订单的创建时间和到期时间。
为了提高可见性,显示不会沿价格绘制,而是在单独的轴上绘制。
from __future__ import (absolute_import, division, print_function, unicode_literals) import math import backtrader as bt class OrderObserver(bt.observer.Observer): lines = ('created', 'expired',) plotinfo = dict(plot=True, subplot=True, plotlinelabels=True) plotlines = dict( created=dict(marker='*', markersize=8.0, color='lime', fillstyle='full'), expired=dict(marker='s', markersize=8.0, color='red', fillstyle='full') ) def next(self): for order in self._owner._orderspending: if order.data is not self.data: continue if not order.isbuy(): continue # Only interested in "buy" orders, because the sell orders # in the strategy are Market orders and will be immediately # executed if order.status in [bt.Order.Accepted, bt.Order.Submitted]: self.lines.created[0] = order.created.price elif order.status in [bt.Order.Expired]: self.lines.expired[0] = order.created.price
自定义 observer 只关心买入 订单,因为这是一个只购买以试图获利的分层。卖出订单是市价订单,将立即运行。
Close-SMA 交叉策略更改为:
-
创建一个限价订单,其价格低于信号时刻 close 价格的1.0%
-
订单有效期为 7(日历)天
生成的图表。
在新子图表(红色方块)中可以看到,有几个订单已经过期,我们也可以理解,在“创建”和“运行”之间恰好有几天。
注意
从分支中的development
提交1560fa8802 开始,如果在创建订单时未设置价格,则收盘价将用作参考价格。
这对市场订单没有影响,但始终保持order.create.price
可用,并简化了 buy
最后,该策略的代码应用了新的observer
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind from orderobserver import OrderObserver class MyStrategy(bt.Strategy): params = ( ('smaperiod', 15), ('limitperc', 1.0), ('valid', 7), ) def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print('%s, %s' % (dt.isoformat(), txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt) self.order = order return if order.status in [order.Expired]: self.log('BUY EXPIRED') elif order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) # Sentinel to None: new orders allowed self.order = None def __init__(self): # SimpleMovingAverage on main data # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod) sma = btind.SMA(period=self.p.smaperiod) # CrossOver (1: up, -1: down) close / sma self.buysell = btind.CrossOver(self.data.close, sma, plot=True) # Sentinel to None: new ordersa allowed self.order = None def next(self): if self.order: # pending order ... do nothing return # Check if we are in the market if self.position: if self.buysell < 0: self.log('SELL CREATE, %.2f' % self.data.close[0]) self.sell() elif self.buysell > 0: plimit = self.data.close[0] * (1.0 - self.p.limitperc / 100.0) valid = self.data.datetime.date(0) + \ datetime.timedelta(days=self.p.valid) self.log('BUY CREATE, %.2f' % plimit) self.buy(exectype=bt.Order.Limit, price=plimit, valid=valid) def runstrat(): cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname='../datas/2006-day-001.txt') cerebro.adddata(data) cerebro.addobserver(OrderObserver) cerebro.addstrategy(MyStrategy) cerebro.run() cerebro.plot() if __name__ == '__main__': runstrat()
保存/保留统计信息
截至目前backtrader
,尚未实施任何机制来跟踪 observers 的值将其存储到档中。最好的方法:
-
在策略的方法中
start
Open档 -
在策略的方法中
next
写下值
考虑到DrawDown
observer,可以这样完成:
class MyStrategy(bt.Strategy): def start(self): self.mystats = open('mystats.csv', 'wb') self.mystats.write('datetime,drawdown, maxdrawdown\n') def next(self): self.mystats.write(self.data.datetime.date(0).strftime('%Y-%m-%d')) self.mystats.write(',%.2f' % self.stats.drawdown.drawdown[-1]) self.mystats.write(',%.2f' % self.stats.drawdown.maxdrawdown-1]) self.mystats.write('\n')
要保存索引 0 的值,一旦处理完所有 observers ,就可以添加一个自定义 observer ,该自定义可以作为 last observer 添加到系统中,以将值保存到 csv 档中。