快速入门#

使用平台#

让我们通过一系列例子来介绍(从一个几乎空白的例子到一个完整的策略),但首先粗略解释一下使用 backtrader 时的两个基本概念。

  1. 线

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

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

    • 开盘价、最高价、最低价、收盘价、成交量、持仓量

    时间上的一系列“开盘价”是一条线。因此,一个数据源通常有6条线。

    如果我们还考虑“日期时间”(实际上是单个点的参考),我们可以计算出7条线。2. 索引0方法

当访问一行中的值时,当前值通过索引 0 访问。

而且,“last”输出值通过 -1 访问。这符合Python的惯例,用于迭代器(一行可以迭代,因此是一个可迭代对象)中,索引 * -1* 用于访问可迭代对象/数组的“最后”一个item。

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

因此,在索引 0 紧随 * -1* 之后的情况下,它用于访问当前时刻的值。

在考虑这一点的基础上,如果我们想象一个在初始化期间创建的简单移动平均线策略:

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

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

av = self.sma[0]

不需要知道有多少个条/分钟/天/月已经处理,因为“0”唯一地标识当前时刻。按照Python的传统,可以使用*-1*来访问“最后”的输出值:

previous_value = self.sma[-1]

当然,可以使用-2、-3等来访问前面的输出值。

从0到100:样本#

基本设置#

让我们开始吧。

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

执行后的输出为:

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

在这个例子中: - 导入了backtrader库

  • 实例化了Cerebro引擎

  • 告诉生成的 cerebro 实例去*运行*(遍历数据)

  • 打印出结果

虽然看起来不是很多,但是我们来明确一些显示的内容:

  • Cerebro引擎在后台创建了一个 broker 实例

  • 该实例已经有一些现金可以开始交易了

这种在平台中背后进行的broker实例化是为了简化用户的生活而设置的常标属性。如果用户没有设置broker,将会默认设置一个。

在某些经纪人那里,开始时的金额通常都是1万单位。

设置资金#

在金融世界中,只有”输家”才从一开始就有1万。让我们改变现金量,再次运行示例。

任务完成。让我们进入动荡的水域。

添加数据源#

现金很有趣,但所有这一切的目的是让自动化策略能够不费吹灰之力地通过操作一种我们称之为 数据源 的资产来使现金增值

因此… 没有 数据源没意思 。 让我们将一个数据源添加到逐渐增长的示例中。

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

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

执行后的输出为:

初始组合价值:1000000.00 最终组合价值:1000000.00

由于我们新增了以下内容,所以样板代码略有增加:

  • 查找示例脚本所在的位置,以便能够找到示例的 数据源 文件- 通过使用 datetime 对象对 Data Feed 中的数据进行过滤,我们将对其进行操作。

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

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

注意

Yahoo Online以日期降序发送CSV数据,这不是标准约定。 reversed=True 参数考虑了文件中CSV数据已经 反向 和具有标准预期日期升序的情况。

我们的第一个策略#

现金在 broker 中, Data Feed 也在那里。看来冒险业务就在拐角处。

让我们加入一个策略并打印每天的”Close”价格(柱状图)。

DataSeriesData Feeds 中的底层类)对象具有用于访问众所周知的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)
        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 __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-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 list ,可以按照插入的顺序访问数据。

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

  • self.dataclose = self.datas[0].close 保留了对 收盘价线 的引用。之后只需要一级间接引用即可访问收盘价的数值。

  • 策略的 next 方法将在系统的时钟(self.datas[0])的每个柱上调用。这在其他因素(如指标)开始产生输出之前是正确的。稍后详细了解。

给策略添加一些逻辑#

让我们尝试一些我们通过观察一些图表得出的疯狂想法

  • 如果价格连续下降了3个会话… 买买买!!!

发出了多个创建买入订单的指令,我们的投资组合价值下降了。显然有几个重要的事情是缺失的。

  • 虽然创建了订单,但并不知道是否被执行、何时执行以及执行价格。

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

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

  • 如果没有指定其他资产,self.datas[0](主要数据,也称为系统时钟)是目标资产。

  • 股份由 position sizer 在后台提供,其使用固定的股份,默认为”1”。稍后将进行修改

  • 订单是以市价进行执行的。经纪人(在之前的例子中显示)使用下一个柱(bar)的开盘价进行执行,因为这是当前检验柱之后的第一笔交易。

  • 目前订单没有任何佣金(稍后会详细介绍)

不仅仅购买…还要卖出#

了解了如何进入市场(做多)之后,还需要一个“离场概念”,并且需要知道策略当前是否在市场中。

  • 幸运的是,Strategy对象提供对默认数据源的 position 属性的访问

  • buy 和*sell*方法返回尚未执行的 已创建 订单

  • 订单状态的更改将通过 notify 方法通知策略。“退出策略”是一个很简单的概念:

  • 在过去的5个柱之后(第6个柱子),不论好坏都退出

请注意,这里没有暗示“时间”或“时间段”:柱子的数量。柱子可以表示1分钟、1小时、1天、1周或其他任何时间段。

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

此外,为了简化:

  • 只允许在市场中没有买单时才能发起买入订单

注意

“下一个”方法没有传递“柱索引”,因此不清楚如何理解5个柱是否已经过去,但这是通过python的方式进行建模的:对一个对象调用 len 方法,它会告诉您 lines 的长度。只需记录(保存在一个变量中)发生操作的长度,并查看当前长度是否相差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].close

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

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

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

执行后的输出是:

起始组合价值:100000.00 2000-01-03T00:00:00,结束,27.85 2000-01-04T00:00:00,结束,25.39 2000-01-05T00:00:00,结束,24.05 2000-01-05T00:00:00,买入信号,24.05 2000-01-06T00:00:00,买入成交,23.61 2000-01-06T00:00:00,结束,22.63 2000-01-07T00:00:00,结束,24.37 2000-01-10T00:00:00,结束,27.29 2000-01-11T00:00:00,结束,26.49 2000-01-12T00:00:00,结束,24.90 2000-01-13T00:00:00,结束,24.77 2000-01-13T00:00:00,卖出信号,24.77 2000-01-14T00:00:00,卖出成交,25.70 2000-01-14T00:00:00,结束,25.18 … … … 2000-12-15T00:00:00,卖出信号,26.93 2000-12-18T00:00:00,卖出成交,28.29 2000-12-18T00:00:00,结束,30.18 2000-12-19T00:00:00,结束,28.88 2000-12-20T00:00:00,结束,26.88 2000-12-20T00:00:00,买入信号,26.88 2000-12-21T00:00:00,买入成交,26.23 2000-12-21T00:00:00,结束,27.82 2000-12-22T00:00:00,结束,30.06 2000-12-26T00:00:00,结束,29.17 2000-12-27T00:00:00,结束,28.94 2000-12-28T00:00:00,结束,29.29 2000-12-29T00:00:00,结束,27.41 2000-12-29T00:00:00,卖出信号,27.41 最终组合价值:100018.53当系统赚了钱时…一定出现了问题

经纪人说:给我看看钱!#

这些钱被称为“佣金”。

让我们为每一次交易(无论是买入还是卖出)添加一个合理的 0.1% 的佣金率(是的,经纪人很贪婪…)

一条简单的代码就足够了:

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

然后,我们想在买入/卖出周期之后查看有无佣金的盈亏。

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

执行后的输出为:

起始投资组合价值:100000.00
2000-01-03T00:00:00, 收盘价, 27.85
2000-01-04T00:00:00, 收盘价, 25.39
2000-01-05T00:00:00, 收盘价, 24.05
2000-01-05T00:00:00, 创建买入订单, 24.05
2000-01-06T00:00:00, 买入订单成交, 价格: 23.61, 成本: 23.61, 佣金 0.02
2000-01-06T00:00:00, 收盘价, 22.63
2000-01-07T00:00:00, 收盘价, 24.37
2000-01-10T00:00:00, 收盘价, 27.29
2000-01-11T00:00:00, 收盘价, 26.49
2000-01-12T00:00:00, 收盘价, 24.90
2000-01-13T00:00:00, 收盘价, 24.77
2000-01-13T00:00:00, 创建卖出订单, 24.77
2000-01-14T00:00:00, 卖出订单成交, 价格: 25.70, 成本: 25.70, 佣金 0.03
2000-01-14T00:00:00, 交易盈利, 毛收益 2.09, 净收益 2.04
2000-01-14T00:00:00, 收盘价, 25.18
...
...
...
2000-12-15T00:00:00, 创建卖出订单, 26.93
2000-12-18T00:00:00, 卖出订单成交, 价格: 28.29, 成本: 28.29, 佣金 0.03
2000-12-18T00:00:00, 交易亏损, 毛亏损 -0.06, 净亏损 -0.12
2000-12-18T00:00:00, 收盘价, 30.18
2000-12-19T00:00:00, 收盘价, 28.88
2000-12-20T00:00:00, 收盘价, 26.88
2000-12-20T00:00:00, 创建买入订单, 26.88
2000-12-21T00:00:00, 买入订单成交, 价格: 26.23, 成本: 26.23, 佣金 0.03
2000-12-21T00:00:00, 收盘价, 27.82
2000-12-22T00:00:00, 收盘价, 30.06
2000-12-26T00:00:00, 收盘价, 29.17
2000-12-27T00:00:00, 收盘价, 28.94
2000-12-28T00:00:00, 收盘价, 29.29
2000-12-29T00:00:00, 收盘价, 27.41
2000-12-29T00:00:00, 创建卖出订单, 27.41
最终投资组合价值: 100016.98多亏国王保佑!!!系统仍然赚了钱。

在继续前,让我们注意一下通过过滤“OPERATION PROFIT”行得到的信息:

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

将”NET”利润相加,最终值为:

` 15.83 `

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

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

显然, 15.83 不等于 16.98 。这里没有任何错误。 15.83 的”NET”利润已经是实际收入了。

不幸的是(或者说幸运的是,可以更好地理解这个平台),在最后一天的 数据源 中有一个未平仓的仓位。即使发送了一个卖出操作… 它尚未被执行。

经纪人计算的”Final Portfolio Value”考虑了2000-12-29的“收盘”价格。实际执行价格将在下一个交易日设置,即2001-01-02。扩展 数据源 以考虑这一天的输出为:2001-01-02T00:00:00,销售执行,价格:27.87,成本:27.87,佣金:0.03 2001-01-02T00:00:00,操作盈利,总盈利:1.64,净盈利:1.59 2001-01-02T00:00:00,收盘,价格:24.87 2001-01-02T00:00:00,创建购买订单,价格:24.87 最终组合价值:100017.41

现在将上一次净盈利加到已完成操作的净盈利中:

15.83 + 1.59 = 17.42

这是策略开始时初始价值100000货币单位之上的额外投资组合。

自定义策略:参数#

在策略中硬编码一些值并且无法轻松更改将会很不实用。*参数*可以派上用场。

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

params = ((‘myparam’, 27), (‘exitbars’, 5),)

这是一个标准的Python元组,其中包含一些元组,对于某些人来说,以下方式可能更加吸引人:

params = (

(‘myparam’, 27), (‘exitbars’, 5),

)无论在将策略添加到Cerebro引擎时使用哪种格式化参数化策略都是允许的:

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

注意

下面的 setsizing 方法已经被弃用。这个内容被保留在这里,供查看旧样本的人参考。源代码已经更新为使用:

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

请阅读关于 sizer 的部分

使用策略中的参数很容易,因为它们存储在”params”属性中。例如,如果我们想固定设置资金量,则可以将stake参数传递给 position sizer ,如下所示durint __init__:

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

我们也可以通过一个 stake 参数和 self.params.stake 作为值来调用 buy 和*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())

执行后的输出为:

初始投资组合价值:100,000.00 2000-01-03T00:00:00, 收盘价, 27.85 2000-01-04T00:00:00, 收盘价, 25.39 2000-01-05T00:00:00, 收盘价, 24.05 2000-01-05T00:00:00, 创建买入信号, 24.05 2000-01-06T00:00:00, 买入成交, 数量 10, 价格: 23.61, 成本: 236.10, 手续费 0.24 2000-01-06T00:00:00, 收盘价, 22.63 … … … 2000-12-20T00:00:00, 创建买入信号, 26.88 2000-12-21T00:00:00, 买入成交, 数量 10, 价格: 26.23, 成本: 262.30, 手续费 0.26 2000-12-21T00:00:00, 收盘价, 27.82 2000-12-22T00:00:00, 收盘价, 30.06 2000-12-26T00:00:00, 收盘价, 29.17 2000-12-27T00:00:00, 收盘价, 28.94 2000-12-28T00:00:00, 收盘价, 29.29 2000-12-29T00:00:00, 收盘价, 27.41 2000-12-29T00:00:00, 创建卖出信号, 27.41 最终投资组合价值:100,169.80

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

将赌注乘以10后,显而易见的是:盈亏也被乘以10。盈余现在不是 16.98 ,而是 169.80

添加指标#

听说了*指标*之后,下一个任何人都会将其添加到策略中。 毫无疑问,它们一定比简单的“3个连续下跌”策略要好得多。

受PyAlgoTrade中的一个示例的启发,我们使用一个简单移动平均线来制定一种策略。

  • 如果收盘价大于平均值,以市价“买入”

  • 如果处于市场中,则在收盘价小于平均值时“卖出”

  • 市场中只允许进行一次有效操作大部分的现有代码可以保留。让我们在 __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())

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

  • 它不再是 2000-01-03 ,也不再是年份为2K的第一个交易日。

    它是2000-01-24… 谁偷了我的奶酪?

这些缺失的日子并不存在。平台已经适应了新的情况:

  • 在策略中添加了一个指标(SimpleMovingAverage)。- 这个指标需要X个柱来产生结果,在这个例子中是:15个

  • 2000-01-24是第15个柱出现的那一天

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

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

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

执行后的输出为:

起始投资组合价值:1000.00 2000-01-24T00:00:00,收盘价,25.55 2000-01-25T00:00:00,收盘价,26.61 2000-01-25T00:00:00,买入信号,26.61 2000-01-26T00:00:00,执行买入,数量10,价格:26.76,成本:267.60,佣金0.00 2000-01-26T00:00:00,收盘价,25.96 2000-01-27T00:00:00,收盘价,24.43 2000-01-27T00:00:00,卖出信号,24.43 … … … 2000-12-20T00:00:00,卖出信号,26.88 2000-12-21T00:00:00,执行卖出,数量10,价格:26.23,成本:262.30,佣金0.00 2000-12-21T00:00:00,操作盈利,毛利 -20.60,净利 -20.60 2000-12-21T00:00:00,收盘价,27.82 2000-12-21T00:00:00,买入信号,27.82 2000-12-22T00:00:00,执行买入,数量10,价格:28.65,成本:286.50,佣金0.00 2000-12-22T00:00:00,收盘价,30.06 2000-12-26T00:00:00,收盘价,29.17 2000-12-27T00:00:00,收盘价,28.94 2000-12-28T00:00:00,收盘价,29.29 2000-12-29T00:00:00,收盘价,27.41 2000-12-29T00:00:00,卖出信号,27.41 最终投资组合价值:973.90

以国王之名!!!一个赢利的系统变成了一个亏损的系统…而且还没有佣金。可能仅仅是 添加一个指标 并不是万能的灵丹妙药。

注意

使用 PyAlgoTrade 得到了略有不同的结果(稍偏离)。查看整个打印输出发现,有些操作并不完全相同。罪魁祸首又是常犯的嫌疑犯: 四舍五入

在将调整后的收盘价应用到数据源值时,PyAlgoTrade 不会将数据源的值四舍五入。由 backtrader 提供的Yahoo数据源在应用调整后,将值四舍五入到小数点后两位。在打印值时,一切似乎都一样,但很明显,有时第5位小数起到了作用。

四舍五入到小数点后两位似乎更加真实,因为市场交易所每个资产只允许一定的小数位数(通常为2位小数)。

备注

Yahoo数据源(从版本 1.8.11.99 开始)允许指定是否进行四舍五入以及小数位数

视觉检查:绘图#

将系统在每个柱状实例的实际位置打印出来或记录下来是很好的,但是人类往往是 视觉化 的,因此提供一个图表视图似乎是正确的选择。

备注

若要进行绘图,需要安装 matplotlib

一次又一次地,绘图的默认设置都是为了帮助平台用户。绘图实际上只需要一行操作:

cerebro.plot()

当确保已调用 cerebro.run() 后,位置肯定已经更新。

为了展示自动绘图功能和几个简单的自定义操作,将执行以下操作:- 将添加第二个MovingAverage (Exponential)指标。默认情况下,它将与数据一起绘制。 - 将添加第三个MovingAverage (Weighted)指标。自定义设置为在单独的图上绘制(即使不合理)。 - 将添加一个Stochastic (Slow)指标。默认设置不变。 - 将添加一个MACD指标。默认设置不变。 - 将添加一个RSI指标。默认设置不变。 - 将在RSI上应用一个MovingAverage (Simple)指标。默认设置不变(它将与RSI一起绘制)。 - 将添加一个AverageTrueRange指标。更改默认设置以避免将其绘制出来。

在Strategy的__init__方法中添加的全部内容:

`python # 用于绘图显示的指标 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。

现在来看示例代码:

```python .. literalinclude:: ./quickstart10.py

language:

python

lines:

21-

```

执行后的输出为:

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

最终结果已经改变,尽管逻辑没有改变 。这是事实,但逻辑并没有应用到相同数量的数据条上。.. 注意:: 如前所述,当所有指标准备好生成一个值时,平台将首先调用 next 。在这个绘图示例中(在图表中非常清楚),MACD是最后一个完全准备好的指标(所有3条线都产生一个输出)。第一个买入订单不再在2000年1月计划进行,而是接近于2000年2月末。

图表:

让我们进行优化#

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

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

注意

有许多关于优化和相关优缺点的文献。但是建议始终指向同一个方向:不要过度优化。如果交易思想不合理,优化可能最终会产生一个仅对回测数据集有效的积极结果。

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

现在的例子:

其中一个“策略”挂钩是 stop 方法,当数据耗尽并且回测结束时会调用该方法。它用于打印经纪人中投资组合的最终净值(之前在Cerebro中完成这个任务)。

该系统会对每个范围内的值执行策略。输出如下:

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

结果:

  • 对于18以下的期数,策略(无佣金)会亏钱。

  • 对于18至26(包括18和26)之间的期数,策略会赚钱。

  • 26以上的期数又会亏钱。

此策略在给定数据集下的获胜期数为:

  • 20个周期,相对于1000 $/ €赢得78.00单位 (7.8%)

注意

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

结论 =====增量样本展示了如何从一个简单的脚本逐步发展成一个完整的交易系统,该系统甚至还可以绘制结果并进行优化。

还有很多可以尝试来提高获胜机会的方法:

  • 自定义指标

    创建指标很简单(甚至可以很容易地绘制它们)

  • 仓位控制

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

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

  • 其他一些功能

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

请查看目录并继续阅读…并发展。祝你好运。