backtrader数据 (backtrader中文官方文档)

快速入门指南

注意: 《快速入门指南》中使用的数据文件会不时更新,这意味着调整后的收盘价会更改,收盘价也会随之更改(以及其他组件)。这意味着实际输出可能与编写本文时文档中的内容不同。

使用平台

让我们通过一系列的例子(从几乎一个空白策略到一个完全成熟的策略),但是还有两个BackTrader基本概念需要大致解释一下

  1. 线(Lines)数据槽、指标和策略都有各自的线一条线是一系列的点连接在一起形成的。谈到到交易市场时,数据槽通常每天有以下几个要点:开盘价、最高价、最低价、收盘价、成交量、开盘利率沿着时间的一系列“开盘价”是一条线。因此,数据槽通常有6条线。如果我们也考虑“DateTime”(这是单个点的实际引用),我们可以说有7条线。
  2. 索引0方法当访问线中的值时,使用索引0访问当前值上一个输出值用-1访问。这符合iterables的Python约定(线可以迭代,因此是iterable),其中索引-1用于访问iterable/数组的“上一”项。在我们的例子中,上一个输出值被访问。因此,它在-1后面的索引0用于访问行中的当前时刻。

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

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

获取移动平均线当前值的最简单方法:

av = self.sma[0]

不需要知道已经处理了多少条/分钟/天/月,因为“0”唯一地标识当前时刻。

按照pythonic的传统,使用-1访问“上一个”输出值:

previous_value = self.sma[-1]

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

从0到100个样本

1. 基本设置

运行如下代码:

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())

执行后,输出为:

Starting Portfolio Value: 10000.00
Final Portfolio Value: 10000.00

在本例中:

  • 调用了BackTrader包
  • 大脑引擎被实例化了
  • 生成的cerebro大脑实例被告知运行(遍历数据)
  • 打印出了结果

虽然看起来不多,但让我们明确看到了:

  • 大脑引擎在后台创建了一个代理实例
  • 该实例已经有一些现金可供启动

这种在后台代理实例化是平台中一个默认的设定,可以简化用户的使用。如果用户没有设置任何代理,则会设置一个默认代理。对于一些代理来说,1万个货币单位是一个常见的值。

2. 设置现金

在金融的世界里,当然,只有“失败者”才以1万开始。让我们进行更改,再举一次例子。

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())

执行后,输出为:

Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00

任务完成。让我们再多了解一些其他的。

3. 添加数据槽

拥有现金是很有趣的,但这一切背后的目的,是让一个自动化的策略,通过对一个我们视为数据槽的资产进行操作,在不动手指的情况下让现金成倍增长。因此…没有数据输入-> 没有乐趣。让我们在不断扩充的例子中添加一个。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(100000.0)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行后,输出为:

Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00

示例代码量略有增长,因为我们添加了:

  • 定位我们示例代码所需的数据文件的位置
  • 有datetime对象来过滤我们将要操作的数据槽中的那些数据

除此之外,数据槽被创建并添加到大脑中。输出并没有变化。如果有变化,那将是一个奇迹。

注意: 雅虎网上数据输出的CSV数据是按标准的降序发送的。reversed=True参数考虑到文件中的CSV数据已经被反转,并且具有标准的预期日期升序。

4. 我们的第一个策略

现金在代理那里,数据槽在那里。冒险的生意似乎就在眼前。让我们在等式中加入一个策略,并打印出每天的“收盘价”(K线)。

DataSeries(数据槽中的底层类)对象具有别名,可以访问众所周知的OHLC(Open High Low Close)日值。这将简化打印逻辑的创建。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

    def next(self):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(100000.0)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行后,输出为:

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

有人说股票市场是有风险的行业,但事实似乎并非如此。让我们来解释一下其中的一些魔法:

  • 在调用init时,策略已经有了平台中存在的数据列表这是一个标准的Python列表,可以按插入顺序访问数据。列表中的第一个数据self.datas[0]是交易操作的默认数据,用于保持所有策略元素同步(这是系统时钟)
  • self.dataclose = self.datas[0] 保持对收盘价一行的引用。以后只需要一级间接寻址就可以访问close值。
  • 策略的next方法将在系统时钟的每根K线上调用(self.datas[0])。 其他事情,比如指标完成后,它返回True,它需要一些K线开始产生一个输出。这部分之后会详细介绍。

5. 为策略添加一些逻辑

让我们看看图表来试试我们的疯狂想法

  • 如果价格已经连续下跌3个交易日…买,买,买!!!
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

    def next(self):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        if self.dataclose[0] < self.dataclose[-1]:
            # 当前收盘价小于前一K线收盘价

            if self.dataclose[-1] < self.dataclose[-2]:
                # 前一收盘价小于更前一收盘价

                # 买,买,买!!! (应用所有可能的默认参数)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()

if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(100000.0)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    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

有几个“做多”的下单命令被发出,最终我们的投资组合价值减少。显然遗漏了几件重要的事。

  • 订单已创建,但不知道是否执行,何时执行,价格如何。下一个示例将通过侦听订单状态的通知来构建该示例。

好奇的读者可能会问,有多少股票正在被购买,什么资产正在被购买,以及命令是如何执行的。在可能的情况下(在这种情况下是这样),平台会填补以下空白:

  • self.datas[0](主数据即系统时钟)是目标资产(如果未指定其他资产)
  • 交易量是由一个使用固定股票的仓位量(position sizer)在幕后提供的,默认值为“1”。它将在后面的示例中进行修改
  • 订单是“按市场”执行的。代理(在前面的例子中展示的)使用下一K线的开盘价来执行这个操作,因为这是当前正在检查的条形图之后的第一个tick。
  • 到目前为止,该命令的执行没有产生任何佣金(稍后将详细介绍)

6. 不仅做多…还要做空

在知道如何进入市场(做多)后,需要一个“出场概念”,也需要了解策略是否在市场中。

  • 幸运的是,Strategy对象提供了对默认数据槽的position属性的访问
  • buy和sell方法返回创建的(尚未执行的)订单
  • 订单状态的变化将通过notify方法通知策略“出场概念”将是一个简单的概念:
  • 在5根K线(总第6根)之后退出,不管是好是坏请注意,并非是“时间”或“时间范围”:而是K线数。K线可以表示1分钟、1小时、1天、1周或任何其他时间段。虽然我们知道数据槽是日线数据,但该策略对此不作任何假设。

此外,为了简化:

  • 只有在市场上还没有订单时才允许购买做多

注意: next方法没有接收“K线索引”,因此它似乎不太清楚如何理解5根K线何时已经过去,但这是以python的方式建模的:对一个对象调用len,它会告诉你它的行的长度。只需写下(保存在变量中)操作发生的长度,看看当前的长度是否为5。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理的订单
        self.order = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log('BUY EXECUTED, %.2f' % order*ex.e**cuted.price)
            elif order.issell():
                self.log('SELL EXECUTED, %.2f' % order*ex.e**cuted.price)

            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 next(self):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查订单是否挂起。。。如果是,我们无法发送第二个
        if self.order:
            return

        # 检查我们是否在市场上
        if not self.position:

            # 还没有。。。我们可能会做多如果。。。
            if self.dataclose[0] < self.dataclose[-1]:
                    # 当前收盘价小于前一K线收盘价

                    if self.dataclose[-1] < self.dataclose[-2]:
                        # 前一收盘价小于更前一收盘价

                        # 买,买,买!!! (应用所有可能的默认参数)
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])

                        # 跟踪创建的订单以避免第二个订单
                        self.order = self.buy()
        else:
            # 已经在市场,我们可能需要做空
            if len(self) >= (self.bar_executed + 5):
                # 卖,卖,卖!!! (应用所有可能的默认参数)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # 跟踪创建的订单以避免第二个订单
                self.order = self.sell()


if __name__ == '__main__':
        if self.dataclose[0] < self.dataclose[-1]:
            # 当前收盘价小于前一K线收盘价

            if self.dataclose[-1] < self.dataclose[-2]:
                # 前一收盘价小于更前一收盘价

                # 买,买,买!!! (应用所有可能的默认参数)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()

if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(100000.0)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行后,输出为:

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, 23.61
2000-01-06T00:00:00, Close, 22.63
2000-01-07T00:00:00, Close, 24.37
2000-01-10T00:00:00, Close, 27.29
2000-01-11T00:00:00, Close, 26.49
2000-01-12T00:00:00, Close, 24.90
2000-01-13T00:00:00, Close, 24.77
2000-01-13T00:00:00, SELL CREATE, 24.77
2000-01-14T00:00:00, SELL EXECUTED, 25.70
2000-01-14T00:00:00, Close, 25.18
...
...
...
2000-12-15T00:00:00, SELL CREATE, 26.93
2000-12-18T00:00:00, SELL EXECUTED, 28.29
2000-12-18T00:00:00, Close, 30.18
2000-12-19T00:00:00, Close, 28.88
2000-12-20T00:00:00, Close, 26.88
2000-12-20T00:00:00, BUY CREATE, 26.88
2000-12-21T00:00:00, BUY EXECUTED, 26.23
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: 100018.53

天哪!!!系统赚钱了…一定是哪出了问题

7. 代理说:把钱给我看看!

这笔钱叫做“佣金”。让我们对每个操作(无论做多还是做空…是的,代理是狂热的…)增加一个合理的0.1%的佣金率只需一行就够了:

# 0.1% ,,,去掉百分号,再除以100
cerebro.broker.setcommission(commission=0.001)

对平台有经验后,我们想看到做多/做空周期后的盈利或亏损情况,计入或不计入佣金。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理订单和买入价格/佣金
        self.order = None
        self.buyprice = None
        self.buycomm = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order*ex.e**cuted.price,
                     order*ex.e**cuted.value,
                     order*ex.e**cuted.comm))

                self.buyprice = order*ex.e**cuted.price
                self.buycomm = order*ex.e**cuted.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order*ex.e**cuted.price,
                          order*ex.e**cuted.value,
                          order*ex.e**cuted.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):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查订单是否挂起。。。如果是,我们无法发送第二个
        if self.order:
            return

        # 检查我们是否在市场上
        if not self.position:

            # 还没有。。。我们可能会做多如果。。。
            if self.dataclose[0] < self.dataclose[-1]:
                    # 当前收盘价小于前一K线收盘价

                    if self.dataclose[-1] < self.dataclose[-2]:
                        # 前一收盘价小于更前一收盘价

                        # 买,买,买!!! (应用所有可能的默认参数)
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])

                        # 跟踪创建的订单以避免第二个订单
                        self.order = self.buy()
        else:
            # 已经在市场,我们可能需要做空
            if len(self) >= (self.bar_executed + 5):
                # 卖,卖,卖!!! (应用所有可能的默认参数)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # 跟踪创建的订单以避免第二个订单
                self.order = self.sell())


if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(100000.0)

    # 设定佣金为0.1%,去掉百分号除以100
    cerebro.broker.setcommission(commission=0.001)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行后,输出为:

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, Price: 23.61, Cost: 23.61, Commission 0.02
2000-01-06T00:00:00, Close, 22.63
2000-01-07T00:00:00, Close, 24.37
2000-01-10T00:00:00, Close, 27.29
2000-01-11T00:00:00, Close, 26.49
2000-01-12T00:00:00, Close, 24.90
2000-01-13T00:00:00, Close, 24.77
2000-01-13T00:00:00, SELL CREATE, 24.77
2000-01-14T00:00:00, SELL EXECUTED, Price: 25.70, Cost: 25.70, Commission 0.03
2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.04
2000-01-14T00:00:00, Close, 25.18
...
...
...
2000-12-15T00:00:00, SELL CREATE, 26.93
2000-12-18T00:00:00, SELL EXECUTED, Price: 28.29, Cost: 28.29, Commission 0.03
2000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12
2000-12-18T00:00:00, Close, 30.18
2000-12-19T00:00:00, Close, 28.88
2000-12-20T00:00:00, Close, 26.88
2000-12-20T00:00:00, BUY CREATE, 26.88
2000-12-21T00:00:00, BUY EXECUTED, Price: 26.23, Cost: 26.23, Commission 0.03
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: 100016.98

谢天谢地!!!系统仍然赚钱。在继续之前,让我们通过筛选“交易利润”线行来观察细节:

2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.04
2000-02-07T00:00:00, OPERATION PROFIT, GROSS 3.68, NET 3.63
2000-02-28T00:00:00, OPERATION PROFIT, GROSS 4.48, NET 4.42
2000-03-13T00:00:00, OPERATION PROFIT, GROSS 3.48, NET 3.41
2000-03-22T00:00:00, OPERATION PROFIT, GROSS -0.41, NET -0.49
2000-04-07T00:00:00, OPERATION PROFIT, GROSS 2.45, NET 2.37
2000-04-20T00:00:00, OPERATION PROFIT, GROSS -1.95, NET -2.02
2000-05-02T00:00:00, OPERATION PROFIT, GROSS 5.46, NET 5.39
2000-05-11T00:00:00, OPERATION PROFIT, GROSS -3.74, NET -3.81
2000-05-30T00:00:00, OPERATION PROFIT, GROSS -1.46, NET -1.53
2000-07-05T00:00:00, OPERATION PROFIT, GROSS -1.62, NET -1.69
2000-07-14T00:00:00, OPERATION PROFIT, GROSS 2.08, NET 2.01
2000-07-28T00:00:00, OPERATION PROFIT, GROSS 0.14, NET 0.07
2000-08-08T00:00:00, OPERATION PROFIT, GROSS 4.36, NET 4.29
2000-08-21T00:00:00, OPERATION PROFIT, GROSS 1.03, NET 0.95
2000-09-15T00:00:00, OPERATION PROFIT, GROSS -4.26, NET -4.34
2000-09-27T00:00:00, OPERATION PROFIT, GROSS 1.29, NET 1.22
2000-10-13T00:00:00, OPERATION PROFIT, GROSS -2.98, NET -3.04
2000-10-26T00:00:00, OPERATION PROFIT, GROSS 3.01, NET 2.95
2000-11-06T00:00:00, OPERATION PROFIT, GROSS -3.59, NET -3.65
2000-11-16T00:00:00, OPERATION PROFIT, GROSS 1.28, NET 1.23
2000-12-01T00:00:00, OPERATION PROFIT, GROSS 2.59, NET 2.54
2000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12

把“净”利润加起来,最后的数字是:15.83但系统最后显示:

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

显然15.83不是16.98。没有任何错误。15.83的“净”利润已经落袋为安。不幸的是(或者更好地理解平台角度来说幸运的是),在数据槽的最后一天有一个开仓。即使卖出操作已经发送…它还没有被执行。代理计算的“最终投资组合价值”考虑了2000年12月29日的“收盘价”。实际执行价格将在下一个交易日确定,而下一个交易日恰好是2001-01-02。扩展数据槽以考虑到那天的输出是:

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

现在将以前的净利润加在已完成经营的净利润中:

15.83 + 1.59 = 17.42

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

8. 自定义策略:参数

硬编码策略中的某些值不便于更改,实际应用一般不采用这种方式。参数应当是更好的方式。参数的定义很简单,如下所示:

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

这是一个标准的Python元组,它包含一些元组,下面的写法对某些人看起来更舒适:

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

无论是哪种格式,在将策略添加到Cerebro引擎时,都允许对策略进行参数化:

# 添加策略
cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)

注意: setsizing方法已弃用。此段内容保留在这里提示那些看到旧代码例子的人。这部分代码已经更新:

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

请阅读有订单量的章节在策略中使用参数很容易,因为它们存储在“params”属性中。例如,如果我们想设置固定单量,我们可以将stake参数传递给仓位 sizer,比如通过init:

# 从参数设置sizer量
self.sizer.setsizing(self.params.stake)

我们也可以用做多和做空的量的参数和self.params.stake作为值出场的逻辑被修改:

# 已经在市场上。。。我们可以做空了
if len(self) >= (self.bar_executed + self.params.exitbars):

综合这些改变,示例演变为:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    params = (
        ('exitbars', 5),
    )

    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理订单和买入价格/佣金
        self.order = None
        self.buyprice = None
        self.buycomm = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order*ex.e**cuted.price,
                     order*ex.e**cuted.value,
                     order*ex.e**cuted.comm))

                self.buyprice = order*ex.e**cuted.price
                self.buycomm = order*ex.e**cuted.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order*ex.e**cuted.price,
                          order*ex.e**cuted.value,
                          order*ex.e**cuted.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):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查订单是否挂起。。。如果是,我们无法发送第二个
        if self.order:
            return

        # 检查我们是否在市场上
        if not self.position:
            # 还没有。。。我们可能会做多如果。。。
            if self.dataclose[0] < self.dataclose[-1]:
                    # 当前收盘价小于前一K线收盘价

                    if self.dataclose[-1] < self.dataclose[-2]:
                        # 前一收盘价小于更前一收盘价
                        # 买,买,买!!! (应用所有可能的默认参数)
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])
                        # 跟踪创建的订单以避免第二个订单
                        self.order = self.buy()
        else:
            # 已经在市场,我们可能需要做空
            if len(self) >= (self.bar_executed + self.params.exitbars):
                # 卖,卖,卖!!! (应用所有可能的默认参数)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                # 跟踪创建的订单以避免第二个订单
                self.order = self.sell()

if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(100000.0)

    # 根据stake添加一个固定下单量
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # 设定佣金为0.1%,去掉百分号除以100
    cerebro.broker.setcommission(commission=0.001)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行后,输出为:

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

为了查看差异,还扩展了打印输出以显示执行单量大小。把下单量乘以10,显而易见:利润和亏损都乘以了10。盈余现在不是16.98,而是169.80

9. 添加指标

了解指标的人都会选取一个加入到策略中。当然,它们肯定比简单的“三连跌做多”策略要好得多。从PyAlgoTrade的一个例子中,我们得到了一个使用简单移动平均线的策略的启发。

  • 如果收盘价高于均线值,市价买入
  • 如果有持仓,且收盘价低于平均水平,就卖出
  • 市场上只允许有1个活跃操作

大多数现有代码都可以保留在适当的位置。让我们在初始化时添加平均值并保留对它的引用:

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

当然,进入和退出市场的逻辑将取决于平均值。在代码中查找逻辑。

注意: 起始现金参照PyAlgoTrade的例子设定为1000个货币单位,且不收取佣金

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
    )

    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理订单和买入价格/佣金
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # 添加简单移动平均指标
        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]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order*ex.e**cuted.price,
                     order*ex.e**cuted.value,
                     order*ex.e**cuted.comm))

                self.buyprice = order*ex.e**cuted.price
                self.buycomm = order*ex.e**cuted.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order*ex.e**cuted.price,
                          order*ex.e**cuted.value,
                          order*ex.e**cuted.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):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查订单是否挂起。。。如果是,我们无法发送第二个
        if self.order:
            return

        # 检查我们是否在市场上
        if not self.position:
            # 还没有。。。我们可能会做多如果。。。
            if self.dataclose[0] > self.sma[0]:
                # 买,买,买!!! (应用所有可能的默认参数)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])

                # 跟踪创建的订单以避免第二个订单
                self.order = self.buy()
        else:
            if self.dataclose[0] < self.sma[0]:
                # 卖,卖,卖!!! (应用所有可能的默认参数)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                # 跟踪创建的订单以避免第二个订单
                self.order = self.sell()


if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(1000.0)

    # 根据stake添加一个固定下单量
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # 设定佣金
    cerebro.broker.setcommission(commission=0.0)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

现在,在跳到下一节之前,请仔细查看日志中显示的第一个日期:

  • 现在已经不是2000-01-03,2000年的第一个交易日了。而是2000-01-24…这是怎么回事?

错过的日子不是忘记了。BackTrader平台已经适应了新的设定:

  • 策略中添加了一个指标(SimpleMovingAverage)。
  • 这个指标需要X条K线来产生输出:在本例中:X=15
  • 2000年1月24日是第15根K线出现的日子

BackTrader平台在决策过程中针对指标策略,做出这样的设定有一个很好的理由。在决策过程中使用该指标。如果指标还没有就绪并产生指标值,那么试图做出决策是没有意义的。

  • 当所有指标达到生成指标值所需的最小周期时,才第一次调用next方法
  • 在本例中,只有一个指标,但该策略可以有任意数量的指标。执行后,输出为:
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

糟糕!!!一个成功的系统变成了一个失败的系统…而且没有佣金。很可能仅仅增加一个指标并不是万能的灵丹妙药。

注意: PyAlgoTrade的相同逻辑和数据产生了稍微不同的结果(稍微偏离)。查看整个打印输出会发现有些操作并不完全相同。罪魁祸首通常还是因为:四舍五入。

当对数据槽值应用除以的“调整后收盘价”时,PyAlgoTrade不会对数据槽值进行舍入。BackTrader提供的Yahoo数据槽在应用调整后的收盘价后,将数值舍入到2位小数。在打印这些值时,一切看起来都是一样的,但很明显,有时第五位小数会起作用。四舍五入到2位小数似乎更为现实,因为市场交易只允许每个资产有若干位小数(股票通常是2位小数)

注意: Yahoo数据槽,从版本1.8.11.99开始允许指定是否必须进行舍入以及小数位数

10. 可视化检测:绘图

用打印输出的方式来跟踪系统在每根K线的运行的瞬间状态是有帮助的,但人类往往依赖视觉,因此提供一个运行曲线图应该是极好的。

注意: 要绘图,需要安装matplotlib

同样,打印的默认设置也是为了帮助平台用户。绘图简单到令人难以置信地只用一行语句:

cerebro.plot()

要确定在cerebro.run()被调用后运行。

为了显示自动打印功能和一些简单的自定义设置,将执行以下操作:

  • 将添加第二个移动平均值(指数)。默认值将用数据绘制它(就像第一个一样)。
  • 将添加第三个移动平均值(加权)。自定义为在自己的绘图中绘图(即使不合理)
  • 添加一个随机震动指标(慢速)。默认值不变。
  • 添加MACD。默认值不变。
  • 添加RSI。默认值不变。
  • 移动平均值(简单)将应用于RSI指标。默认值不变(将与RSI一起绘制)
  • 添加AverageTrueRange。更改默认值以避免打印。

在策略的init方法中添加:

# 用于绘图的指标
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

注意: 即使指标没有显式地添加到策略的成员变量(如self.sma=MovingAverageSimple…,它们将自动注册策略,并将影响next方法判定的最短周期,并且将成为绘图的一部分。

在这个例子中,只有RSI被添加到一个临时变量RSI中,唯一的目的是在它上面创建一个MovingAverageSmoothed现在举个例子:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
    )

    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理订单和买入价格/佣金
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # 添加简单移动平均指标
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)

        # 用于绘图的指标
        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]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order*ex.e**cuted.price,
                     order*ex.e**cuted.value,
                     order*ex.e**cuted.comm))

                self.buyprice = order*ex.e**cuted.price
                self.buycomm = order*ex.e**cuted.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order*ex.e**cuted.price,
                          order*ex.e**cuted.value,
                          order*ex.e**cuted.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):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查订单是否挂起。。。如果是,我们无法发送第二个
        if self.order:
            return

        # 检查我们是否在市场上
        if not self.position:
            # 还没有。。。我们可能会做多如果。。。
            if self.dataclose[0] > self.sma[0]:
                # 买,买,买!!! (应用所有可能的默认参数)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])

                # 跟踪创建的订单以避免第二个订单
                self.order = self.buy()
        else:
            if self.dataclose[0] < self.sma[0]:
                # 卖,卖,卖!!! (应用所有可能的默认参数)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                # 跟踪创建的订单以避免第二个订单
                self.order = self.sell()


if __name__ == '__main__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    cerebro.addstrategy(TestStrategy)

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

    # 创建一个数据槽
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        # 不接收这个日期更早的数据
        fromdate=datetime.datetime(2000, 1, 1),
        # 不接收晚于这个日期的数据
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(1000.0)

    # 根据stake添加一个固定下单量
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # 设定佣金
    cerebro.broker.setcommission(commission=0.0)

    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行所有命令
    cerebro.run()

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # Plot the result
    cerebro.plot()

执行后,输出为:

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

即使策略逻辑没有改变,最终结果也发生了改变。这是因为逻辑没有应用到相同数量的K线上。

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

图表:

backtrader自定义绘图,backtrader从入门到精通

11. 让我们进行优化

许多关于交易的书都说每个市场和每个交易的股票(或商品或……)有不同的周期。没有普遍适用的。在绘图样本之前,当策略开始使用指标时,期间默认值为15根K线。这是一个策略参数,可用于优化,以改变参数的值,并确定哪一个更适合市场。

注意: 有大量关于优化及其相关利弊的文献。但建议始终指向同一个方向:不要过度优化。如果一个交易思路不健全,优化可能会最终产生一个正面的结果,该结果仅对经过回溯测试的数据集有效。

下面对样例进行修正,以优化简单移动平均的周期。为清楚起见,有关买卖订单的任何输出已被删除现在举个例子:

from __future__ import (absolute_import, division, print_function, unicode_literals)
import datetime  # 用于管理日期时间
import os.path  # 来管理路径
import sys  # 用于找到脚本名称(argv[0])

# 导入BackTrader平台
import backtrader as bt

# 创建一个策略y
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
        ('printlog', False),
    )

    def log(self, txt, dt=None, doprint=False):
        '''此策略的日志记录功能'''
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存对收盘价线最新数据的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理订单和买入价格/佣金
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # 添加简单移动平均指标
        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]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order*ex.e**cuted.price,
                     order*ex.e**cuted.value,
                     order*ex.e**cuted.comm))

                self.buyprice = order*ex.e**cuted.price
                self.buycomm = order*ex.e**cuted.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order*ex.e**cuted.price,
                          order*ex.e**cuted.value,
                          order*ex.e**cuted.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):
        # 引用的收盘价的日志
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查订单是否挂起。。。如果是,我们无法发送第二个
        if self.order:
            return

        # 检查我们是否在市场上
        if not self.position:
            # 还没有。。。我们可能会做多如果。。。
            if self.dataclose[0] > self.sma[0]:
                # 买,买,买!!! (应用所有可能的默认参数))
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                # 跟踪创建的订单以避免第二个订单
                self.order = self.buy()
        else:

            if self.dataclose[0] < self.sma[0]:
                # 卖,卖,卖!!! (应用所有可能的默认参数)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # 跟踪创建的订单以避免第二个订单
                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__':
    # 创建一个大脑实例
    cerebro = bt.Cerebro()

    # 添加一个策略
    strats = cerebro.optstrategy(
        TestStrategy,
        maperiod=range(10, 31))

    # 数据保存在样本的一个子文件夹中。我们需要找到脚本的位置
    # 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')

    # 创建一个数据槽
    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)

    # 把数据槽添加到大脑引擎中
    cerebro.adddata(data)

    # 设定我们希望的初始金额
    cerebro.broker.setcash(1000.0)

    # 根据stake添加一个固定下单量
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

    # 设定佣金
    cerebro.broker.setcommission(commission=0.0)

    # 运行所有命令
    cerebro.run(maxcpus=1)

本例不再调用addstrategy方法以向cerebro添加stratey类,而是调用optstrategy。同时不是传递一个值,而是传递一系列的值。添加了一个“Strategy”钩子,即stop方法,当数据跑完且回溯测试结束时将调用该方法。它用于在代理中打印投资组合的最终净值(之前在cerebro完成)系统将对范围内的每个值执行策略。将输出以下内容:

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

结果:

  • 均线周期低于18,策略(无佣金)亏损
  • 18至26(均含)周期,策略可盈利
  • 26周期以上,又开始亏损而该策略对此数据的最佳周期为:
  • 20根K线,赢78个单位,超过1,000美元╱欧元(7.8%)

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

12. 结论

增量样本展示了如何从一个简单的脚本过渡到一个完全工作的交易系统,该系统甚至可以绘制结果并进行优化。为了提高获胜的机会,我们可以做更多的工作:

  • 自定指标创建指标很容易(甚至绘图也很容易)
  • 单量控制对许多人来说,资金管理是成功的关键
  • 订单类型(限制、停止、停止限制)
  • 其他

为确保上述所有项目均能得到充分利用,文件提供了对这些项目(及其他主题)的深入了解查阅目录并继续阅读…和研发。

祝你好运