Backtrader官方中文文档:第三章Quickstart Guide快速入门

本文档参考backtrader官方文档,是官方文档的完整中文翻译,可作为backtrader中文教程、backtrader中文参考手册、backtrader中文开发手册、backtrader入门资料使用。

快速入门

使用平台

让我们通过一系列的示例(从几乎空白到完全成熟的策略)来运行,但在此之前,让我们大致解释一下使用backtrader时的两个基本概念。

  1. Lines线

    数据源、指标和策略都有线

    一条线是一系列点的连续性,这些点连接在一起形成这条线。当谈论市场时,数据源通常每天有以下一组点:

    • Open, High, Low, Close, Volume, OpenInterest (开盘价、最高价、最低价、收盘价、成交量、持仓量)

    时间序列中的开盘价是一条线。因此,数据源通常有6条线。

    如果我们还考虑DateTime(这是单个点的实际参考),我们可以计算出7条线。

  2. 索引0的用法

    在访问线中的值时,使用索引:0访问当前值

    last输出值使用*-1访问。这符合Python可迭代对象的惯例(线可以迭代,因此是可迭代的),其中索引-1*用于访问可迭代/数组的last项。

    在我们的情况下,访问的是最后一个输出值。

    因此,索引0紧随*-1*之后,用于访问线中的当前时刻。

有了这个想法,如果我们想在初始化期间创建一个简单移动平均线的策略:

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

访问此移动平均线的当前值的最简单和最简单的方法是:

    av = self.sma[0]

无需知道已处理了多少个bar/分钟/天/月,因为0是当前时刻的唯一标识。

按照Python的传统,使用*-1*访问last输出值:

    previous_value = self.sma[-1]

当然,可以使用-2、-3等访问早期的输出值。

从0到100:一步一步的演示

基本设置

让我们开始吧。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
# 导入Backtrader
import backtrader as bt

if __name__ == '__main__':
    # 创建一个Cerebro引擎实例
    cerebro = bt.Cerebro()
    # 打印初始资金
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    # 执行回测
    cerebro.run()
    # 打印最终资金
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

上面示例代码实现(虽然什么也没做,但是流程走完了):

  • 导入Backtrader
  • 实例化Cerebro引擎
  • 打印初始资金
  • 执行回测
  • 打印最终资金

代码的执行结果(小白注意不用拷贝执行,这是运行后得到的内容):

Starting Portfolio Value: 10000.00
Final Portfolio Value: 10000.00

在这个例子中:

  • 导入了backtrader

  • 实例化了Cerebro引擎

  • 生成的cerebro实例被告知run(循环数据)

  • 并打印出了结果

虽然看起来不起眼,但让我们明确指出一些东西:

  • Cerebro引擎在后台创建了一个broker实例
  • 该实例已经有一些现金可以开始交易

这种幕后经纪人实例化是平台中的一个常见特征,以简化用户的生活。如果用户没有设置经纪人,则会放置一个默认经纪人。

而10K的测试货币量是回测常用的一个货币价值。

设置现金

在金融世界中,只有输家才从10k开始。让我们改变现金并再次运行示例。

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

本节任务完成,让我们提升任务的难度。

添加一个数据源

有现金很有趣,但所有这一切的目的是让策略在我们提供的数据源资产上自动操作,而不需要动手。

因此…没有数据源->没有乐趣。让我们将其添加到不断增长的示例中。

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实体
    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
    cerebro.adddata(data)

    # 设置我们所需的现金起始值
    cerebro.broker.setcash(100000.0)

    # 打印出起始条件
    print('起始投资组合价值:%.2f' % cerebro.broker.getvalue())

    # 运行所有
    cerebro.run()

    # 打印出最终结果
    print('最终投资组合价值:%.2f' % cerebro.broker.getvalue())

执行后的输出为:

起始投资组合价值:100000.00
最终投资组合价值:100000.00

模板代码的数量略有增加,因为我们添加了:

  • 找出我们的示例脚本所在的位置,以便能够定位示例数据源文件

  • datetime对象以过滤我们将要操作的数据源中的数据

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

输出没有改变,如果它改变了,那将是一个奇迹。

注意:Yahoo Online以日期降序发送CSV数据,这不是标准约定。reversed=True参数考虑到CSV数据在文件中已经被反转并具有标准预期的日期升序。

我们的第一个策略

现金在broker中,Data Feed也在那里。我们马上就可以跑生意了。

让我们将策略放入方程式中,并打印每天(bar)的Close价格。

DataSeriesData Feeds中的基础类)对象具有访问众所周知的OHLC(Open High Low Close)每日值的别名。这应该简化我们的打印逻辑的创建。

# 导入所需模块
from __future__ import (absolute_import, division, print_function, unicode_literals)
import datetime  # 日期时间模块
import os.path  # 路径模块
import sys  # 系统模块

# 导入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):
        # 保留对数据序列中`close`线的引用
        self.dataclose = self.datas[0].close

    def next(self):
        # 记录数据序列的收盘价
        self.log('收盘价, %.2f' % self.dataclose[0])

if __name__ == '__main__':
    # 创建cerebro实体
    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
    cerebro.adddata(data)

    # 设置初始资金
    cerebro.broker.setcash(100000.0)

    # 打印初始条件
    print('初始资产价值: %.2f' % cerebro.broker.getvalue())

    # 运行策略
    cerebro.run()

    # 打印最终结果
    print('最终资产价值: %.2f' % cerebro.broker.getvalue())


执行后的输出为::

初始资产价值: 100000.00
2000-01-03, 收盘价, 26.27
2000-01-04, 收盘价, 23.95
  ...
  ...
  ...
2000-12-28, 收盘价, 27.63
2000-12-29, 收盘价, 25.85
最终资产价值: 100000.00

有人说股票市场是冒险的生意,但似乎并非如此。

让我们解释一些神奇的事:

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

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

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

  • self.dataclose = self.datas[0].close保留对close line的引用。只需要一个间接级别即可访问关闭值。

  • 策略next方法将在系统时钟(self.datas[0])的每个柱上调用。这是真实的,直到其他事情进入比如indicators,它需要一些柱才能开始产生输出。稍后再说。

给策略添加执行逻辑

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

  • 如果价格连续下跌3个会话… 买买买!!!
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # 导入日期时间库
import os.path  # 导入路径管理库
import sys  # 导入系统库

# 导入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):
        # 保留对数据序列中`close`线的引用
        self.dataclose = self.datas[0].close

    def next(self):
        # 记录数据序列的收盘价
        self.log('Close, %.2f' % self.dataclose[0])

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

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

                # 买入
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()


if __name__ == '__main__':
    # 创建cerebro实体
    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
    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, 26.27
2000-01-04, Close, 23.95
2000-01-05, Close, 22.68
2000-01-05, BUY CREATE, 22.68
2000-01-06, Close, 21.35
2000-01-06, BUY CREATE, 21.35
...
...
...
2000-12-20, BUY CREATE, 25.35
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-27, BUY CREATE, 27.30
2000-12-28, Close, 27.63
2000-12-29, Close, 25.85
Final Portfolio Value: 99740.45

发出了几个买入订单,我们的组合价值减少了。显然缺少了一些重要的事情。

  • 订单已创建,但不知道是否执行,何时执行以及以什么价格执行。

    下一个示例将在此基础上构建,通过监听订单状态的通知来解决这个问题。

好奇的读者可能会问买了多少股票,买了哪些资产以及如何执行订单。在可能的情况下(在这种情况下),平台填补了这些空白:

  • self.datas[0](主数据,即系统时钟)是操作买卖的目标资产,如果没有指定其他资产的话(即默认操作datas[0])
  • 买卖股份数量由position sizer在幕后提供,使用固定股份,即默认值1。它将在稍后的示例中进行修改
  • 订单是市价执行的。经纪人(在前面的示例中显示)使用下一个条的开盘价执行此操作,因为那是当前正在检查的条下一个tick。
  • 订单迄今为止没有任何佣金(稍后会详细介绍)

不仅要买…还要卖

在了解如何进入市场(多头)之后,需要一个退出概念,即给策略添加从市场退出的逻辑(卖股)。

  • 幸运的是,Strategy对象提供了对默认数据源position属性的访问
  • 方法buysell返回已创建(尚未执行)的订单
  • 订单状态的更改将通过notify方法通知策略

退出概念将是一个简单的概念:

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

    请注意,1个bar的bar可以表示1分钟,1小时,1天,1周或其他任何长度的时间单位。

    尽管我们知道数据源是每日的,但策略不会对此做出任何假设。

此外,为了简化:

  • 仅在尚未进入市场时才允许购买订单

注意:next方法没有传递bar索引,因此似乎不清楚如何理解何时可能已经过去了5个bar,但是这已经以pythonic的方式建模:在对象上调用len,它将告诉您其lines的长度。
只需在操作中记录(保存在变量中)已经迭代过的bar的长度,然后查看当前长度是否相距5个bar。

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

import datetime  # 导入datetime模块
import os.path  # 导入os.path模块
import sys  # 导入sys模块

# 导入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):
        # 保留对数据序列中`close`线的引用
        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('买入已执行, %.2f' % order.executed.price)
            elif order.issell():
                self.log('卖出已执行, %.2f' % order.executed.price)

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('订单已取消/保证金不足/拒绝')

        # 记录:没有待处理订单
        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]:
                    # 当前收盘价小于前一个收盘价

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

                        # 买入
                        self.log('买入创建, %.2f' % self.dataclose[0])

                        # 记录已创建的订单,以避免产生第二个订单
                        self.order = self.buy()

        else:
            # 已经在市场中...我们可能会卖出
            if len(self) >= (self.bar_executed + 5):
                # 卖出
                self.log('卖出创建, %.2f' % self.dataclose[0])

                # 记录已创建的订单,以避免第二个订单
                self.order = self.sell()


if __name__ == '__main__':
    # 创建cerebro实体
    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
    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, 26.27
2000-01-04, Close, 23.95
2000-01-05, Close, 22.68
2000-01-05, 买入创建, 22.68
2000-01-06, 买入已执行, 22.27
...
...
2000-12-20, Close, 25.35
2000-12-20, 买入创建, 25.35
2000-12-21, 买入已执行, 24.74
2000-12-21, Close, 26.24
...
2000-12-28, Close, 27.63
2000-12-29, Close, 25.85
2000-12-29, 卖出创建, 25.85
Final Portfolio Value: 100017.52

系统赚了钱…一定有什么问题

券商说:打钱!

这笔钱叫做佣金

让我们为每次操作(买入和卖出)添加合理的*0.1%*佣金率(是的,经纪人很贪婪……)

只需要一行代码:

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

熟悉平台后,我们想在买卖周期之后查看有无佣金的利润或损失。

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

执行后的输出为:

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

上帝保佑!系统仍然赚了钱。

在继续之前,让我们通过过滤OPERATION PROFIT行来注意一些事情:

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

利润相加,最终数字为:

15.83(备注原文数字,实际执行代码结果和文档有差异,此处示意)

但系统在最后说了以下内容:

2000-12-29, SELL CREATE, 25.85
Final Portfolio Value: 100016.06

显然,15.83不是16.06。没有任何错误。15.83利润已经到手了。

不幸的是(或者幸运的是,为了更好地理解平台),在数据源最后一天有一个未平仓头寸。即使已经发送了SELL操作……它还没有被执行。

经纪人计算的最终组合价值考虑了2000-12-29的收盘价格。实际执行价格将在下一个交易日(即2001-1-3)设置。将数据源扩展到考虑这一天后,输出为:

2000-12-29, SELL CREATE, 25.85
2001-01-02, SELL EXECUTED, Price: 26.30, Cost: 24.74, Comm 0.03
2001-01-02, OPERATION PROFIT, GROSS 1.56, NET 1.51
2001-01-02, Close, 23.46
2001-01-02, BUY CREATE, 23.46
Final Portfolio Value: 100016.48

现在将前面的利润添加到已完成操作的利润中:

15.83 + 1.59 = 17.42(备注原文数字,实际执行代码结果和文档有差异,此处示意)

(忽略print语句中的舍入误差),这是初始100000货币单位以上的额外组合。

自定义策略:参数

在策略中硬编码一些值并没有什么实际意义,而且很难轻松更改它们。参数很有用。

定义参数很容易,看起来像这样:

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

这是一个标准的Python元组,里面有一些元组,以下可能更吸引人:

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

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

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

注意:下面的setsizing方法已弃用。这个内容是为了让任何人查看旧的源代码示例而保留的。源代码已经更新为使用:

# 已经弃用
cerebro.addsizer(bt.sizers.FixedSize, stake=10)``

可以阅读sizers相关章节查看更多。

在策略中使用参数很容易,因为它们存储在params属性中。如果我们例如想要设置固定的股份,我们可以在__init__期间将股份参数传递给position sizer

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

我们也可以使用stake参数和self.params.stake作为值来调用buysell

退出逻辑被修改:

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

考虑到所有这些,示例的演变如下:

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

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

执行后的输出为:

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: 222.70, Comm 0.22
...
...
...
2000-12-28, Close, 27.63
2000-12-29, Close, 25.85
2000-12-29, SELL CREATE, 25.85
Final Portfolio Value: 100160.58

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

将股份乘以10后,显然发生了以下情况:利润和损失乘以10。而不是16.58,剩余现金现在是165.80

添加指标

指标可以简单理解为:用于衡量市场趋势和价格变化的工具。指标可以是基于价格、成交量或其他市场数据的计算结果。

听说过指标后,下一步任何人都会添加一个指标到策略中。毫无疑问,它们一定比简单的*3个较低的收盘价*策略好得多。

受PyAlgoTrade中的一个示例启发,使用简单移动平均线的策略。

  • 如果收盘价大于平均值,则以市价买入
  • 如果在市场上,如果收盘价小于平均值,则卖出
  • 市场上只允许有1个活动操作

大部分现有的代码可以保持不变。让我们在__init__中添加平均值,并保留对它的引用:

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

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

注意:: 起始现金将为1000货币单位,以与PyAlgoTrade示例保持一致,并且不会应用佣金

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

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

  • 它不再是2K年的第一个交易日2000-01-03

    它是2000-01-24 …

缺失的天数并不不是真的缺失。而是平台已适应新的情况:

  • 已将指标(SimpleMovingAverage)添加到策略中。

  • 此指标需要X个条形图才能产生输出:例如:15

  • 2000-01-24是第15个条形图出现的日期

backtrader平台假定策略已经有了指标,有一个很好的理由,在决策过程中使用它。如果指标尚未准备好并产生值,则尝试做出决策是没有意义的。

  • 当所有指标已经达到产生值所需的最小周期时,next将首先被调用

  • 在示例中只有一个指标,但是策略可以有任意数量的指标。

执行后的输出为:

Starting Portfolio Value: 1000.00
2000-01-24, Close, 24.10
2000-01-25, Close, 25.10
2000-01-25, BUY CREATE, 25.10
2000-01-26, BUY EXECUTED, Price: 25.24, Cost: 252.40, Comm 0.00
...
...
...
2000-12-21, OPERATION PROFIT, GROSS -19.40, NET -19.40
2000-12-21, Close, 26.24
2000-12-21, BUY CREATE, 26.24
2000-12-22, BUY EXECUTED, Price: 27.02, Cost: 270.20, Comm 0.00
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: 975.60

一个获胜的系统变成了一个失败的系统…而且没有佣金。很可能仅仅添加一个指标,说明指标并不是万能药。

注意:在PyAlgoTrade中使用相同的逻辑和数据会产生略有不同的结果(略微偏离)。查看整个打印输出会发现一些操作不完全相同。罪魁祸首再次是通常的嫌疑人:四舍五入

PyAlgoTrade在将分裂的调整收盘价应用于数据源值时不会将其舍入到小数点后2位。

backtrader提供的Yahoo数据源将在应用调整后将值向下舍入到2位小数。在打印值时,一切似乎都是相同的,但很明显,有时第5位小数起作用。

向下舍入到2位小数似乎更为现实,因为Marke Exchange仅允许每个资产有一定数量的小数位数(通常为股票的2个小数位数)

注意:Yahoo数据源(从版本1.8.11.99开始)允许指定是否进行舍入以及舍入到多少位小数)

可视化检查:绘图

打印或记录系统在每个bar的实际执行情况是可以的,但人类是视觉动物,因此提供一个图表视图输出机制无疑是正确的。

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

再次,默认情况下绘图可帮助平台用户。绘图是一个仅有的1行操作:

cerebro.plot()

在调用cerebro.run()后,位置肯定是在那里。

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

  • 添加第二个移动平均线(指数)。默认情况下将其与数据一起绘制(就像第一个一样)。
  • 添加第三个移动平均线(加权)。自定义绘制在自己的图中(即使不明智)
  • 添加随机(慢)。默认情况下不更改。
  • 添加MACD。默认情况下不更改。
  • 添加RSI。默认情况下不更改。
  • 对RSI应用移动平均线(简单)。默认情况下不更改(将与RSI一起绘制)
  • 添加AverageTrueRange。更改默认值以避免绘制。

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

注意:即使indicators没有显式添加到策略的成员变量中(例如self.sma = MovingAverageSimple…),它们也将自动注册到策略中,并影响next的最小周期,并成为绘图的一部分。

在示例中,只有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()

执行后的输出为:

Starting Portfolio Value: 1000.00
2000-02-18, Close, 26.05
2000-02-22, Close, 26.38
2000-02-22, BUY CREATE, 26.38
2000-02-23, BUY EXECUTED, Price: 26.77, Cost: 267.70, Comm 0.00
2000-02-23, Close, 28.05
...
...
...
2000-12-22, BUY EXECUTED, Price: 27.02, Cost: 270.20, Comm 0.00
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: 982.30

最终结果已更改,即使逻辑没有。这是真的,但逻辑没有应用于相同数量的bar。

注意:如前所述,平台将在所有指标准备好生成值时首先调用next。在这个绘图示例中(在图表中非常清晰),MACD是最后一个完全准备好的指标(所有3条线都产生输出)。第一个BUY订单不再在2000年1月期间计划,而是接近2000年2月底。

图表:
请添加图片描述

让我们优化

许多交易书籍都说每个市场和每个交易的股票(或商品或…)都有不同的节奏。没有一种适合所有人的东西。

在绘图示例之前,当策略开始使用指标时,期间的默认值为15个bar。这是一个策略参数,可以在优化中使用该参数的值并查看哪个更适合市场。

注意:有很多关于优化和相关利弊的文献。但是建议总是指向同一个方向:不要过度优化。如果交易想法不合理,则优化可能会产生仅对回测数据集有效的正面结果。

示例已修改为优化简单移动平均线的期间。为了清晰起见,已删除与买入/卖出订单相关的任何输出

现在的例子:

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

不是调用addstrategy将策略类添加到Cerebro中,而是调用optstrategy。而不是传递一个值,而是传递一系列值。

添加了Strategy钩子之一,stop方法,当数据用尽并且回测结束时将调用该方法。它用于在经纪人的投资组合中打印最终净值(之前在Cerebro中完成)

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

2000-12-29, (MA Period 10) Ending Value 877.50
2000-12-29, (MA Period 11) Ending Value 878.70
2000-12-29, (MA Period 12) Ending Value 839.80
2000-12-29, (MA Period 13) Ending Value 899.90
2000-12-29, (MA Period 14) Ending Value 902.50
2000-12-29, (MA Period 15) Ending Value 975.60
2000-12-29, (MA Period 16) Ending Value 961.90
2000-12-29, (MA Period 17) Ending Value 952.60
2000-12-29, (MA Period 18) Ending Value 1011.00
2000-12-29, (MA Period 19) Ending Value 1039.40
2000-12-29, (MA Period 20) Ending Value 1073.20
2000-12-29, (MA Period 21) Ending Value 1055.10
2000-12-29, (MA Period 22) Ending Value 1057.60
2000-12-29, (MA Period 23) Ending Value 1021.50
2000-12-29, (MA Period 24) Ending Value 1018.80
2000-12-29, (MA Period 25) Ending Value 1012.40
2000-12-29, (MA Period 26) Ending Value 998.30
2000-12-29, (MA Period 27) Ending Value 983.10
2000-12-29, (MA Period 28) Ending Value 976.90
2000-12-29, (MA Period 29) Ending Value 984.20
2000-12-29, (MA Period 30) Ending Value 980.80

结果:

  • 对于小于18的期间,策略(无佣金)会亏钱。
  • 对于18到26之间的期间(包括两者),策略会赚钱。
  • 26以上再次失去了钱。

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

  • 20个bar,赢得1000 $的78.00个单位(7.8%)

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

结论

示例逐步的展示了如何从一个简单的脚本到一个完整的交易系统,甚至可以绘制结果并进行优化。

可以做很多事情来提高获胜的机会:

  • 自定义指标

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

  • 仓位管理

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

  • 订单类型(限价,止损,止损限价)

  • 其他一些

为了确保可以充分利用上述所有项目,文档提供了对它们(和其他主题)的深入了解。

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

祝你好运!

猜你喜欢

转载自blog.csdn.net/windanchaos/article/details/130945718