BackTrader 中文文档(二十一)

原文:www.backtrader.com/

目标订单

原文:www.backtrader.com/blog/posts/2016-09-02-target-orders/target-orders/

直到版本 1.8.10.96,通过Strategy方法:买入和卖出,在backtrader上实现了智能的投注。一切都是关于向方程中添加一个 Sizer,它负责赌注的大小。

Sizer无法决定操作是买入还是卖出。这意味着需要引入一个新概念,在其中添加一个小的智能层来做出这样的决定。

这就是Strategy中的order_target_xxx方法家族发挥作用的地方。受到zipline中的方法的启发,这些方法提供了简单指定最终目标的机会,目标可以是:

  • size -> 特定资产组合中的股票、合约数量

  • value -> 投资组合中资产的货币单位价值

  • percent -> 当前投资组合中资产的百分比价值

注意

方法的参考可以在策略参考中找到。简而言之,这些方法使用与buysell相同的签名,除了参数size被参数target替换。

在这种情况下,重点是指定最终目标,方法决定操作是买入还是卖出。相同的逻辑适用于这 3 种方法。让我们从order_target_size开始

  • 如果目标大于仓位,则会发出买入指令,差额为目标 - 仓位大小

    示例:

    • 仓位:0目标7 -> 买入(size=7 - 0) -> 买入(size=7)

    • 仓位:3目标7 -> 买入(size=7 - 3) -> 买入(size=4)

    • 仓位:-3目标7 -> 买入(size=7 - -3) -> 买入(size=10)

    • 仓位:-3目标-2 -> 买入(size=-2 - -3) -> 买入(size=1)

  • 如果目标小于仓位,则会发出卖出指令,差额为仓位大小 - 目标

    示例:

    • 仓位:0目标-7 -> 卖出(size=0 - -7) -> 卖出(size=7)

    • 仓位:3目标-7 -> 卖出(size=3 - -7) -> 卖出(size=10)

    • 仓位:-3目标-7 -> 卖出(size=-3 - -7) -> 卖出(size=4)

    • 仓位:3目标2 -> 卖出(size=3 - 2) -> 卖出(size=1)

当使用order_target_value来设置目标值时,投资组合中资产的当前价值和仓位大小都会被考虑在内,以决定最终的基础操作。推理如下:

  • 如果仓位大小为负值(空头)且目标价值必须大于当前价值,则意味着:卖出更多

因此,逻辑如下:

  • 如果目标 > 值size >=0 -> 买入

  • 如果目标 > 值size < 0 -> 卖出

  • 如果目标 < 值size >= 0 -> 卖出

  • 如果目标 < 值size\* < 0 -> 买入

order_target_percent的逻辑与order_target_value相同。该方法简单地考虑了投资组合的当前总价值,以确定资产的目标价值

示例

backtrader尝试为每个新功能提供一个示例,这不例外。没有花里胡哨,只是为了测试结果是否符合预期。这个示例位于 samples 中的order_target目录下。

示例中的逻辑相当愚蠢,只是用于测试:

  • 奇数月(一月,三月,...)中,使用作为目标(对于order_target_value,将日乘以1000

    这模拟了一个递增的目标

  • 偶数月(二月,四月,...)中,使用31 - 日作为目标

    这模拟了一个递减的目标

order_target_size

让我们看看一月二月发生了什么。

$ ./order_target.py --target-size -- plot
0001 - 2005-01-03 - Position Size:     00 - Value 1000000.00
0001 - 2005-01-03 - Order Target Size: 03
0002 - 2005-01-04 - Position Size:     03 - Value 999994.39
0002 - 2005-01-04 - Order Target Size: 04
0003 - 2005-01-05 - Position Size:     04 - Value 999992.48
0003 - 2005-01-05 - Order Target Size: 05
0004 - 2005-01-06 - Position Size:     05 - Value 999988.79
...
0020 - 2005-01-31 - Position Size:     28 - Value 999968.70
0020 - 2005-01-31 - Order Target Size: 31
0021 - 2005-02-01 - Position Size:     31 - Value 999954.68
0021 - 2005-02-01 - Order Target Size: 30
0022 - 2005-02-02 - Position Size:     30 - Value 999979.65
0022 - 2005-02-02 - Order Target Size: 29
0023 - 2005-02-03 - Position Size:     29 - Value 999966.33
0023 - 2005-02-03 - Order Target Size: 28
...

一月目标从年初的第 1 个交易日开始为3,并逐渐增加。持仓大小最初从0增加到3,然后以1的增量移动。

结束一月时,最后的order_target31,当进入二月的第 1 天时报告了该持仓大小,当新的目标大小请求为30时,并随着持仓以1的递减变化。

图片

order_target_value

预计目标值会有类似的行为。

$ ./order_target.py --target-value --plot
0001 - 2005-01-03 - Position Size:     00 - Value 1000000.00
0001 - 2005-01-03 - data value 0.00
0001 - 2005-01-03 - Order Target Value: 3000.00
0002 - 2005-01-04 - Position Size:     78 - Value 999854.14
0002 - 2005-01-04 - data value 2853.24
0002 - 2005-01-04 - Order Target Value: 4000.00
0003 - 2005-01-05 - Position Size:     109 - Value 999801.68
0003 - 2005-01-05 - data value 3938.17
0003 - 2005-01-05 - Order Target Value: 5000.00
0004 - 2005-01-06 - Position Size:     138 - Value 999699.57
...
0020 - 2005-01-31 - Position Size:     808 - Value 999206.37
0020 - 2005-01-31 - data value 28449.68
0020 - 2005-01-31 - Order Target Value: 31000.00
0021 - 2005-02-01 - Position Size:     880 - Value 998807.33
0021 - 2005-02-01 - data value 30580.00
0021 - 2005-02-01 - Order Target Value: 30000.00
0022 - 2005-02-02 - Position Size:     864 - Value 999510.21
0022 - 2005-02-02 - data value 30706.56
0022 - 2005-02-02 - Order Target Value: 29000.00
0023 - 2005-02-03 - Position Size:     816 - Value 999130.05
0023 - 2005-02-03 - data value 28633.44
0023 - 2005-02-03 - Order Target Value: 28000.00
...

还有一行额外的信息,告诉实际的数据值(在投资组合中)是多少。这有助于确定是否已达到目标值

初始目标为3000.0,报告的初始值为2853.24。这里的问题是这是否足够接近。答案是

  • 该示例在每日 K 线结束时使用Market订单和最后可用价格来计算目标大小,以满足目标价值

  • 执行然后使用下一天的open价格,这不太可能是前一天的close

以任何其他方式进行将意味着在欺骗自己。

下一个目标值最终值更接近:40003938.17

当转变为二月时,目标价值开始从31000减少到3000029000数据值也随之从30580.00减少到30706.56,然后到28633.44。等待:

  • 30580 -> 30706.56是一个正向变化。

    确实。在这种情况下,计算出的目标值大小遇到了将值提升到30706.56开盘价

如何避免这种影响:

  • 该示例使用Market类型执行订单,这种效果无法避免。

  • 方法order_target_xxx允许指定执行类型价格

    可以指定Limit作为执行订单,并让价格为close价格(如果没有提供其他价格,则由方法选择),甚至提供特定定价。

图片

order_target_value

在这种情况下,它只是当前投资组合价值的一个百分比。

$ ./order_target.py --target-percent --plot
0001 - 2005-01-03 - Position Size:     00 - Value 1000000.00
0001 - 2005-01-03 - data percent 0.00
0001 - 2005-01-03 - Order Target Percent: 0.03
0002 - 2005-01-04 - Position Size:     785 - Value 998532.05
0002 - 2005-01-04 - data percent 0.03
0002 - 2005-01-04 - Order Target Percent: 0.04
0003 - 2005-01-05 - Position Size:     1091 - Value 998007.44
0003 - 2005-01-05 - data percent 0.04
0003 - 2005-01-05 - Order Target Percent: 0.05
0004 - 2005-01-06 - Position Size:     1381 - Value 996985.64
...
0020 - 2005-01-31 - Position Size:     7985 - Value 991966.28
0020 - 2005-01-31 - data percent 0.28
0020 - 2005-01-31 - Order Target Percent: 0.31
0021 - 2005-02-01 - Position Size:     8733 - Value 988008.94
0021 - 2005-02-01 - data percent 0.31
0021 - 2005-02-01 - Order Target Percent: 0.30
0022 - 2005-02-02 - Position Size:     8530 - Value 995005.45
0022 - 2005-02-02 - data percent 0.30
0022 - 2005-02-02 - Order Target Percent: 0.29
0023 - 2005-02-03 - Position Size:     8120 - Value 991240.75
0023 - 2005-02-03 - data percent 0.29
0023 - 2005-02-03 - Order Target Percent: 0.28
...

信息已更改,以查看投资组合中数据代表的%

图片

示例用法

$ ./order_target.py --help
usage: order_target.py [-h] [--data DATA] [--fromdate FROMDATE]
                       [--todate TODATE] [--cash CASH]
                       (--target-size | --target-value | --target-percent)
                       [--plot [kwargs]]

Sample for Order Target

optional arguments:
  -h, --help            show this help message and exit
  --data DATA           Specific data to be read in (default:
                        ../../datas/yhoo-1996-2015.txt)
  --fromdate FROMDATE   Starting date in YYYY-MM-DD format (default:
                        2005-01-01)
  --todate TODATE       Ending date in YYYY-MM-DD format (default: 2006-12-31)
  --cash CASH           Ending date in YYYY-MM-DD format (default: 1000000)
  --target-size         Use order_target_size (default: False)
  --target-value        Use order_target_value (default: False)
  --target-percent      Use order_target_percent (default: False)
  --plot [kwargs], -p [kwargs]
                        Plot the read data applying any kwargs passed For
                        example: --plot style="candle" (to plot candles)
                        (default: None)

示例代码

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

import argparse
from datetime import datetime

import backtrader as bt

class TheStrategy(bt.Strategy):
    '''
    This strategy is loosely based on some of the examples from the Van
    K. Tharp book: *Trade Your Way To Financial Freedom*. The logic:

      - Enter the market if:
        - The MACD.macd line crosses the MACD.signal line to the upside
        - The Simple Moving Average has a negative direction in the last x
          periods (actual value below value x periods ago)

     - Set a stop price x times the ATR value away from the close

     - If in the market:

       - Check if the current close has gone below the stop price. If yes,
         exit.
       - If not, update the stop price if the new stop price would be higher
         than the current
    '''

    params = (
        ('use_target_size', False),
        ('use_target_value', False),
        ('use_target_percent', False),
    )

    def notify_order(self, order):
        if order.status == order.Completed:
            pass

        if not order.alive():
            self.order = None  # indicate no order is pending

    def start(self):
        self.order = None  # sentinel to avoid operrations on pending order

    def next(self):
        dt = self.data.datetime.date()

        portfolio_value = self.broker.get_value()
        print('%04d - %s - Position Size:     %02d - Value %.2f' %
              (len(self), dt.isoformat(), self.position.size, portfolio_value))

        data_value = self.broker.get_value([self.data])

        if self.p.use_target_value:
            print('%04d - %s - data value %.2f' %
                  (len(self), dt.isoformat(), data_value))

        elif self.p.use_target_percent:
            port_perc = data_value / portfolio_value
            print('%04d - %s - data percent %.2f' %
                  (len(self), dt.isoformat(), port_perc))

        if self.order:
            return  # pending order execution

        size = dt.day
        if (dt.month % 2) == 0:
            size = 31 - size

        if self.p.use_target_size:
            target = size
            print('%04d - %s - Order Target Size: %02d' %
                  (len(self), dt.isoformat(), size))

            self.order = self.order_target_size(target=size)

        elif self.p.use_target_value:
            value = size * 1000

            print('%04d - %s - Order Target Value: %.2f' %
                  (len(self), dt.isoformat(), value))

            self.order = self.order_target_value(target=value)

        elif self.p.use_target_percent:
            percent = size / 100.0

            print('%04d - %s - Order Target Percent: %.2f' %
                  (len(self), dt.isoformat(), percent))

            self.order = self.order_target_percent(target=percent)

def runstrat(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()
    cerebro.broker.setcash(args.cash)

    dkwargs = dict()
    if args.fromdate is not None:
        dkwargs['fromdate'] = datetime.strptime(args.fromdate, '%Y-%m-%d')
    if args.todate is not None:
        dkwargs['todate'] = datetime.strptime(args.todate, '%Y-%m-%d')

    # data
    data = bt.feeds.YahooFinanceCSVData(dataname=args.data, **dkwargs)
    cerebro.adddata(data)

    # strategy
    cerebro.addstrategy(TheStrategy,
                        use_target_size=args.target_size,
                        use_target_value=args.target_value,
                        use_target_percent=args.target_percent)

    cerebro.run()

    if args.plot:
        pkwargs = dict(style='bar')
        if args.plot is not True:  # evals to True but is not True
            npkwargs = eval('dict(' + args.plot + ')')  # args were passed
            pkwargs.update(npkwargs)

        cerebro.plot(**pkwargs)

def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Order Target')

    parser.add_argument('--data', required=False,
                        default='../../datas/yhoo-1996-2015.txt',
                        help='Specific data to be read in')

    parser.add_argument('--fromdate', required=False,
                        default='2005-01-01',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', required=False,
                        default='2006-12-31',
                        help='Ending date in YYYY-MM-DD format')

    parser.add_argument('--cash', required=False, action='store',
                        type=float, default=1000000,
                        help='Ending date in YYYY-MM-DD format')

    pgroup = parser.add_mutually_exclusive_group(required=True)

    pgroup.add_argument('--target-size', required=False, action='store_true',
                        help=('Use order_target_size'))

    pgroup.add_argument('--target-value', required=False, action='store_true',
                        help=('Use order_target_value'))

    pgroup.add_argument('--target-percent', required=False,
                        action='store_true',
                        help=('Use order_target_percent'))

    # Plot options
    parser.add_argument('--plot', '-p', nargs='?', required=False,
                        metavar='kwargs', const=True,
                        help=('Plot the read data applying any kwargs passed\n'
                              '\n'
                              'For example:\n'
                              '\n'
                              '  --plot style="candle" (to plot candles)\n'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

将期货滚动

原文:www.backtrader.com/blog/posts/2016-08-31-rolling-over-futures/rolling-futures-over/

并非每个提供商都为可以交易的工具提供连续未来数据。有时,提供的数据是仍然有效的到期日期的数据,即:仍在交易的数据

当涉及到回测时,这并不是很有帮助,因为数据分散在几个不同的工具中,而且还… 时间上重叠

能够将那些过去的工具数据正确地连接成连续流可以减轻痛苦。问题在于:

  • 没有一条法律规定如何最好地将不同到期日期的期货合并成一个连续的期货

一些文献,由SierraChart提供:

  • 我的链接

滚动数据源

backtrader在 1.8.10.99 版本中添加了一个功能,可以将不同到期日期的期货数据合并成一个连续的期货:

import backtrader as bt

cerebro = bt.Cerebro()
data0 = bt.feeds.MyFeed(dataname='Expiry0')
data1 = bt.feeds.MyFeed(dataname='Expiry1')
...
dataN = bt.feeds.MyFeed(dataname='ExpiryN')

drollover = cerebro.rolloverdata(data0, data1, ..., dataN, name='MyRoll', **kwargs)

cerebro.run()

下面解释了可能的\*\*kwargs

也可以通过直接访问RollOver数据源来完成(如果进行了子类化,则会有所帮助):

import backtrader as bt

cerebro = bt.Cerebro()
data0 = bt.feeds.MyFeed(dataname='Expiry0')
data1 = bt.feeds.MyFeed(dataname='Expiry1')
...
dataN = bt.feeds.MyFeed(dataname='ExpiryN')

drollover = bt.feeds.RollOver(data0, data1, ..., dataN, dataname='MyRoll', **kwargs)
cerebro.adddata(drollover)

cerebro.run()

下面解释了可能的\*\*kwargs

使用RollOver时,使用dataname分配名称。这是所有数据源用于传递名称/标记的标准参数。在这种情况下,它被重用以为所有滚动期货分配一个公共名称。

对于cerebro.rolloverdata,名称使用name分配给一个数据源,这已经是该方法的一个命名参数

底线:

  • 数据源通常创建,但添加到cerebro

  • 这些数据源作为输入提供给bt.feeds.RollOver

    也给出了一个dataname,主要用于识别目的。

  • 然后将这个滚动数据源添加到cerebro

滚动的选项

提供了两个参数来控制滚动过程

  • checkdate(默认值:None

    这必须是一个callable,具有以下签名:

    checkdate(dt, d):` 
    

    其中:

    • dt是一个datetime.datetime对象

    • d是当前活跃期货的数据源

    预期的返回值:

    • True:只要可调用返回这个,就可以切换到下一个未来

      如果某种商品在三月的第三个星期五到期,则checkdate可以在到期发生的整个周返回True

    • False:到期无法发生

  • checkcondition(默认值:None

    注意:只有当checkdate返回True时才会调用此函数

    如果是None,这将内部求值为True(执行滚动)

    否则,这必须是一个callable,具有以下签名:

    checkcondition(d0, d1)` 
    

    其中:

    • d0是当前活跃期货的数据源

    • d1是下一个到期的数据源

    预期的返回值:

    • True:滚动到下一个未来

      继续使用checkdate的示例,这可以说明如果d0volume已经小于d1的 volume,则可以进行滚动

    • False:到期无法发生

子类化RollOver

如果指定可调用对象不够,总是可以通过子类化RollOver。要子类化的方法:

  • def _checkdate(self, dt, d):

    与上面同名参数的签名相匹配。预期的返回值也是相同的。

  • def _checkcondition(self, d0, d1)

    与上面同名参数的签名相匹配。预期的返回值也是相同的。

让我们滚动

注意

示例中的默认行为是使用cerebro.rolloverdata。可以通过传递-no-cerebro标志来更改。在这种情况下,示例使用RollOvercerebro.adddata

实现包括一个示例,可在backtrader源代码中找到。

期货连接

让我们从纯连接开始,运行示例而不带任何参数。

$ ./rollover.py

Len, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest
0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0
0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0
...
0176, FESX, 199FESXM4, 2014-06-20, Fri, 3315.0, 3324.0, 3307.0, 3322.0, 134777.0, 520978.0
0177, FESX, 199FESXU4, 2014-06-23, Mon, 3301.0, 3305.0, 3265.0, 3285.0, 730211.0, 3003692.0
...
0241, FESX, 199FESXU4, 2014-09-19, Fri, 3287.0, 3308.0, 3286.0, 3294.0, 144692.0, 566249.0
0242, FESX, 199FESXZ4, 2014-09-22, Mon, 3248.0, 3263.0, 3231.0, 3240.0, 582077.0, 2976624.0
...
0306, FESX, 199FESXZ4, 2014-12-19, Fri, 3196.0, 3202.0, 3131.0, 3132.0, 226415.0, 677924.0
0307, FESX, 199FESXH5, 2014-12-22, Mon, 3151.0, 3177.0, 3139.0, 3168.0, 547095.0, 2952769.0
...
0366, FESX, 199FESXH5, 2015-03-20, Fri, 3680.0, 3698.0, 3672.0, 3695.0, 147632.0, 887205.0
0367, FESX, 199FESXM5, 2015-03-23, Mon, 3654.0, 3655.0, 3608.0, 3618.0, 802344.0, 3521988.0
...
0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0
0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

这使用cerebro.chaindata,结果应该很清楚:

  • 每当一个数据源结束时,下一个数据源接管

  • 这总是在星期五星期一之间发生:示例中的期货总是在星期五到期

期货滚动没有检查

让我们执行--rollover

$ ./rollover.py --rollover --plot

Len, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest
0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0
0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0
...
0176, FESX, 199FESXM4, 2014-06-20, Fri, 3315.0, 3324.0, 3307.0, 3322.0, 134777.0, 520978.0
0177, FESX, 199FESXU4, 2014-06-23, Mon, 3301.0, 3305.0, 3265.0, 3285.0, 730211.0, 3003692.0
...
0241, FESX, 199FESXU4, 2014-09-19, Fri, 3287.0, 3308.0, 3286.0, 3294.0, 144692.0, 566249.0
0242, FESX, 199FESXZ4, 2014-09-22, Mon, 3248.0, 3263.0, 3231.0, 3240.0, 582077.0, 2976624.0
...
0306, FESX, 199FESXZ4, 2014-12-19, Fri, 3196.0, 3202.0, 3131.0, 3132.0, 226415.0, 677924.0
0307, FESX, 199FESXH5, 2014-12-22, Mon, 3151.0, 3177.0, 3139.0, 3168.0, 547095.0, 2952769.0
...
0366, FESX, 199FESXH5, 2015-03-20, Fri, 3680.0, 3698.0, 3672.0, 3695.0, 147632.0, 887205.0
0367, FESX, 199FESXM5, 2015-03-23, Mon, 3654.0, 3655.0, 3608.0, 3618.0, 802344.0, 3521988.0
...
0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0
0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

相同的行为。可以清楚地看到合同更改是在 3 月、6 月、9 月、12 月的第 3 个星期五进行的。

但这大多是错误的。backtradr不可能知道,但作者知道EuroStoxx 50期货在12:00 CET 停止交易。因此,即使到期月份的第 3 个星期五有每日条形图,更改发生得太晚。

图片

在一周内更改

示例中实现了一个checkdate可调用对象,用于计算当前活跃合同的到期日期。

checkdate将在月份的第 3 个星期五到来时(例如,如果星期一是银行假日,则可能是星期二)进行滚动。

$ ./rollover.py --rollover --checkdate --plot

Len, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest
0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0
0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0
...
0171, FESX, 199FESXM4, 2014-06-13, Fri, 3283.0, 3292.0, 3253.0, 3276.0, 734907.0, 2715357.0
0172, FESX, 199FESXU4, 2014-06-16, Mon, 3261.0, 3275.0, 3252.0, 3262.0, 180608.0, 844486.0
...
0236, FESX, 199FESXU4, 2014-09-12, Fri, 3245.0, 3247.0, 3220.0, 3232.0, 650314.0, 2726874.0
0237, FESX, 199FESXZ4, 2014-09-15, Mon, 3209.0, 3224.0, 3203.0, 3221.0, 153448.0, 983793.0
...
0301, FESX, 199FESXZ4, 2014-12-12, Fri, 3127.0, 3143.0, 3038.0, 3042.0, 1409834.0, 2934179.0
0302, FESX, 199FESXH5, 2014-12-15, Mon, 3041.0, 3089.0, 2963.0, 2980.0, 329896.0, 904053.0
...
0361, FESX, 199FESXH5, 2015-03-13, Fri, 3657.0, 3680.0, 3627.0, 3670.0, 867678.0, 3499116.0
0362, FESX, 199FESXM5, 2015-03-16, Mon, 3594.0, 3641.0, 3588.0, 3629.0, 250445.0, 1056099.0
...
0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0
0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

好多了。现在滚动发生在5 天前。快速查看Len指数就可以看到。例如:

  • 199FESXM4199FESXU4发生在len 171-172。没有checkdate时发生在176-177

滚动将在到期月份的第 3 个星期五之前的星期一发生。

图片

添加成交量条件

即使有改进,情况仍然可以进一步改善,不仅日期,而且协商的成交量也将被考虑在内。当新合同的交易量超过当前活跃合同时进行切换。

让我们添加一个checkcondition并运行。

$ ./rollover.py --rollover --checkdate --checkcondition --plot

Len, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest
0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0
0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0
...
0175, FESX, 199FESXM4, 2014-06-19, Thu, 3307.0, 3330.0, 3300.0, 3321.0, 717979.0, 759122.0
0176, FESX, 199FESXU4, 2014-06-20, Fri, 3309.0, 3318.0, 3290.0, 3298.0, 711627.0, 2957641.0
...
0240, FESX, 199FESXU4, 2014-09-18, Thu, 3249.0, 3275.0, 3243.0, 3270.0, 846600.0, 803202.0
0241, FESX, 199FESXZ4, 2014-09-19, Fri, 3273.0, 3293.0, 3250.0, 3252.0, 1042294.0, 3021305.0
...
0305, FESX, 199FESXZ4, 2014-12-18, Thu, 3095.0, 3175.0, 3085.0, 3172.0, 1309574.0, 889112.0
0306, FESX, 199FESXH5, 2014-12-19, Fri, 3195.0, 3200.0, 3106.0, 3147.0, 1329040.0, 2964538.0
...
0365, FESX, 199FESXH5, 2015-03-19, Thu, 3661.0, 3691.0, 3646.0, 3668.0, 1271122.0, 1054639.0
0366, FESX, 199FESXM5, 2015-03-20, Fri, 3607.0, 3664.0, 3595.0, 3646.0, 1182235.0, 3407004.0
...
0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0
0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

更好了**。我们已将切换日期移至众所周知的到期月份第 3 个星期五之前的星期四*

这应该不会让人感到惊讶,因为到期的期货在那个星期五交易时间更少,成交量必须很小。

注意

通过checkdate可调用函数,到那个星期四也可以设置为滚动日期。但这并不是示例的重点。

图片

结论

backtrader 现在包含了一个灵活的机制,允许滚动期货以创建连续流。

使用示例

$ ./rollover.py --help
usage: rollover.py [-h] [--no-cerebro] [--rollover] [--checkdate]
                   [--checkcondition] [--plot [kwargs]]

Sample for Roll Over of Futures

optional arguments:
  -h, --help            show this help message and exit
  --no-cerebro          Use RollOver Directly (default: False)
  --rollover
  --checkdate           Change during expiration week (default: False)
  --checkcondition      Change when a given condition is met (default: False)
  --plot [kwargs], -p [kwargs]
                        Plot the read data applying any kwargs passed For
                        example: --plot style="candle" (to plot candles)
                        (default: None)

示例代码

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

import argparse
import bisect
import calendar
import datetime

import backtrader as bt

class TheStrategy(bt.Strategy):
    def start(self):
        header = ['Len', 'Name', 'RollName', 'Datetime', 'WeekDay', 'Open',
                  'High', 'Low', 'Close', 'Volume', 'OpenInterest']
        print(', '.join(header))

    def next(self):
        txt = list()
        txt.append('%04d' % len(self.data0))
        txt.append('{}'.format(self.data0._dataname))
        # Internal knowledge ... current expiration in use is in _d
        txt.append('{}'.format(self.data0._d._dataname))
        txt.append('{}'.format(self.data.datetime.date()))
        txt.append('{}'.format(self.data.datetime.date().strftime('%a')))
        txt.append('{}'.format(self.data.open[0]))
        txt.append('{}'.format(self.data.high[0]))
        txt.append('{}'.format(self.data.low[0]))
        txt.append('{}'.format(self.data.close[0]))
        txt.append('{}'.format(self.data.volume[0]))
        txt.append('{}'.format(self.data.openinterest[0]))
        print(', '.join(txt))

def checkdate(dt, d):
    # Check if the date is in the week where the 3rd friday of Mar/Jun/Sep/Dec

    # EuroStoxx50 expiry codes: MY
    # M -> H, M, U, Z (Mar, Jun, Sep, Dec)
    # Y -> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 -> year code. 5 -> 2015
    MONTHS = dict(H=3, M=6, U=9, Z=12)

    M = MONTHS[d._dataname[-2]]

    centuria, year = divmod(dt.year, 10)
    decade = centuria * 10

    YCode = int(d._dataname[-1])
    Y = decade + YCode
    if Y < dt.year:  # Example: year 2019 ... YCode is 0 for 2020
        Y += 10

    exp_day = 21 - (calendar.weekday(Y, M, 1) + 2) % 7
    exp_dt = datetime.datetime(Y, M, exp_day)

    # Get the year, week numbers
    exp_year, exp_week, _ = exp_dt.isocalendar()
    dt_year, dt_week, _ = dt.isocalendar()

    # print('dt {} vs {} exp_dt'.format(dt, exp_dt))
    # print('dt_week {} vs {} exp_week'.format(dt_week, exp_week))

    # can switch if in same week
    return (dt_year, dt_week) == (exp_year, exp_week)

def checkvolume(d0, d1):
    return d0.volume[0] < d1.volume[0]  # Switch if volume from d0 < d1

def runstrat(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()

    fcodes = ['199FESXM4', '199FESXU4', '199FESXZ4', '199FESXH5', '199FESXM5']
    store = bt.stores.VChartFile()
    ffeeds = [store.getdata(dataname=x) for x in fcodes]

    rollkwargs = dict()
    if args.checkdate:
        rollkwargs['checkdate'] = checkdate

        if args.checkcondition:
            rollkwargs['checkcondition'] = checkvolume

    if not args.no_cerebro:
        if args.rollover:
            cerebro.rolloverdata(name='FESX', *ffeeds, **rollkwargs)
        else:
            cerebro.chaindata(name='FESX', *ffeeds)
    else:
        drollover = bt.feeds.RollOver(*ffeeds, dataname='FESX', **rollkwargs)
        cerebro.adddata(drollover)

    cerebro.addstrategy(TheStrategy)
    cerebro.run(stdstats=False)

    if args.plot:
        pkwargs = dict(style='bar')
        if args.plot is not True:  # evals to True but is not True
            npkwargs = eval('dict(' + args.plot + ')')  # args were passed
            pkwargs.update(npkwargs)

        cerebro.plot(**pkwargs)

def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Roll Over of Futures')

    parser.add_argument('--no-cerebro', required=False, action='store_true',
                        help='Use RollOver Directly')

    parser.add_argument('--rollover', required=False, action='store_true')

    parser.add_argument('--checkdate', required=False, action='store_true',
                        help='Change during expiration week')

    parser.add_argument('--checkcondition', required=False,
                        action='store_true',
                        help='Change when a given condition is met')

    # Plot options
    parser.add_argument('--plot', '-p', nargs='?', required=False,
                        metavar='kwargs', const=True,
                        help=('Plot the read data applying any kwargs passed\n'
                              '\n'
                              'For example:\n'
                              '\n'
                              '  --plot style="candle" (to plot candles)\n'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

信用利息

原文:www.backtrader.com/blog/posts/2016-08-22-credit-interest/credit-interest/

在一些情况下,真实经纪人的现金金额可能会减少,因为资产操作包括利率。例如:

  • 股票的空头卖出

  • ETFs 长和短

这意味着不仅交易构成了系统的盈利,因为信用的利息也在消耗账户。

为了涵盖这种情况,backtrader1.8.8.96版本开始(功能)可以考虑到这一点。

扩展 CommissionInfo

即使与任何order/trade无关,从账户中扣除现金的折扣可以被建模为经纪人收取的佣金。因此,鉴于backtrader已经提供了灵活和可扩展的佣金系统,该系统已稍微扩展以支持credit interest

现在可以使用两个新参数实例化 CommissionInfo

  • interest(默认:0.0

    如果这个值不为零,那么这就是持有空头寸时收取的年利率。这主要是为了股票空头卖出

    公式:days \* price \* abs(size) \* (interest / 365)

    必须以绝对值指定:0.05 -> 5%

    注意

    通过覆盖方法 _get_credit_interest 可以改变行为

  • interest_long(默认:False

    一些产品(如 ETFs)无论是空头还是多头都会收取利息。如果这是 True 并且 interest 不为零,则会对两个方向收取利息。

参数也可以通过broker使用以下方法设置:

def setcommission(self,
                  commission=0.0, margin=None, mult=1.0,
                  commtype=None, percabs=True, stocklike=False,
                  interest=0.0, interest_long=False,
                  name=None)

其中 interestinterest_long 显然与上文的含义相同。

应用一个 CommissionInfo

对于佣金百分比的股票,带有credit interest的典型使用场景如下

import backtrader as bt

cerebro = bt.Cerebro()
comminfo = bt.CommissionInfo(commtype=bt.CommissionInfo.COMM_PERC,  # % commission
                             commission=0.005,  # 0.5%
                             percabs=True,  # perc expressed in abs terms
                             stocklike=True,
                             interest=0.05,  # 5% anual credit interest rate
                            )

cerebro.broker.addcommissioninfo(comminfo)
...

如果最终用户有自己的佣金方案,这将非常有用。

一个更简单的案例与setcommission

import backtrader as bt

cerebro = bt.Cerebro()
cerebro.broker.setcommission(commtype=bt.CommissionInfo.COMM_PERC,  # % commission
                             commission=0.005,  # 0.5%
                             percabs=True,  # perc expressed in abs terms
                             stocklike=True,
                             interest=0.05,  # 5% anual credit interest rate
                            )

...

其余的和任何其他常规的backtrader脚本一样。

一些样本场景

仅限long,无退出,无利息

为了建立一个最低基线,让我们先不要利息,只让脚本进入市场并避免退出。

$ ./credit-interest.py --plot --stocklike --long --no-exit
01 2005-04-11 23:59:59 BUY  Size: +10 / Price: 3088.47

image

现在应该很清楚了。从总投资组合价值中有一条平直的现金线,不显示任何扣除。

仅限long,无退出和利息

让我们尝试添加利息,看看会发生什么(我们将添加一个巨大的15%利息来尝试注意到这些动向)

$ ./credit-interest.py --plot --stocklike --long --no-exit --interest 0.15
01 2005-04-11 23:59:59 BUY  Size: +10 / Price: 3088.47

image

什么都没变!这是意料之中的。在大多数情况下,利息只适用于short头寸(使用信用卖出)而这是一个long-only头寸。

让我们告诉脚本也要为long头寸做这个

$ ./credit-interest.py --plot --stocklike --long --no-exit --interest 0.15 --interest_long
01 2005-04-11 23:59:59 BUY  Size: +10 / Price: 3088.47

image

改变已经存在。有一个减少,并且很大(鉴于正在收取的利息很大)

多空头场景

这将模拟类似于具有年度利息的ETF,可以是常规的或反向的。首先让我们建立基线。

$ ./credit-interest.py --plot --stocklike
01 2005-03-22 23:59:59 SELL Size: -10 / Price: 3040.55
02 2005-04-11 23:59:59 BUY  Size: +10 / Price: 3088.47
...
...
34 2006-12-19 23:59:59 BUY  Size: +10 / Price: 4121.01
35 2006-12-19 23:59:59 BUY  Size: +10 / Price: 4121.01

还有更多操作,系统始终在市场中

图片

由于ETF将对多头和空头操作收取利息,现在利息将同时增加:

$ ./credit-interest.py --plot --stocklike --interest 0.15 --interest_long
01 2005-03-22 23:59:59 SELL Size: -10 / Price: 3040.55
02 2005-04-11 23:59:59 BUY  Size: +10 / Price: 3088.47
...
...
34 2006-12-19 23:59:59 BUY  Size: +10 / Price: 4121.01

图片

注意34次操作而不是35。似乎有些地方可能出了问题,但是……没有……

收取的利息会稍微减少现金储备,最终导致最后一个订单无法执行,因为现金不足

多头操作中去除利息费用(即使对于ETF来说并非真实)将使系统能够结束:

$ ./credit-interest.py --plot --stocklike --interest 0.15
01 2005-03-22 23:59:59 SELL Size: -10 / Price: 3040.55
02 2005-04-11 23:59:59 BUY  Size: +10 / Price: 3088.47
...
...
34 2006-12-19 23:59:59 BUY  Size: +10 / Price: 4121.01
35 2006-12-19 23:59:59 BUY  Size: +10 / Price: 4121.01

回到业务,直到第35次操作。

图片

与原始数据快速比较显示,最终现金从7490(不计利息)变为5418(仅对空头操作应用利息)

结论

这种新功能允许更真实地模拟回测场景,以尝试实现梦想:一个盈利系统

示例用法

$ ./credit-interest.py --help
usage: credit-interest.py [-h] [--data DATA] [--fromdate FROMDATE]
                          [--todate TODATE] [--cash CASH] [--period1 PERIOD1]
                          [--period2 PERIOD2] [--interest INTEREST]
                          [--interest_long] [--long | --short] [--no-exit]
                          [--stocklike] [--margin MARGIN] [--mult MULT]
                          [--stake STAKE] [--plot [kwargs]]

Sample for Slippage

optional arguments:
  -h, --help            show this help message and exit
  --data DATA           Specific data to be read in (default:
                        ../../datas/2005-2006-day-001.txt)
  --fromdate FROMDATE   Starting date in YYYY-MM-DD format (default: None)
  --todate TODATE       Ending date in YYYY-MM-DD format (default: None)
  --cash CASH           Cash to start with (default: 50000)
  --period1 PERIOD1     Fast moving average period (default: 10)
  --period2 PERIOD2     Slow moving average period (default: 30)
  --interest INTEREST   Activate credit interest rate (default: 0.0)
  --interest_long       Credit interest rate for long positions (default:
                        False)
  --long                Do a long only strategy (default: False)
  --short               Do a long only strategy (default: False)
  --no-exit             The 1st taken position will not be exited (default:
                        False)
  --stocklike           Consider the asset to be stocklike (default: False)
  --margin MARGIN       Margin for future like instruments (default: 0.0)
  --mult MULT           Multiplier for future like instruments (default: 1.0)
  --stake STAKE         Stake to apply (default: 10)
  --plot [kwargs], -p [kwargs]
                        Plot the read data applying any kwargs passed For
                        example: --plot style="candle" (to plot candles)
                        (default: None)

示例代码

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

import argparse
import collections
import datetime
import itertools

import backtrader as bt

class SMACrossOver(bt.Signal):
    params = (('p1', 10), ('p2', 30),)

    def __init__(self):
        sma1 = bt.indicators.SMA(period=self.p.p1)
        sma2 = bt.indicators.SMA(period=self.p.p2)
        self.lines.signal = bt.indicators.CrossOver(sma1, sma2)

class NoExit(bt.Signal):
    def next(self):
        self.lines.signal[0] = 0.0

class St(bt.SignalStrategy):
    opcounter = itertools.count(1)

    def notify_order(self, order):
        if order.status == bt.Order.Completed:
            t = ''
            t += '{:02d}'.format(next(self.opcounter))
            t += ' {}'.format(order.data.datetime.datetime())
            t += ' BUY ' * order.isbuy() or ' SELL'
            t += ' Size: {:+d} / Price: {:.2f}'
            print(t.format(order.executed.size, order.executed.price))

def runstrat(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()
    cerebro.broker.set_cash(args.cash)

    dkwargs = dict()
    if args.fromdate is not None:
        fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
        dkwargs['fromdate'] = fromdate

    if args.todate is not None:
        todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
        dkwargs['todate'] = todate

    # if dataset is None, args.data has been given
    data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs)
    cerebro.adddata(data)

    cerebro.signal_strategy(St)
    cerebro.addsizer(bt.sizers.FixedSize, stake=args.stake)

    sigtype = bt.signal.SIGNAL_LONGSHORT
    if args.long:
        sigtype = bt.signal.SIGNAL_LONG
    elif args.short:
        sigtype = bt.signal.SIGNAL_SHORT

    cerebro.add_signal(sigtype,
                       SMACrossOver, p1=args.period1, p2=args.period2)

    if args.no_exit:
        if args.long:
            cerebro.add_signal(bt.signal.SIGNAL_LONGEXIT, NoExit)
        elif args.short:
            cerebro.add_signal(bt.signal.SIGNAL_SHORTEXIT, NoExit)

    comminfo = bt.CommissionInfo(
        mult=args.mult,
        margin=args.margin,
        stocklike=args.stocklike,
        interest=args.interest,
        interest_long=args.interest_long)

    if True:
        cerebro.broker.addcommissioninfo(comminfo)

    cerebro.run()
    if args.plot:
        pkwargs = dict(style='bar')
        if args.plot is not True:  # evals to True but is not True
            npkwargs = eval('dict(' + args.plot + ')')  # args were passed
            pkwargs.update(npkwargs)

        cerebro.plot(**pkwargs)

def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Slippage')

    parser.add_argument('--data', required=False,
                        default='../../datas/2005-2006-day-001.txt',
                        help='Specific data to be read in')

    parser.add_argument('--fromdate', required=False, default=None,
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', required=False, default=None,
                        help='Ending date in YYYY-MM-DD format')

    parser.add_argument('--cash', required=False, action='store',
                        type=float, default=50000,
                        help=('Cash to start with'))

    parser.add_argument('--period1', required=False, action='store',
                        type=int, default=10,
                        help=('Fast moving average period'))

    parser.add_argument('--period2', required=False, action='store',
                        type=int, default=30,
                        help=('Slow moving average period'))

    parser.add_argument('--interest', required=False, action='store',
                        default=0.0, type=float,
                        help=('Activate credit interest rate'))

    parser.add_argument('--interest_long', required=False, action='store_true',
                        help=('Credit interest rate for long positions'))

    pgroup = parser.add_mutually_exclusive_group()
    pgroup.add_argument('--long', required=False, action='store_true',
                        help=('Do a long only strategy'))

    pgroup.add_argument('--short', required=False, action='store_true',
                        help=('Do a long only strategy'))

    parser.add_argument('--no-exit', required=False, action='store_true',
                        help=('The 1st taken position will not be exited'))

    parser.add_argument('--stocklike', required=False, action='store_true',
                        help=('Consider the asset to be stocklike'))

    parser.add_argument('--margin', required=False, action='store',
                        default=0.0, type=float,
                        help=('Margin for future like instruments'))

    parser.add_argument('--mult', required=False, action='store',
                        default=1.0, type=float,
                        help=('Multiplier for future like instruments'))

    parser.add_argument('--stake', required=False, action='store',
                        default=10, type=int,
                        help=('Stake to apply'))

    # Plot options
    parser.add_argument('--plot', '-p', nargs='?', required=False,
                        metavar='kwargs', const=True,
                        help=('Plot the read data applying any kwargs passed\n'
                              '\n'
                              'For example:\n'
                              '\n'
                              '  --plot style="candle" (to plot candles)\n'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

迪克森移动平均线

原文:www.backtrader.com/blog/posts/2016-08-18-dickson-moving-average/dickson-moving-average/

下面的reddit帖子将这种平均线称为迪克森移动平均线,以其作者内森·迪克森reddit用户名)的名字命名。

在定期访问reddit Algotrading时,我发现了一篇关于一种试图模拟 Jurik Moving Average(又称JMA)的移动平均线的帖子

描述为EasyLanguage中的算法,我不得不询问种子值和ec的性质,最终导致了Ehlers零滞后指标

为了将迪克森移动平均线实现到backtrader中,并且考虑到对EhlersHull 移动平均线的依赖,这两者也被添加到了移动平均线的工具中。

总结一下,以下内容已添加到Release 1.8.7.96中:

  • Hull 移动平均线

  • 零滞后指标

  • 迪克森移动平均线

结果可以通过使用样本数据和btrun绘制图表来观察:

$ btrun --nostdstats \
    --format btcsv \
    --data ../../../backtrader/datas/2006-day-001.txt \
    --indicator :SMA \
    --indicator :EMA \
    --indicator :HMA \
    --indicator :ZeroLagIndicator \
    --indicator :DMA \
    --plot style=\'line\'

image

现在的问题在于让迪克森移动平均线产生利润……就像任何其他指标一样。

注意

注意Hull 移动平均线(又名HMA)开始比其他几个值晚几个值产生。这是因为它使用了移动平均延迟了初始产生。

通过比较显示DMA如何处于ZeroLagIndicatorHullMovingAverage之间的中间位置。后者的period=7迪克森移动平均线内部的默认值匹配:

$ btrun --nostdstats \
    --format btcsv \
    --data ../../../backtrader/datas/2006-day-001.txt \
    --indicator :HMA:period=7 \
    --indicator :ZeroLagIndicator \
    --indicator :DMA \
    --plot style=\'line\'

image

股票筛选

原文:www.backtrader.com/blog/posts/2016-08-15-stock-screening/stock-screening/

在寻找其他东西时,我在 StackOverlow 家族网站之一上看到了一个问题:Quantitative Finance,也就是 Quant StackExchange。问题是:

它被标记为 Python,所以值得看看 backtrader 是否能胜任这项任务。

分析器本身

这个问题似乎适合一个简单的分析器。虽然问题只想要那些高于移动平均线的股票,但我们会保留额外的信息,比如那些不符合条件的股票,以确保谷物确实被分离出来。

class Screener_SMA(bt.Analyzer):
    params = dict(period=10)

    def start(self):
        self.smas = {data: bt.indicators.SMA(data, period=self.p.period)
                     for data in self.datas}

    def stop(self):
        self.rets['over'] = list()
        self.rets['under'] = list()

        for data, sma in self.smas.items():
            node = data._name, data.close[0], sma[0]
            if data > sma:  # if data.close[0] > sma[0]
                self.rets['over'].append(node)
            else:
                self.rets['under'].append(node)

注意

当然,还需要 import backtrader as bt

这基本上解决了问题。对 Analyzer 的分析:

  • period 作为参数,以便有一个灵活的分析器

  • start 方法

    对系统中的每个 data 制作一个 Simple Moving AverageSMA)。

  • stop 方法

    查看哪个 data(如果没有其他指定,则为 close)高于其 sma,并将其存储在返回值(rets)的键 over 下的 list

    成员 retsanalyzers 中是标准的,是一个 collections.OrderedDict。由基类创建。

    将不符合条件的股票保留在 under 键下

现在的问题是:让分析器运行起来。

注意

我们假设代码已经放在一个名为 st-screener.py 的文件中

方法 1

backtrader 自从很早以前就包含了一个名为 btrun 的自动脚本运行,可以从 Python 模块加载策略、指标、分析器,解析参数,当然还可以绘图。

让我们来运行一下:

$ btrun --format yahoo --data YHOO --data IBM --data NVDA --data TSLA --data ORCL --data AAPL --fromdate 2016-07-15 --todate 2016-08-13 --analyzer st-screener:Screener_SMA --cerebro runonce=0 --writer --nostdstats
===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
  - Datas:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data0:
      - Name: YHOO
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data1:
      - Name: IBM
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data2:
      - Name: NVDA
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data3:
      - Name: TSLA
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data4:
      - Name: ORCL
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data5:
      - Name: AAPL
      - Timeframe: Days
      - Compression: 1
  -----------------------------------------------------------------------------
  - Strategies:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Strategy:
      *************************************************************************
      - Params:
      *************************************************************************
      - Indicators:
        .......................................................................
        - SMA:
          - Lines: sma
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 10
      *************************************************************************
      - Observers:
      *************************************************************************
      - Analyzers:
        .......................................................................
        - Value:
          - Begin: 10000.0
          - End: 10000.0
        .......................................................................
        - Screener_SMA:
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 10
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - over: ('ORCL', 41.09, 41.032), ('IBM', 161.95, 161.221), ('YHOO', 42.94, 39.629000000000005), ('AAPL', 108.18, 106.926), ('NVDA', 63.04, 58.327)
            - under: ('TSLA', 224.91, 228.423)

我们使用了一组众所周知的股票代码:

  • AAPL, IBM, NVDA, ORCL, TSLA, YHOO

唯一一个低于 10Simple Moving Average 的是 TSLA

让我们尝试一个 50 天的周期。是的,这也可以通过 btrun 控制。运行(输出缩短):

$ btrun --format yahoo --data YHOO --data IBM --data NVDA --data TSLA --data ORCL --data AAPL --fromdate 2016-05-15 --todate 2016-08-13 --analyzer st-screener:Screener_SMA:period=50 --cerebro runonce=0 --writer --nostdstats
===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
  - Datas:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data0:
...
...
...
        - Screener_SMA:
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 50
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - over: ('ORCL', 41.09, 40.339), ('IBM', 161.95, 155.0356), ('YHOO', 42.94, 37.9648), ('TSLA', 224.91, 220.4784), ('AAPL', 108.18, 98.9782), ('NVDA', 63.04, 51.4746)
            - under:

注意如何在命令行中指定了 50 天的周期:

  • st-screener:Screener_SMA:period=50

    在上一次运行中,这是 st-screener:Screener_SMA,并且使用了代码中的默认 10

我们还需要调整 fromdate,以确保有足够的条形图用于计算 Simple Moving Averages

在这种情况下,所有股票的 50 天移动平均线都在上面。

方法 2

制作一个小脚本(请参见下面的完整代码)以更好地控制我们的操作。但结果是一样的。

核心相当小:

 cerebro = bt.Cerebro()
    for ticker in args.tickers.split(','):
        data = bt.feeds.YahooFinanceData(dataname=ticker,
                                         fromdate=fromdate, todate=todate)
        cerebro.adddata(data)

    cerebro.addanalyzer(Screener_SMA, period=args.period)
    cerebro.run(runonce=False, stdstats=False, writer=True)

其余大部分是关于参数解析的。

对于 10 天(再次缩短输出):

$ ./st-screener.py
===============================================================================
Cerebro:
...
...
...
        - Screener_SMA:
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 10
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - over: (u'NVDA', 63.04, 58.327), (u'AAPL', 108.18, 106.926), (u'YHOO', 42.94, 39.629000000000005), (u'IBM', 161.95, 161.221), (u'ORCL', 41.09, 41.032)
            - under: (u'TSLA', 224.91, 228.423)

结果相同。所以让我们避免重复为 50 天做这个。

结论

方法 1btrun方法 2的小脚本都使用完全相同的analyzer,因此提供相同的结果。

backtrader已经能够应对又一个小挑战。

两个最后的注意事项:

  • 这两种方法都使用内置的writer功能来提供输出。

    • 作为btrun的参数,带有--writer

    • 作为参数传递给cerebro.run时,带有writer=True

  • 在两种情况下,runonce都已被停用。这是为了确保在线数据保持同步,因为结果可能具有不同的长度(其中一个股票可能交易较少)。

脚本用法

$ ./st-screener.py --help
usage: st-screener.py [-h] [--tickers TICKERS] [--period PERIOD]

SMA Stock Screener

optional arguments:
  -h, --help         show this help message and exit
  --tickers TICKERS  Yahoo Tickers to consider, COMMA separated (default:
                     YHOO,IBM,AAPL,TSLA,ORCL,NVDA)
  --period PERIOD    SMA period (default: 10)

完整的脚本

#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015, 2016 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import datetime

import backtrader as bt

class Screener_SMA(bt.Analyzer):
    params = dict(period=10)

    def start(self):
        self.smas = {data: bt.indicators.SMA(data, period=self.p.period)
                     for data in self.datas}

    def stop(self):
        self.rets['over'] = list()
        self.rets['under'] = list()

        for data, sma in self.smas.items():
            node = data._name, data.close[0], sma[0]
            if data > sma:  # if data.close[0] > sma[0]
                self.rets['over'].append(node)
            else:
                self.rets['under'].append(node)

DEFAULTTICKERS = ['YHOO', 'IBM', 'AAPL', 'TSLA', 'ORCL', 'NVDA']

def run(args=None):
    args = parse_args(args)
    todate = datetime.date.today()
    # Get from date from period +X% for weekeends/bank/holidays: let's double
    fromdate = todate - datetime.timedelta(days=args.period * 2)

    cerebro = bt.Cerebro()
    for ticker in args.tickers.split(','):
        data = bt.feeds.YahooFinanceData(dataname=ticker,
                                         fromdate=fromdate, todate=todate)
        cerebro.adddata(data)

    cerebro.addanalyzer(Screener_SMA, period=args.period)
    cerebro.run(runonce=False, stdstats=False, writer=True)

def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='SMA Stock Screener')

    parser.add_argument('--tickers', required=False, action='store',
                        default=','.join(DEFAULTTICKERS),
                        help='Yahoo Tickers to consider, COMMA separated')

    parser.add_argument('--period', required=False, action='store',
                        type=int, default=10,
                        help=('SMA period'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()

if __name__ == '__main__':
    run()

带有信号的策略

原文:www.backtrader.com/blog/posts/2016-08-01-signal-strategy/signal-strategy/

也可以在不编写策略的情况下操作backtrader。尽管这是首选方式,由于构成机器的对象层次结构,使用信号也是可能的。

注意

从版本1.8.0.x起可用

快速总结:

  • 不是编写一个策略类,实例化指标,编写买入/卖出逻辑…

  • 最终用户添加信号(无论如何是指标),其余工作在后台完成

快速示例:

import backtrader as bt

data = bt.feeds.OneOfTheFeeds(dataname='mydataname')
cerebro.adddata(data)

cerebro.add_signal(bt.SIGNAL_LONGSHORT, MySignal)
cerebro.run()

Et voilá!。

当然,信号本身是缺失的。让我们定义一个非常简单的信号,它产生:

  • 如果收盘价格高于简单移动平均线,则为买入指示

  • 如果收盘价格低于简单移动平均线,则为卖出指示

定义:

class MySignal(bt.Indicator):
    lines = ('signal',)
    params = (('period', 30),)

    def __init__(self):
        self.lines.signal = self.data - bt.indicators.SMA(period=self.p.period)

现在真的完成了。当执行run时,Cerebro将负责实例化一个特殊的策略实例,该实例知道如何处理信号

初始常见问题

  • 买入/卖出操作的成交量如何确定?

    cerebro实例会自动向策略添加FixedSize调整器。最终用户可以通过cerebro.addsizer更改调整器以改变策略

  • 订单如何执行?

    执行类型为市价,有效期为直到取消

信号技术细节

从技术和理论角度来看可以描述为:

  • 调用时返回另一个对象的可调用函数(仅一次)

    这在大多数情况下是一个类的实例化,但不一定是

  • 支持__getitem__接口。唯一请求的/索引将是0

从实际角度看,看上面的示例,信号是:

  • 来自backtrader生态系统的lines对象,主要是指标

    当使用其他指标时,比如在示例中使用简单移动平均线时,这很有帮助。

信号指示

当使用signal[0]查询信号时,会提供指示,含义是:

  • > 0 -> 买入指示

  • ´< 0 -> 卖出指示

  • ´== 0 -> 无指示

示例使用self.data - SMA进行简单算术运算,并且:

  • 数据高于SMA时发出买入指示

  • 数据低于SMA时发出卖出指示

注意

当未指定特定价格字段用于数据时,收盘价格是参考价格。

信号类型

下面示例中指示的常量直接从主backtrader模块中获取,如:

import backtrader as bt

bt.SIGNAL_LONG

有 5 种信号类型,分为 2 组。

主要组

  • LONGSHORT:此信号的买入卖出指示都被采纳

  • LONG

    • 买入指示用于开多头头寸

    • 卖出指示用于平仓多头头寸。但是:

    • 如果系统中存在LONGEXIT(见下文)信号,将用于退出多头头寸

    • 如果有SHORT信号可用且没有LONGEXIT可用,则会用于在开空头之前关闭多头

  • 空头

    • 空头指示被用来做空

    • 多头指示被用来关闭空头头寸。但是:

    • 如果系统中存在SHORTEXIT信号,则将用于退出空头

    • 如果有LONG信号可用且没有SHORTEXIT可用,则会用于在开多头之前关闭空头

退出组

这两个信号旨在覆盖其他信号,并提供退出多头/空头头寸的标准

  • LONGEXIT空头指示被用来退出多头头寸

  • SHORTEXIT多头指示被用来退出空头头寸

积累和订单并发

上面显示的Signal示例将不断发出多头空头指示,因为它只是从SMA值中减去close价格,这将始终是> 0< 0(有几次== 0

这将导致持续生成订单,产生 2 种情况:

  • 积累:即使已经在市场中,信号也会产生新订单,这将增加市场中的头寸

  • 并发:新订单将被生成,而不必等待其他订单的执行

为了避免这种情况,默认行为是:

  • 不积累

  • 不允许并发

如果希望实现这两种行为中的任何一种,可以通过cerebro进行控制:

  • cerebro.signal_accumulate(True)(或False以重新禁用它)

  • cerebro.signal_concurrency(True)(或False以重新禁用它)

示例

backtrader源代码包含一个用于测试功能的示例。

要使用的主要信号。

class SMACloseSignal(bt.Indicator):
    lines = ('signal',)
    params = (('period', 30),)

    def __init__(self):
        self.lines.signal = self.data - bt.indicators.SMA(period=self.p.period)

以及如果指定了选项,则退出信号

class SMAExitSignal(bt.Indicator):
    lines = ('signal',)
    params = (('p1', 5), ('p2', 30),)

    def __init__(self):
        sma1 = bt.indicators.SMA(period=self.p.p1)
        sma2 = bt.indicators.SMA(period=self.p.p2)
        self.lines.signal = sma1 - sma2

第一次运行:多头和空头

$ ./signals-strategy.py --plot --signal longshort

输出

图片

注意:

  • Signal被绘制。这是正常的,因为它只是一个指标,适用于它的绘图规则

  • 策略实际上是多头空头。这可以看出,因为现金水平从未回到价值水平

  • 旁注:即使是一个愚蠢的想法…(并且没有佣金),策略也没有亏钱…

第二次运行:仅多头

$ ./signals-strategy.py --plot --signal longonly

输出

图片

注意:

  • 这里每次卖出后现金水平都会回到价值水平,这意味着策略已经退出市场

  • 旁注:再次没有损失金钱…

第三次运行:仅空头

$ ./signals-strategy.py --plot --signal shortonly

输出

图片

注意:

  • 第 1 次操作是一个卖出,如预期的那样,比前面 2 个示例中的第 1 次操作晚发生。直到close低于SMA且简单的减法产生负数时才会发生

  • 这里每次买入后现金水平都会回到价值水平,这意味着策略已经退出市场

  • 旁注:最终系统会亏钱

第四次运行:多头 + 多头退出

$ ./signals-strategy.py --plot --signal longonly --exitsignal longexit

输出

图片

注意:

  • 许多交易都是相同的,但有些会提前中断,因为快速移动平均线在退出信号中向下穿越慢速移动平均线。

  • 系统展现了其longonly属性,现金在每笔交易结束时成为价值。

  • 旁注:再次提到金钱……即使进行了一些修改的交易

用法

$ ./signals-strategy.py --help
usage: signals-strategy.py [-h] [--data DATA] [--fromdate FROMDATE]
                           [--todate TODATE] [--cash CASH]
                           [--smaperiod SMAPERIOD] [--exitperiod EXITPERIOD]
                           [--signal {longshort,longonly,shortonly}]
                           [--exitsignal {longexit,shortexit}]
                           [--plot [kwargs]]

Sample for Signal concepts

optional arguments:
  -h, --help            show this help message and exit
  --data DATA           Specific data to be read in (default:
                        ../../datas/2005-2006-day-001.txt)
  --fromdate FROMDATE   Starting date in YYYY-MM-DD format (default: None)
  --todate TODATE       Ending date in YYYY-MM-DD format (default: None)
  --cash CASH           Cash to start with (default: 50000)
  --smaperiod SMAPERIOD
                        Period for the moving average (default: 30)
  --exitperiod EXITPERIOD
                        Period for the exit control SMA (default: 5)
  --signal {longshort,longonly,shortonly}
                        Signal type to use for the main signal (default:
                        longshort)
  --exitsignal {longexit,shortexit}
                        Signal type to use for the exit signal (default: None)
  --plot [kwargs], -p [kwargs]
                        Plot the read data applying any kwargs passed For
                        example: --plot style="candle" (to plot candles)
                        (default: None)

代码

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

import argparse
import collections
import datetime

import backtrader as bt

MAINSIGNALS = collections.OrderedDict(
    (('longshort', bt.SIGNAL_LONGSHORT),
     ('longonly', bt.SIGNAL_LONG),
     ('shortonly', bt.SIGNAL_SHORT),)
)

EXITSIGNALS = {
    'longexit': bt.SIGNAL_LONGEXIT,
    'shortexit': bt.SIGNAL_LONGEXIT,
}

class SMACloseSignal(bt.Indicator):
    lines = ('signal',)
    params = (('period', 30),)

    def __init__(self):
        self.lines.signal = self.data - bt.indicators.SMA(period=self.p.period)

class SMAExitSignal(bt.Indicator):
    lines = ('signal',)
    params = (('p1', 5), ('p2', 30),)

    def __init__(self):
        sma1 = bt.indicators.SMA(period=self.p.p1)
        sma2 = bt.indicators.SMA(period=self.p.p2)
        self.lines.signal = sma1 - sma2

def runstrat(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()
    cerebro.broker.set_cash(args.cash)

    dkwargs = dict()
    if args.fromdate is not None:
        fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
        dkwargs['fromdate'] = fromdate

    if args.todate is not None:
        todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
        dkwargs['todate'] = todate

    # if dataset is None, args.data has been given
    data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs)
    cerebro.adddata(data)

    cerebro.add_signal(MAINSIGNALS[args.signal],
                       SMACloseSignal, period=args.smaperiod)

    if args.exitsignal is not None:
        cerebro.add_signal(EXITSIGNALS[args.exitsignal],
                           SMAExitSignal,
                           p1=args.exitperiod,
                           p2=args.smaperiod)

    cerebro.run()
    if args.plot:
        pkwargs = dict(style='bar')
        if args.plot is not True:  # evals to True but is not True
            npkwargs = eval('dict(' + args.plot + ')')  # args were passed
            pkwargs.update(npkwargs)

        cerebro.plot(**pkwargs)

def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Signal concepts')

    parser.add_argument('--data', required=False,
                        default='../../datas/2005-2006-day-001.txt',
                        help='Specific data to be read in')

    parser.add_argument('--fromdate', required=False, default=None,
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', required=False, default=None,
                        help='Ending date in YYYY-MM-DD format')

    parser.add_argument('--cash', required=False, action='store',
                        type=float, default=50000,
                        help=('Cash to start with'))

    parser.add_argument('--smaperiod', required=False, action='store',
                        type=int, default=30,
                        help=('Period for the moving average'))

    parser.add_argument('--exitperiod', required=False, action='store',
                        type=int, default=5,
                        help=('Period for the exit control SMA'))

    parser.add_argument('--signal', required=False, action='store',
                        default=MAINSIGNALS.keys()[0], choices=MAINSIGNALS,
                        help=('Signal type to use for the main signal'))

    parser.add_argument('--exitsignal', required=False, action='store',
                        default=None, choices=EXITSIGNALS,
                        help=('Signal type to use for the exit signal'))

    # Plot options
    parser.add_argument('--plot', '-p', nargs='?', required=False,
                        metavar='kwargs', const=True,
                        help=('Plot the read data applying any kwargs passed\n'
                              '\n'
                              'For example:\n'
                              '\n'
                              '  --plot style="candle" (to plot candles)\n'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()
posted @ 2024-04-15 11:14  绝不原创的飞龙  阅读(60)  评论(0编辑  收藏  举报