Backtrader中文笔记_Quickstart(二次修复)[快速开始]。

Note

The data files used in the quickstart guide are updated from time to time, which means that the adjusted close changes and with it the close (and the other components). That means that the actual output may be different to what was put in the documentation at the time of writing.

注意:

由于调整收盘价可能发生变化,所以后续示例代码的运行结果可能与本教程的输出不一致

 Using the platform

使用平台

Let’s run through a series of examples (from almost an empty one to a fully fledged strategy) but not without before roughly explaining 2 basic concepts when working with backtrader

让我们通过一系列示例(从一个几乎是空空如也的战略变成一个全面成熟的战略)。但是在使用backtrader之前我将讲两个基本概念。

1、Lines 航线(数据线)

Data Feeds, Indicators and Strategies have lines.

数据源,指标,策略都有lines属性

策略对象的lines对象里面就只有一个时间线。

A line is a succession of points that when joined together form this line. When talking about the markets, a Data Feed has usually the following set of points per day:

一条线是由一连串的点组成的,这些点联合在一起就变成了这条线。当我们在讨论市场的时候,每天的数据反馈的点通常由以下的几点:

Open(开盘价),High(最高价),Low(最低价),Close(收盘价),Volume(成交量),OpenInterset(未平仓量)

 The series of “Open”s along time is a Line. And therefore a Data Feed has usually 6 lines.

这一系列的“Open”沿着时间称为一条线,因此数据反馈通常有6条线

If we also consider “DateTime” (which is the actual reference for a single point), we could count 7 lines.

如果我们也考虑"时间"(他是单个点的实际引用),我们也可以统计7行

Index 0 Approach

2、索引0的方法

When accessing the values in a line, the current value is accessed with index: 0

当访问线上的值,可以使用索引0访问当前的值。

And the “last” output value is accessed with -1. This in line with Python conventions for iterables (and a line can be iterated and is therefore an iterable) where index -1 is used to access the “last” item of the iterable/array.

当访问最后的一次输出值,可以用-1.这符合Python对可迭代对象的定义(一条线的数据能被迭代,因此它是可迭代的)当用索引-1时就是访问可迭代对象/数组的最后一个元素

In our case is the last output value what’s getting accessed.

在我们的例子中,访问的是最后一次输出的值

As such and being index 0 right after -1, it is used to access the current moment in line.

因此,索引0的数据在索引-1之后,它用于对线上当前时刻的数据访问。

With that in mind and if we imagine a Strategy featuring a Simple Moving average created during initialization:

考虑到这一点,如果我们设想一个策略,在初始化时创建一个简单的移动平均线:

self.sma = SimpleMovingAverage(.....)

The easiest and simplest way to access the current value of this moving average:

最简单与方便的访问当前移动平均值的方法

av = self.sma[0]  

There is no need to know how many bars/minutes/days/months have been processed, because “0” uniquely identifies the current instant.

我们不需要知道已经处理了多少个bars/分/天/月,因为0独特的标识了当前的既可状态.

Following pythonic tradition, the “last” output value is accessed using -1:

按照Python的惯例,访问最后一次输出的值可以使用-1:

previous_value = self.sma[-1]  

Of course earlier output values can be accessed with -2, -3, …

当然更早的输出值可以使用-2,-3.

 

From 0 to 100: the samples

从0开始:样本

Let’s get running.

让我们跑起来

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt

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

    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()  

After the execution the output is:

运行结束后的输出:

Starting Portfolio Value: 10000.00
Final Portfolio Value: 10000.00  

In this example:

在这个示例:

  • backtrader was imported

  • backtrader被导入
  • The Cerebro engine was instantiated

  • Cerebro被实例化
  • The resulting cerebro instance was told to run (loop over data)

  • 生成的cerebro的实例运行run方法(循环处理数据)

  • And the resulting outcome was printed out

  • 结果被输出

Although it doesn’t seem much, let’s point out something explicitly shown:

虽然看起来输出不多,让我们指出一些明确显示的东西:

  • The Cerebro engine has created a broker instance in the background

  • 在后台Cerebro引擎将创建经纪人(broker)实例
  • The instance already has some cash to start with

  • 实例一开始就拥有一些现金

 

This behind the scenes broker instantiation is a constant trait in the platform to simplify the life of the user. If no broker is set by the user, a default one is put in place.

这种后台实现经纪人实例是平台中常规的特征,它简化了用户的使用。如果用户没有设置经纪人,平台会默认的给予设置。

And 10K monetary units is a usual value with some brokers to begin with.

1万货币单位是经纪人通常的价格,或初始价格。

 

 

Setting the Cash

设置现金
In the world of finance, for sure only “losers” start with 10k. Let’s change the cash and run the example again.

在金融世界里,肯定只有“失败者”才会从10000美元开始。让我们更改现金,再运行一遍这个例子。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt

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

    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())  

After the execution the output is: 

运行以后的输出:

Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00

Mission accomplished. Let’s move to tempestuous waters.

任务完成,让我们来点更加猛料。

 

Adding a Data Feed

添加数据源

Having cash is fun, but the purpose behind all this is to let an automated strategy multiply the cash without moving a finger by operating on an asset which we see as a Data Feed

拥有现金很快乐,但这一切背后的目的是让一种自动策略在不动手指的情况下,通过操作我们视之为数据源,使资产、现金成倍增长

Ergo … No Data Feed -> No Fun. Let’s add one to the ever growing example.

因此... 没有数据源没意思, 让我们在不断增加的示例中添加一个

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
# 获取运行脚本的绝对路径,用于拼接数据的路径 modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values after this date todate=datetime.datetime(2000, 12, 31), reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

After the execution the output is:

运行输出:

Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00

The amount of boilerplate has grown slightly, because we added:

样板代码量有所增加,因为我们增加了:

  • Finding out where our example script is to be able to locate the sample Data Feed file

  • 找出我们的示例脚本可以定位示例数据源文件的位置
  • Having datetime objects to filter on which data from the Data Feed we will be operating

  • 让datetime对象筛选我们将要操作的数据源中的哪些数据

Aside from that, the Data Feed is created and added to cerebro.

除此之外,数据Feed被创建并添加到cerebro中。

The output has not changed and it would be a miracle if it had.

输出没有改变,如果改变了,那就是奇迹了。

Note

Yahoo Online sends the CSV data in date descending order, which is not the standard convention. The reversed=True prameter takes into account that the CSV data in the file has already been reversed and has the standard expected date ascending order.

注意

feed里面有一个参数reversed,如果按照升序的情况,不需要设置为True,如果数据反转的话,需要设置为True

本来经过测试,实际反转的情况输出会少一条行数据。

 

Our First Strategy

我们的第一个策略

The cash is in the broker and the Data Feed is there. It seems like risky business is just around the corner.

现金在经纪人那里,数据源也在这里了,危险的买卖就在眼前了。

Let’s put a Strategy into the equation and print the “Close” price of each day (bar).

让我们把一个策略放入公式,并输出每天的收盘价

DataSeries (the underlying class in Data Feeds) objects have aliases to access the well known OHLC (Open High Low Close) daily values. This should ease up the creation of our printing logic.

DataSeries(数据提要中的基础类)对象具有别名来访问已知的OHLC(开盘价,最高价,最低价,收盘价)的值。

这将简化输出逻辑的创建。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        ''' Logging function for this strategy'''
# 用了默认参数并用了短路原则,如果没有输入参数,就输出数据的日期,一个函数多用
# 通过数据对象的属性方式取出线对象 dt = dt or self.datas[0].datetime.date(0)
      # 生成ISO 8601日期 print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries
# 一般情况下,通过self.datas[0]选出数据线集合的对象,通过属性选定指定的数据线,就是lines里面的一个对象 self.dataclose = self.datas[0].close def next(self): # Simply log the closing price of the series from the reference
# 这里将执行每个数据点位状态的输出,所以具体的逻辑后续也在这里写 self.log('Close, %.2f' % self.dataclose[0]) if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())  

After the execution the output is:

执行后的输出:

Starting Portfolio Value: 100000.00
2000-01-03T00:00:00, Close, 27.85
2000-01-04T00:00:00, Close, 25.39
2000-01-05T00:00:00, Close, 24.05
...
...
...
2000-12-26T00:00:00, Close, 29.17
2000-12-27T00:00:00, Close, 28.94
2000-12-28T00:00:00, Close, 29.29
2000-12-29T00:00:00, Close, 27.41
Final Portfolio Value: 100000.00  

 Someone said the stockmarket was risky business, but it doesn’t seem so.

有人说股市有风险,但看上去并不是这样

Let’s explain some of the magic:

让我们来解释一下他的魔力:

Upon init being called the strategy already has a list of datas that are present in the platform

在调用__init__时,策略已经在平台中有一个数据列表

This is a standard Python list and datas can be accessed in the order they were inserted.

这是一个标准的Python列表,可以按照插入的顺序访问数据。

The first data in the list self.datas[0] is the default data for trading operations and to keep all strategy elements synchronized (it’s the system clock)

列表中的第一个数据self.datas[0] 是交易操作的默认数据,用于保持所有策略元素同步(它是系统时钟)

self.dataclose = self.datas[0].close keeps a reference to the close line. Only one level of indirection is later needed to access the close values.

self.dataclose = self.datas[0].close将保持对close线的引用,稍后只需要一个级别的间接寻址来访问close值。

The strategy next method will be called on each bar of the system clock (self.datas[0]). This is true until other things come into play like indicators, which need some bars to start producing an output. More on that later.

策略的next方法将在系统时钟的每一个bar上面调用。这是真的,知道另外的东西出来,比如指标(indicators),它需要一些bar开始生产,稍后详细介绍。

 

Adding some Logic to the Strategy

给策略添加一些逻辑

Let’s try some crazy idea we had by looking at some charts

我们通过看一些图标,尝试一些疯狂的想法

If the price has been falling 3 sessions in a row … BUY BUY BUY!!!

如果价格已经连续下跌3个交易日。。。买买买

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])
     # 现在的收盘价低于上一个点的收盘价
        if self.dataclose[0] < self.dataclose[-1]:
            # current close less than previous close
        # 上一个收盘价,低于上上一个收盘价,逻辑成立
            if self.dataclose[-1] < self.dataclose[-2]:
                # previous close less than the previous close

                # BUY, BUY, BUY!!! (with all possible default parameters)
          # 输出成交信息 self.log('BUY CREATE, %.2f' % self.dataclose[0])
# 买买买 self.buy() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())  

运行以后的输出

Starting Portfolio Value: 100000.00
2000-01-03, Close, 27.85
2000-01-04, Close, 25.39
2000-01-05, Close, 24.05
2000-01-05, BUY CREATE, 24.05
2000-01-06, Close, 22.63
2000-01-06, BUY CREATE, 22.63
2000-01-07, Close, 24.37
...
...
...
2000-12-20, BUY CREATE, 26.88
2000-12-21, Close, 27.82
2000-12-22, Close, 30.06
2000-12-26, Close, 29.17
2000-12-27, Close, 28.94
2000-12-27, BUY CREATE, 28.94
2000-12-28, Close, 29.29
2000-12-29, Close, 27.41
Final Portfolio Value: 99725.08

Several “BUY” creation orders were issued, our porftolio value was decremented. A couple of important things are clearly missing.

我们发布了几个买的订单,我们的金钱下降了。一些重要的信息明显被遗漏了。

The order was created but it is unknown if it was executed, when and at what price.

订单已经被创建,但不知道何时执行,操作的时候的价格。

The next example will build upon that by listening to notifications of order status.

下一个示例将在此基础上监听订单状态的通知。

The curious reader may ask how many shares are being bought, what asset is being bought and how are orders being executed. Where possible (and in this case it is) the platform fills in the gaps:

好奇的读者可能会问,购买了多少股票,正在购买什么资产,指令是如何执行的。在可能的情况下(在本例中)平台将弥补这些空白:

self.datas[0] (the main data aka system clock) is the target asset if no other one is specified

如果没有指定其他数据,self.data[0](主数据既系统时钟)是目标资产.

The stake is provided behind the scenes by a position sizer which uses a fixed stake, being the default “1”. It will be modified in a later example

交易的股份默认使用的是1,也就是每次交易1份股票。它将在后面进行修改。

The order is executed “At Market”. The broker (shown in previous examples) executes this using the opening price of the next bar, because that’s the 1st tick after the current under examination bar.

命令以"市场"的方式执行。经济人(如前面的示例所示)使用将执行下一个bar的开盘价来操作,因为这是检查满足条件bar以后的第一个刻度(理解为触发满足条件后的第一个数据)

The order is executed so far without any commission (more on that later)

订单的交易没有任何的佣金(后续完善)

 

Do not only buy … but SELL

不要一直买, 也要卖

 After knowing how to enter the market (long), an “exit concept” is needed and also understanding whether the strategy is in the market.

在知道如何进入市场(long)之后,需要一个"退出概念",并知道这个策略是否在市场

  • Luckily a Strategy object offers access to a position attribute for the default data feed

  • 幸运的是,一个策略对象提供了对默认数据提要的位置属性的访问
  • Methods buy and sell return the created (not yet executed) order

  • 方法buy和sell返回创建的(尚未执行的)订单
  • Changes in orders’ status will be notified to the strategy via a notify method

  • 订单状态的变化将通过notify方法通知策略

 

The “exit concept” will be an easy one:

退出将是一个非常简单的逻辑

Exit after 5 bars (on the 6th bar) have elapsed for good or for worse

无论好坏,在5个bar之后(第6个bar)

Please notice that there is no “time” or “timeframe” implied: number of bars. The bars can represent 1 minute, 1 hour, 1 day, 1 week or any other time period.

请注意,这里没有提起“时间”或“时间框架”:条的数量。条形图可以表示1分钟、1小时、1天、1周或任何其他时间段。

Although we know the data source is a daily one, the strategy makes no assumption about that.

虽然我们知道数据源是日常数据源,但该策略没有对此做任何假设。

Additionally and to simplify:

此外,为了简化:

  • Do only allow a Buy order if not yet in the market

买单只有在没有持仓的情况下才交易。

Note

The next method gets no “bar index” passed and therefore it seems obscure how to understand when 5 bars may have elapsed, but this has been modeled in pythonic way: call len on an object and it will tell you the length of its lines. Just write down (save in a variable) at which length in an operation took place and see if the current length is 5 bars away.

注意

next方法没有传递“bar索引”,因此它似乎难以理解什么时候5个条会消失,但这是用python的方式建模的:在一个对象上调用len,它会告诉你它的线的长度。只需写下(保存在一个变量中)操作发生的长度,看看当前长度是否在5条之外。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].open

        # To keep track of pending orders
        self.order = None
  
  
  # 监控订单情况 def notify_order(self, order):
# 如果订单在提交或者允许的情况下,直接返回 if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash
     # 如果下单完成了 if order.status in [order.Completed]: if order.isbuy(): self.log('BUY EXECUTED, %.2f' % order.executed.price) elif order.issell(): self.log('SELL EXECUTED, %.2f' % order.executed.price)        # 记录当前bar的状态,并复制给自身属性bar_executed self.bar_executed = len(self)      # 如果下单的情况在另外状态 elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') # Write down: no pending order
# 将订单状态重置为None self.order = None def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one if self.order: return # Check if we are in the market
# 空仓的状态下,可以去判断是否可以去买 if not self.position: # Not yet ... we MIGHT BUY if ... if self.dataclose[0] < self.dataclose[-1]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: # Already in the market ... we might sell
      # 持仓的状态下判断当前的bar状态点,超过5日进行卖出 if len(self) >= (self.bar_executed + 5): # SELL, SELL, SELL!!! (with all possible default parameters) self.log('SELL CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.sell() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(1995, 1, 1), # Do not pass values before this date todate=datetime.datetime(1995, 3, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

After the execution the output is:  

输出

Starting Portfolio Value: 100000.00
2000-01-03, Close, 26.27
2000-01-04, Close, 23.95
2000-01-05, Close, 22.68
2000-01-05, BUY CREATE, 22.68
2000-01-06, BUY EXECUTED, 22.27
...
 
2000-12-26, Close, 27.52
2000-12-27, Close, 27.30
2000-12-28, Close, 27.63
2000-12-29, Close, 25.85
2000-12-29, SELL CREATE, 25.85
Final Portfolio Value: 100017.52

Blistering Barnacles!!! The system made money … something must be wrong

我X,这个系统赚钱了…一定出了什么问题

 

The broker says: Show me the money!

经济人说:"给我钞票"

And the money is called “commission”.

这笔钱就是所谓的"佣金"

Let’s add a reasonable 0.1% commision rate per operation (both for buying and selling … yes the broker is avid …)

让我们给每次操作设立一笔佣金千分之一0.1%(买进卖出都要,这个券商有点好)

A single line will suffice for it:

一行代码就够了。

 # 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)

Being experienced with the platform we want to see the profit or loss after a buy/sell cycle, with and without commission.

作为一个经验丰富的平台,我们想知道每次交易买进,卖出的利润,包括包含手续费与没有手续费的情况

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):

    # 这是一个输出日志的小助手。
    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

    # 这是监控下单情况的方法。
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if 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))
                # 赋值两个价格,一个是买入价,一个是买入的时候手续费
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell Price为
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None
# 监控交易周期是否完成 def notify_trade(self, trade): # 正在交易返回空 if not trade.isclosed: return # 一堆交易结束,输出信息,第一个为毛利润,第二个为净利润(扣除手续费) self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one # 是否用定单还有未完成的,有的话,不生成新的下单命令 if self.order: return # Check if we are in the market if not self.position: # Not yet ... we MIGHT BUY if ... if self.dataclose[0] < self.dataclose[-1]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: # Already in the market ... we might sell if len(self) >= (self.bar_executed + 5): # SELL, SELL, SELL!!! (with all possible default parameters) self.log('SELL CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.sell() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Set the commission - 0.1% ... divide by 100 to remove the % cerebro.broker.setcommission(commission=0.001) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

After the execution the output is:

执行完成后的输出:

Starting Portfolio Value: 100000.00
2000-01-03, Close, 26.27
2000-01-04, Close, 23.95
2000-01-05, Close, 22.68
2000-01-05, BUY CREATE, 22.68
2000-01-06, BUY EXECUTED, Price: 22.27, Cost: 22.27, Comm 0.02
2000-01-06, Close, 21.35
2000-01-07, Close, 22.99
...
2000-12-21, BUY EXECUTED, Price: 24.74, Cost: 24.74, Comm 0.02
2000-12-21, Close, 26.24
2000-12-22, Close, 28.35
2000-12-26, Close, 27.52
2000-12-27, Close, 27.30
2000-12-28, Close, 27.63
2000-12-29, Close, 25.85
2000-12-29, SELL CREATE, 25.85
Final Portfolio Value: 100016.06

God Save the Queen!!! The system still made money.

上帝保佑女王!!这个系统仍然在赚钱。

Before moving on, let’s notice something by filtering the “OPERATION PROFIT” lines:

在继续之前,让我们通过过滤“运营利润”来注意一些事情(只留下每对交易的盈亏数据):

Starting Portfolio Value: 100000.00
2000-01-14, OPERATION PROFIT, GROSS 1.97, NET 1.92
2000-02-07, OPERATION PROFIT, GROSS 3.48, NET 3.43
2000-02-28, OPERATION PROFIT, GROSS 4.23, NET 4.17
2000-03-13, OPERATION PROFIT, GROSS 3.28, NET 3.21
2000-03-22, OPERATION PROFIT, GROSS -0.39, NET -0.46
2000-04-07, OPERATION PROFIT, GROSS 2.31, NET 2.24
2000-04-20, OPERATION PROFIT, GROSS -1.83, NET -1.90
2000-05-02, OPERATION PROFIT, GROSS 5.15, NET 5.08
2000-05-11, OPERATION PROFIT, GROSS -3.53, NET -3.59
2000-05-30, OPERATION PROFIT, GROSS -1.39, NET -1.45
2000-07-05, OPERATION PROFIT, GROSS -1.53, NET -1.60
2000-07-14, OPERATION PROFIT, GROSS 1.97, NET 1.90
2000-07-28, OPERATION PROFIT, GROSS 0.14, NET 0.07
2000-08-08, OPERATION PROFIT, GROSS 4.11, NET 4.04
2000-08-21, OPERATION PROFIT, GROSS 0.97, NET 0.90
2000-09-15, OPERATION PROFIT, GROSS -4.00, NET -4.08
2000-09-27, OPERATION PROFIT, GROSS 1.22, NET 1.15
2000-10-13, OPERATION PROFIT, GROSS -2.81, NET -2.87
2000-10-26, OPERATION PROFIT, GROSS 2.84, NET 2.78
2000-11-06, OPERATION PROFIT, GROSS -3.39, NET -3.45
2000-11-16, OPERATION PROFIT, GROSS 1.22, NET 1.17
2000-12-01, OPERATION PROFIT, GROSS 2.45, NET 2.41
2000-12-18, OPERATION PROFIT, GROSS -0.06, NET -0.11
Final Portfolio Value: 100016.06

Adding up the “NET” profits the final figure is:

所有的净利润之和为

14.97

But the system said the following at the end:

但是系统的最后说了

2000-12-29T00:00:00, SELL CREATE, 27.41
Final Portfolio Value: 100016.98

 And obviously 15.83 is not 16.98. There is no error whatsoever. The “NET” profit of 15.83 is already cash in the bag. 

显然15.83不是16.98。没有任何错误。15.83的“净”利润已经是囊中之物了。(本人运行输出为14.97与16.06)

Unfortunately (or fortunately to better understand the platform) there is an open position on the last day of the Data Feed. Even if a SELL operation has been sent … IT HAS NOT YET BEEN EXECUTED.

不幸的是(或幸运的是,更好地理解这个平台),在最后一天的数据Feed有一个开放的定位(满足BUY的条件)。即使SELL操作已经被发送,它也还没有被执行。(根据我手工的计算,按照最后截止日的收盘价计算的,实际该订单应该在第二天开盘买入)

The “Final Portfolio Value” calculated by the broker takes into account the “Close” price on 2000-12-29. The actual execution price would have been set on the next trading day which happened to be 2001-01-02. Extending the Data Feed” to take into account this day the output is:

由经纪人计算的“最终投资组合价值”参考了2000年12月29日的“收盘”价格。实际执行价格将在下一个交易日确定也就是2001-01-02。扩展feed数据考虑到这一天的输出是:

2001-01-02T00:00:00, SELL EXECUTED, Price: 27.87, Cost: 27.87, Commission 0.03
2001-01-02T00:00:00, OPERATION PROFIT, GROSS 1.64, NET 1.59
2001-01-02T00:00:00, Close, 24.87
2001-01-02T00:00:00, BUY CREATE, 24.87
Final Portfolio Value: 100017.41

Now adding the previous NET profit to the completed operation’s net profit:

现将之前的净利润与最后一笔已完成的业务净利润相加:

15.83 + 1.59 = 17.42

Which (discarding rounding errors in the “print” statements) is the extra Portfolio above the initial 100000 monetary units the strategy started with.

其中(剔除“打印”报表中的舍入误差)是策略开始时超过最初10万个货币单位的额外投资组合。

 

Customizing the Strategy: Parameters

定制策略:参数

It would a bit unpractical to hardcode some of the values in the strategy and have no chance to change them easily. Parameters come in handy to help.

在策略中一些固定值是不常用的,并且没有机会轻易地更改它们。参数可以派上用场。

Definition of parameters is easy and looks like:

 定义参数很简单,就像下面这样:

params = (('myparam', 27), ('exitbars', 5),)

Being this a standard Python tuple with some tuples inside it, the following may look more appealling to some:

这是一个标准的Python元组,里面有一些元组,下面的看起来可能更受一些人欢迎:

params = (
    ('myparam', 27),
    ('exitbars', 5),
)

With either formatting parametrization of the strategy is allowed when adding the strategy to the Cerebro engine: 

另外一种添加参数的方式,在将策略添加到Cerebro引擎时,允许对策略进行格式化参数化:

# Add a strategy
cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)

Note

The setsizing method below is deprecated. This content is kept here for anyone looking at old samples of the sources. The sources have been update to use:

下面的setsizing方法已弃用。此内容保存在这里,供任何查看源代码的旧示例的人使用。已更新源以使用:

cerebro.addsizer(bt.sizers.FixedSize, stake=10)``

请阅读关于sizers的部分

Using the parameters in the strategy is easy, as they are stored in a “params” attribute. If we for example want to set the stake fix, we can pass the stake parameter to the position sizer like this durint init:

在策略里面使用定义的参数很简单,它将参数保存在对象的"params"属性里面。让我们做个示例,来修复下单的数量,我们可以在init初始化过程中,通过参数读取具体的值

下面这个已经被弃用了

# Set the sizer stake from the params
self.sizer.setsizing(self.params.stake)

We could have also called buy and sell with a stake parameter and self.params.stake as the value.

当我们要使用买和卖的时候,也可以通过参数self.paramd.stake来使用。

The logic to exit gets modified:

 退出逻辑通过内置参数的调用进行修改:

# Already in the market ... we might sell
if len(self) >= (self.bar_executed + self.params.exitbars):

With all this in mind the example evolves to look like: 

所有一点点意见演变成现在的样子

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):
    params = (
        ('exitbars', 5),
        ('stake', 10),
    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None


    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if 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))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # Check if we are in the market
        if not self.position:

            # Not yet ... we MIGHT BUY if ...
            if self.dataclose[0] < self.dataclose[-1]:
                    # current close less than previous close

                    if self.dataclose[-1] < self.dataclose[-2]:
                        # previous close less than the previous close

                        # BUY, BUY, BUY!!! (with default parameters)
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])

                        # Keep track of the created order to avoid a 2nd order
                        self.order = self.buy()

        else:

            # Already in the market ... we might sell
            if len(self) >= (self.bar_executed + self.params.exitbars):
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    cerebro.addstrategy(TestStrategy)

    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')

    # Create a Data Feed
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # Do not pass values before this date
        fromdate=datetime.datetime(2000, 1, 1),
        # Do not pass values before this date
        todate=datetime.datetime(2000, 12, 31),
        # Do not pass values after this date
        reverse=False)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Set our desired cash start
    cerebro.broker.setcash(100000.0)

    # Add a FixedSize sizer according to the stake
    # 通过在实例中添加每次交易笔数,书中说这种快被淘汰了
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # Set the commission - 0.1% ... divide by 100 to remove the %
    cerebro.broker.setcommission(commission=0.001)

    # Print out the starting conditions
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # Run over everything
    cerebro.run()

    # Print out the final result
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

After the execution the output is:

执行输出的结果(就直接参照案例中的):

Starting Portfolio Value: 100000.00
2000-01-03T00:00:00, Close, 27.85
2000-01-04T00:00:00, Close, 25.39
2000-01-05T00:00:00, Close, 24.05
2000-01-05T00:00:00, BUY CREATE, 24.05
2000-01-06T00:00:00, BUY EXECUTED, Size 10, Price: 23.61, Cost: 236.10, Commission 0.24
2000-01-06T00:00:00, Close, 22.63
...
...
...
2000-12-20T00:00:00, BUY CREATE, 26.88
2000-12-21T00:00:00, BUY EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.26
2000-12-21T00:00:00, Close, 27.82
2000-12-22T00:00:00, Close, 30.06
2000-12-26T00:00:00, Close, 29.17
2000-12-27T00:00:00, Close, 28.94
2000-12-28T00:00:00, Close, 29.29
2000-12-29T00:00:00, Close, 27.41
2000-12-29T00:00:00, SELL CREATE, 27.41
Final Portfolio Value: 100169.80

In order to see the difference, the print outputs have also been extended to show the execution size.

为了查看差异,还扩展了打印输出以显示执行大小。

Having multiplied the stake by 10, the obvious has happened: the profit and loss has been multiplied by 10. Instead of 16.98, the surplus is now 169.80

把股份交易增加了10倍之后,明显的事情发生了:利润和损失增加了10倍。盈余从原来的16.98变成了169.80

 

Adding an indicator

添加一些指标

Having heard of indicators, the next thing anyone would add to the strategy is one of them. For sure they must be much better than a simple “3 lower closes” strategy.

听说过指标之后,接下来任何人都可以在策略中添加。当然它们必须比"3低收盘"策略要好的多

Inspired in one of the examples from PyAlgoTrade a strategy using a Simple Moving Average.

从PyAlgoTrade的一个例子例子中得到启发,使用简单移动平均线策略

  • Buy “AtMarket” if the close is greater than the Average

  • 如果收盘价大于平均价格,在市场买入
  • If in the market, sell if the close is smaller than the Average

  •  如果收盘价小于平均价格,在市场卖出

  • Only 1 active operation is allowed in the market

  • 在市场中只允许一个活跃的操作

 

Most of the existing code can be kept in place. Let’s add the average during init and keep a reference to it:

大多数现有代码可以无须更改。让我们在init中添加平均值并保持对它的引用:

self.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod)

And of course the logic to enter and exit the market will rely on the Average values. Look in the code for the logic.

 当然进入和退出市场的逻辑依赖与平均值。在代码中寻找逻辑

Note

The starting cash will be 1000 monetary units to be in line with the PyAlgoTrade example and no commission will be applied

注意

根据PyAlgoTrade的例子,初始现金为1000个货币单位,不收取佣金

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):
    # 参数设置
    params = (
        ('maperiod', 15),
    )

    #  日志输出
    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # Add a MovingAverageSimple indicator
        # 初始化日均线,第一个参数是数据来源,第二个参数为日均线的时间
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if 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))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # Check if we are in the market
        if not self.position:

            # Not yet ... we MIGHT BUY if ...
            # 收盘价大于当日的均线价格
            if self.dataclose[0] > self.sma[0]:

                # BUY, BUY, BUY!!! (with all possible default parameters)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()

        else:
            # 收盘价小于当日均线的价格
            if self.dataclose[0] < self.sma[0]:
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()


if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    cerebro.addstrategy(TestStrategy)

    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')

    # Create a Data Feed
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # Do not pass values before this date
        fromdate=datetime.datetime(2000, 1, 1),
        # Do not pass values before this date
        todate=datetime.datetime(2000, 12, 31),
        # Do not pass values after this date
        reverse=False)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Set our desired cash start
    cerebro.broker.setcash(1000.0)

    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # Set the commission
    cerebro.broker.setcommission(commission=0.0)

    # Print out the starting conditions
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # Run over everything
    cerebro.run()

    # Print out the final result
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

Now, before skipping to the next section LOOK CAREFULLY to the first date which is shown in the log:  

现在,在跳到下一章节之前,仔细的看一下日志中显示的第一条数据

  • It’ no longer 2000-01-03, the first trading day in the year 2K.

  • 开始不再是2000年01月03日,公元2000年的第一个交易日。
  •  

    It’s 2000-01-24 … Who has stolen my cheese?

  • 开始是2000-01-24,谁偷了我的奶酪?

The missing days are not missing. The platform has adapted to the new circumstances:

消失的日子并没有消失。平台为了适应新的环境:

  • An indicator (SimpleMovingAverage) has been added to the Strategy.

  • 策略中增加了一个指标(SimpleMovingAverage)。
  • This indicator needs X bars to produce an output: in the example: 15

  • 这个指示器需要X条来产生输出:在示例中为15条
  • 2000-01-24 is the day in which the 15th bar occurs

   2000-01-24是第15个bar出现的日子

The backtrader platform assumes that the Strategy has the indicator in place for a good reason, to use it in the decision making process. And it makes no sense to try to make decisions if the indicator is not yet ready and producing values.

backtrader平台,假设策略有一个很好的指标,在决策过程中将使用它。如果指标还没有准备好、还没有产生价值,那么试图做出决定就没有意义。

  • next will be 1st called when all indicators have already reached the minimum needed period to produce a value

  • 当所有的指标已经达到产生一个值所需的最小周期,将首先调用next方法
  • In the example there is a single indicator, but the strategy could have any number of them.

  • 在本示例中只有一个指标,但一个策略可以有很多指标

After the execution the output is:

最后的执行输出:

Starting Portfolio Value: 1000.00
2000-01-24T00:00:00, Close, 25.55
2000-01-25T00:00:00, Close, 26.61
2000-01-25T00:00:00, BUY CREATE, 26.61
2000-01-26T00:00:00, BUY EXECUTED, Size 10, Price: 26.76, Cost: 267.60, Commission 0.00
2000-01-26T00:00:00, Close, 25.96
2000-01-27T00:00:00, Close, 24.43
2000-01-27T00:00:00, SELL CREATE, 24.43
2000-01-28T00:00:00, SELL EXECUTED, Size 10, Price: 24.28, Cost: 242.80, Commission 0.00
2000-01-28T00:00:00, OPERATION PROFIT, GROSS -24.80, NET -24.80
2000-01-28T00:00:00, Close, 22.34
2000-01-31T00:00:00, Close, 23.55
2000-02-01T00:00:00, Close, 25.46
2000-02-02T00:00:00, Close, 25.61
2000-02-02T00:00:00, BUY CREATE, 25.61
2000-02-03T00:00:00, BUY EXECUTED, Size 10, Price: 26.11, Cost: 261.10, Commission 0.00
...
...
...
2000-12-20T00:00:00, SELL CREATE, 26.88
2000-12-21T00:00:00, SELL EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.00
2000-12-21T00:00:00, OPERATION PROFIT, GROSS -20.60, NET -20.60
2000-12-21T00:00:00, Close, 27.82
2000-12-21T00:00:00, BUY CREATE, 27.82
2000-12-22T00:00:00, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.00
2000-12-22T00:00:00, Close, 30.06
2000-12-26T00:00:00, Close, 29.17
2000-12-27T00:00:00, Close, 28.94
2000-12-28T00:00:00, Close, 29.29
2000-12-29T00:00:00, Close, 27.41
2000-12-29T00:00:00, SELL CREATE, 27.41
Final Portfolio Value: 973.90

In the name of the King!!! A winning system turned into a losing one … and that with no commission. It may well be that simply adding an indicator is not the universal panacea.  

傻了吧,一个赢钱的变成了一个输钱的系统,而且没有佣金。简单增加指标并不是万能的添加剂

Note

注意

The same logic and data with PyAlgoTrade yields a slightly different result (slightly off). Looking at the entire printout reveals that some operations are not exactly the same. Being the culprit again the usual suspect: rounding.

使用相同的逻辑和数据,PyAlgoTrade会产生稍微不同的结果(稍微差一点)。查看整个打印输出,可以发现有些操作并不完全相同。再次成为罪魁祸首的是通常的疑点:四舍五入。

 PyAlgoTrade does not round the datafeed values when applying the divided “adjusted close” to the data feed values.

当对数据提要值应用划分的“调整关闭”时,PyAlgoTrade没有四舍五入datafeed值。

The Yahoo Data Feed provided by backtrader rounds the values down to 2 decimals after applying the adjusted close. Upon printing the values everything seems the same, but it’s obvious that sometimes that 5th place decimal plays a role.

在应用调整后的收盘价后,backtrader提供的雅虎数据将价值四舍五入到2小数。在打印值的时候,一切看起来都是一样的,但是很明显,有时候第5位的小数起了作用。

Rounding down to 2 decimals seems more realistic, because Market Exchanges do only allow a number of decimals per asset (being that 2 decimals usually for stocks)

四舍五入到2小数似乎更现实一些,因为市场交易只允许每项资产有几个小数(股票通常是2个小数)

backtrader会自动四舍五入数值

Note

The Yahoo Data Feed (starting with version 1.8.11.99 allows to specify if rounding has to happen and how many decimals)

注意:

Yahoo Data Feed(从1.8.11.99版本开始,允许指定是否需要四舍五入以及需要多少小数)

 

Visual Inspection: Plotting

视觉检查:绘图

A printout or log of the actual whereabouts of the system at each bar-instant is good but humans tend to be visual and therefore it seems right to offer a view of the same whereabouts as chart.

为每个bar打印输出或者记录实际位置是很好的系统,但是人类往往更加喜欢可视化的,因此它似乎是正确的,提供相同的行踪作为图标的视图。

Note

注意:

To plot you need to have matplotlib installed

画图要先装matplotlib

Once again defaults for plotting are there to assist the platform user. Plotting is incredibly a 1 line operation:

同样,绘图的默认设置是为了帮助平台用户。绘图是令人难以置信的1个命令操作:

cerebro.plot()

Being the location for sure after cerebro.run() has been called.  

这个命令确保在cerebro.run()之后执行

In order to display the automatic plotting capabilities and a couple of easy customizations, the following will be done:

为了显示自动绘图功能和一对简单的自定义,以下将做:

  • A 2nd MovingAverage (Exponential) will be added. The defaults will plot it (just like the 1st) with the data.

  • 第二条平均线会被添加。默认会跟第一条一样被画出
  • A 3rd MovingAverage (Weighted) will be added. Customized to plot in an own plot (even if not sensible)

  • 第三条平均线被添加。自定义在自己的plot中画出(即使不合理)
  • 简单的说,本来均线应该跟bar在一起,现在分开了,他有自己的小画框了。
  • A Stochastic (Slow) will be added. No change to the defaults.

  • A MACD will be added. No change to the defaults.

  • A RSI will be added. No change to the defaults.

  • A MovingAverage (Simple) will be applied to the RSI. No change to the defaults (it will be plotted with the RSI)

  • An AverageTrueRange will be added. Changed defaults to avoid it being plotted.

  • 一个AverageTrueRange指标被添加。改变了默认的参数,避免被画出

The entire set of additions to the init method of the Strategy:

策略的init方法的整个添加:

 # Indicators for the plotting show
bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
bt.indicators.WeightedMovingAverage(self.datas[0], period=25,subplot = True)
bt.indicators.StochasticSlow(self.datas[0])
bt.indicators.MACDHisto(self.datas[0])
rsi = bt.indicators.RSI(self.datas[0])
bt.indicators.SmoothedMovingAverage(rsi, period=10)
bt.indicators.ATR(self.datas[0]).plot = False

Note

注意

Even if indicators are not explicitly added to a member variable of the strategy (like self.sma = MovingAverageSimple…), they will autoregister with the strategy and will influence the minimum period for next and will be part of the plotting.

即使指标没有显式地添加到策略的成员变量(如self.sma = MovingAverageSimple…)。,他们将自动注册与策略,并将影响下一个最小周期,并将成为绘图的一部分。

In the example only RSI is added to a temporary variable rsi with the only intention to create a MovingAverageSmoothed on it.

在这个例子中,只有RSI被添加到临时变量RSI中,只是为了在其上创建一个MovingAverageSmoothed。

示例如下:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)

        # Indicators for the plotting show
        # 添加的指标线
        bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
        bt.indicators.WeightedMovingAverage(self.datas[0], period=25,
                                            subplot=True)
        bt.indicators.StochasticSlow(self.datas[0])
        bt.indicators.MACDHisto(self.datas[0])
     # 神奇的地方 rsi = bt.indicators.RSI(self.datas[0]) bt.indicators.SmoothedMovingAverage(rsi, period=10) bt.indicators.ATR(self.datas[0], plot=False) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash if 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)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') # Write down: no pending order self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one if self.order: return # Check if we are in the market if not self.position: # Not yet ... we MIGHT BUY if ... if self.dataclose[0] > self.sma[0]: # BUY, BUY, BUY!!! (with all possible default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: if self.dataclose[0] < self.sma[0]: # SELL, SELL, SELL!!! (with all possible default parameters) self.log('SELL CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.sell() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(1000.0) # Add a FixedSize sizer according to the stake cerebro.addsizer(bt.sizers.FixedSize, stake=10) # Set the commission cerebro.broker.setcommission(commission=0.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Plot the result cerebro.plot()

After the execution the output is:  

执行输出

Starting Portfolio Value: 1000.00
2000-02-18T00:00:00, Close, 27.61
2000-02-22T00:00:00, Close, 27.97
2000-02-22T00:00:00, BUY CREATE, 27.97
2000-02-23T00:00:00, BUY EXECUTED, Size 10, Price: 28.38, Cost: 283.80, Commission 0.00
2000-02-23T00:00:00, Close, 29.73
...
...
...
2000-12-21T00:00:00, BUY CREATE, 27.82
2000-12-22T00:00:00, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.00
2000-12-22T00:00:00, Close, 30.06
2000-12-26T00:00:00, Close, 29.17
2000-12-27T00:00:00, Close, 28.94
2000-12-28T00:00:00, Close, 29.29
2000-12-29T00:00:00, Close, 27.41
2000-12-29T00:00:00, SELL CREATE, 27.41
Final Portfolio Value: 981.00

The final result has changed even if the logic hasn’t. This is true but the logic has not been applied to the same number of bars.

即使逻辑没有改变,最终的结果还是改变了。这是正确的,因为这种逻辑并没有应用到相同数量的bar上。

Note

注意:

As explained before, the platform will first call next when all indicators are ready to produce a value. In this plotting example (very clear in the chart) the MACD is the last indicator to be fully ready (all 3 lines producing an output). The 1st BUY order is no longer scheduled during Jan 2000 but close to the end of Feb 2000.

如前所述,当所有指标都准备好产生一个值时,平台将首先调用next。在这个绘图示例中(在图表中非常清楚),MACD是最后一个完全准备好了的指示符(所有3行生成一个输出)。第一个订单不再安排在2000年1月,而是接近2000年2月底。

图:

Let’s Optimize 

让我们来进行优化

Many trading books say each market and each traded stock (or commodity or ..) have different rythms. That there is no such thing as a one size fits all.

许多交易书籍说每个市场和每个交易的股票(或商品或...)拥有不同的规律。没有一刀切的办法。

Before the plotting sample, when the strategy started using an indicator the period default value was 15 bars. It’s a strategy parameter and this can be used in an optimization to change the value of the parameter and see which one better fits the market.

在绘制样本之前,当策略开始使用指示符时,周期默认值为15条。这是一个策略参数,可以用于优化,改变参数的值,看看哪个更适合市场。

Note

注意

There is plenty of literature about Optimization and associated pros and cons. But the advice will always point in the same direction: do not overoptimize. If a trading idea is not sound, optimizing may end producing a positive result which is only valid for the backtested dataset.

关于优化的相关优缺点有很多文献。但是这些建议总是指向同一个方向:不要过度优化。如果一个交易的想法不健全,优化可能产生一个积极的结果,但也仅适合与这些回测数据。

The sample is modified to optimize the period of the Simple Moving Average. For the sake of clarity any output with regards to Buy/Sell orders has been removed

对样本进行修改,以优化简单移动平均的周期。为了清楚起见,任何有关买卖订单的输出都被删除了

The example now:

代码示例如下:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])


# Import the backtrader platform
import backtrader as bt


# Create a Stratey
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
        # 打印输出开关
        ('printlog', False),
    )

    def log(self, txt, dt=None, doprint=False):
        ''' Logging function fot this strategy'''
        # 很聪明的设置了一个默认参数,并使用了短路原则是否输出log
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if 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))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        # Write down: no pending order
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # Check if we are in the market
        if not self.position:

            # Not yet ... we MIGHT BUY if ...
            if self.dataclose[0] > self.sma[0]:

                # BUY, BUY, BUY!!! (with all possible default parameters)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()

        else:

            if self.dataclose[0] < self.sma[0]:
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()

    # 定义该方法,用于一次测试结束后的输出。
    def stop(self):
        self.log('(MA Period %2d) Ending Value %.2f' %
                 (self.params.maperiod, self.broker.getvalue()), doprint=True)


if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    # 添加策略,并输入参数范围
    strats = cerebro.optstrategy(
        TestStrategy,
        maperiod=range(10, 31))

    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')

    # Create a Data Feed
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # Do not pass values before this date
        fromdate=datetime.datetime(2000, 1, 1),
        # Do not pass values before this date
        todate=datetime.datetime(2000, 12, 31),
        # Do not pass values after this date
        reverse=False)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Set our desired cash start
    cerebro.broker.setcash(1000.0)

    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # Set the commission
    cerebro.broker.setcommission(commission=0.0)

    # Run over everything
    # 单核跑,按照顺序输出
    cerebro.run(maxcpus=1)

Instead of calling addstrategy to add a stratey class to Cerebro, the call is made to optstrategy. And instead of passing a value a range of values is passed.

调用optstrategy不是调用addstrategy来向大脑添加一个stratey类。不是传递一个值,而是传递一个值的范围。

One of the “Strategy” hooks is added, the stop method, which will be called when the data has been exhausted and backtesting is over. It’s used to print the final net value of the portfolio in the broker (it was done in Cerebro previously)

添加了一个“策略”钩子,stop方法,当数据耗尽并且回溯测试结束时,将调用该方法。它用于打印经纪人中投资组合的最终净值(以前在大脑中是这样做的)

The system will execute the strategy for each value of the range. The following will be output:

系统将对范围内的每个值执行策略。将输出以下内容:

2000-12-29, (MA Period 10) Ending Value 880.30
2000-12-29, (MA Period 11) Ending Value 880.00
2000-12-29, (MA Period 12) Ending Value 830.30
2000-12-29, (MA Period 13) Ending Value 893.90
2000-12-29, (MA Period 14) Ending Value 896.90
2000-12-29, (MA Period 15) Ending Value 973.90
2000-12-29, (MA Period 16) Ending Value 959.40
2000-12-29, (MA Period 17) Ending Value 949.80
2000-12-29, (MA Period 18) Ending Value 1011.90
2000-12-29, (MA Period 19) Ending Value 1041.90
2000-12-29, (MA Period 20) Ending Value 1078.00
2000-12-29, (MA Period 21) Ending Value 1058.80
2000-12-29, (MA Period 22) Ending Value 1061.50
2000-12-29, (MA Period 23) Ending Value 1023.00
2000-12-29, (MA Period 24) Ending Value 1020.10
2000-12-29, (MA Period 25) Ending Value 1013.30
2000-12-29, (MA Period 26) Ending Value 998.30
2000-12-29, (MA Period 27) Ending Value 982.20
2000-12-29, (MA Period 28) Ending Value 975.70
2000-12-29, (MA Period 29) Ending Value 983.30
2000-12-29, (MA Period 30) Ending Value 979.80

不是调用addstrategy将一个stratey类添加到Cerebro,而是调用optstrategy。而不是传递一个值,而是传递一个范围的值。 添加了一个“策略”钩子,即stop方法,该方法将在耗尽数据和返回测试结束时被调用。它用于在经纪人中打印投资组合的最终净值(之前在Cerebro中做过) 系统将对范围内的每个值执行策略。输出内容如下:

 

Results:

结果:

  • For periods below 18 the strategy (commissionless) loses money.

  • 在18以下的时期,该策略(无佣金)亏损。
  • For periods between 18 and 26 (both included) the strategy makes money.

  • 在18到26之间(包括两者),这种策略会赚钱。
  • Above 26 money is lost again.

  • 26以上的又亏钱了。

而该策略和给定数据集的获胜周期为:

20的数据,获得78.00个单位超过1000美元/欧元(7.8%)

Note

注意:

The extra indicators from the plotting example have been removed and the start of operations is only influenced by the Simple Moving Average which is being optimized. Hence the slightly different results for period 15

绘图示例中的额外指标已被删除,操作的开始只受正在优化的简单移动平均线的影响。只有第15期的结果略有不同 

Conclusion

结论

The incremental samples have shown how to go from a barebones script to a fully working trading system which even plots the results and can be optimized.

递增的示例展示了如何从一个基本的脚本变成一个完全工作的交易系统,该系统甚至可以绘制结果并进行优化。

A lot more can be done to try to improve the chances of winning:

为了提高获胜的几率,我们可以做的还有很多:

Self defined Indicators

自定义指标

Creating an indicator is easy (and even plotting them is easy)

创建指示符很容易(甚至绘制它们也很容易)

Sizers

分级器

Money Management is for many the key to success

对许多人来说,资金管理是成功的关键

Order Types (limit, stop, stoplimit)

订单类型(限制、停止、止损)

Some others

其他一些方法

To ensure all the above items can be fully utilized the documentation provides an insight into them (and other topics)

为了确保上述所有内容都能得到充分利用,文档提供了对它们(以及其他主题)的深入了解

Look in the table of contents and keep on reading … and developing.

查看目录,继续阅读……并发展。

Best of luck

最好的运气

 

OVER

 

 

 二次回看,确实写的不错的官方文档。




posted @ 2020-07-29 14:14  就是想学习  阅读(2117)  评论(1编辑  收藏  举报