滚动期货合约#

并非所有提供商都为可以交易的金融工具提供“连续”期货合约。有时提供的数据是当前仍然有效的到期日的数据,即当前仍在交易的期货合约。

对于 回测,这并不是很有帮助,因为数据分散在几个不同的金融工具上,这些工具还……存在时间上的* 重叠*。

能够将过去的这些合约的数据正确地连接成一个连续流,可以减轻痛苦。问题是:

  • 如何最好地将不同到期日的合约连接成一个连续期货合约,没有硬性规定。

一些有关文献,由 SierraChart 提供:

滚动数据源 *** ** ** ** **

backtrader 已经在 1.8.10.99 版本中添加了将来期货合约的数据从不同到期日连接成连续期货合约的可能性。

```python import backtrader as bt ``````python cerebro = bt.Cerebro() data0 = bt.feeds.MyFeed(dataname=’到期0’) data1 = bt.feeds.MyFeed(dataname=’到期1’) … dataN = bt.feeds.MyFeed(dataname=’到期N’)

drollover = cerebro.rolloverdata(data0, data1, …, dataN, name=’我的滚动数据’, **kwargs)

cerebro.run()

注意

下面解释了可能的 **kwargs 参数

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

import backtrader as bt

cerebro = bt.Cerebro() data0 = bt.feeds.MyFeed(dataname=’到期0’) data1 = bt.feeds.MyFeed(dataname=’到期1’) … dataN = bt.feeds.MyFeed(dataname=’到期N’)

drollover = bt.feeds.RollOver(data0, data1, …, dataN, dataname=’我的滚动数据’, **kwargs) cerebro.adddata(drollover)

cerebro.run()

注意

下面解释了可能的 **kwargs 参数

`.. 注意:: 使用 ``RollOver 时使用 dataname 分配名称。这是用于传递所有数据源的标准参数以传递名称/代号。在这种情况下,它被重新使用以为回滚期货套集分配一个常见名称。

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

底线:

  • 数据源像往常一样创建,但不添加到 cerebro 中。

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

    dataname 也被提供,主要用于标识目的。

  • 这个回滚数据源然后被添加到 cerebro

回滚选项#

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

  • checkdate (默认值: None )这个必须是一个具有以下签名的 可调用 函数:

    checkdate(dt, d):

其中:

  • dt 是一个 datetime.datetime 对象

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

预期返回值:

  • True :只要可调用函数返回这个值,就可以进行交替到下一个期货

  • False :期货无法交替

如果某个商品在三月的第三个星期五到期,那么 checkdate 函数可以在到期当周的整个星期内返回 True

  • checkcondition (默认值: None

注意 :只有在 checkdate 返回 True 时才会调用这个函数。如果为 None ,则会在内部计算为 True (执行滚动操作)

否则,必须提供具有以下签名的可调用对象:

  • checkcondition(d0, d1)

其中:

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

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

预期的返回值:

  • True :滚动到下一个期货

  • False :不能发生到期

继承 RollOver ==================如果仅指定 callables 是不够的,总是有可能子类化 RollOver 。可子类化的方法有:

  • def _checkdate(self, dt, d):

这个方法的参数与前面提到的同名参数一致。期望的返回值也相同。

  • def _checkcondition(self, d0, d1)

这个方法的参数与前面提到的同名参数一致。期望的返回值也相同。

行动起来#

注意

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

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

期货拼接#

让我们先来看一个无参数运行的纯拼接示例。

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

This uses cerebro.chaindata and the result should be clear:

  • Whenever a data feed is over the next one takes over

  • This happens always between a 星期五 and 星期一 : the futures in the samples always expire on 星期五

Futures roll-over with no checks#

Let’s execute with –rollover

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

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

The same behavior. It can clearly be seen that contract changes are being made on the 3rd Friday of either Mar, Jun, Sep, Dec.

But this is mostly WRONG. backtradr cannot know it, but the author knows that the EuroStoxx 50 futures stop trading at 12:00 CET. So even if there is a daily bar for the 3rd Friday of the expiration month, the change is happening too late… 缩略图:: rollover.png

在一周内的更改#

在示例中实现了一个名为 checkdate 的可调用函数,它计算当前活动合同的到期日期。

一旦到达每个月的第三个星期五的周(例如,如果 星期一 是公共假日,则可能为 星期二 ), checkdate 就会允许翻转。

$ ./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

翻转发生在到期月份的第三个星期五之前的星期一。


即使有了改进,情况仍然可以进一步改善,不仅考虑日期,而且还会考虑已协商的 交易量 。当新合约的交易量超过当前活动合约时才进行切换。

我们将把 checkcondition 添加到混合中并运行。

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

 数, 名称, 切换前名称, 日期时间, 工作日, 开盘价, 最高价, 最低价, 收盘价, 交易量, 持仓量
 0001, FESX, 199FESXM4, 2013-09-26, 星期四, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0
 0002, FESX, 199FESXM4, 2013-09-27, 星期五, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0
 ...
 0175, FESX, 199FESXM4, 2014-06-19, 星期四, 3307.0, 3330.0, 3300.0, 3321.0, 717979.0, 759122.0
 0176, FESX, 199FESXU4, 2014-06-20, 星期五, 3309.0, 3318.0, 3290.0, 3298.0, 711627.0, 2957641.0
 ...
 0240, FESX, 199FESXU4, 2014-09-18, 星期四, 3249.0, 3275.0, 3243.0, 3270.0, 846600.0, 803202.0
 0241, FESX, 199FESXZ4, 2014-09-19, 星期五, 3273.0, 3293.0, 3250.0, 3252.0, 1042294.0, 3021305.0
 ...
 0305, FESX, 199FESXZ4, 2014-12-18, 星期四, 3095.0, 3175.0, 3085.0, 3172.0, 1309574.0, 889112.0
 0306, FESX, 199FESXH5, 2014-12-19, 星期五, 3195.0, 3200.0, 3106.0, 3147.0, 1329040.0, 2964538.0
 ...
 0365, FESX, 199FESXH5, 2015-03-19, 星期四, 3661.0, 3691.0, 3646.0, 3668.0, 1271122.0, 1054639.0
 0366, FESX, 199FESXM5, 2015-03-20, 星期五, 3607.0, 3664.0, 3595.0, 3646.0, 1182235.0, 3407004.0
 ...
 0426, FESX, 199FESXM5, 2015-06-18, 星期四, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0
 0427, FESX, 199FESXM5, 2015-06-19, 星期五, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

*更好了**。我们将切换日期移到了* 到期月份第三个星期五 *之前的* 星期四*

这是不足为奇的,因为在那个 星期五 ,即将到期的期货交易时间要少得多,交易量必须很小。

备注

检查日期和可调用的 checkdate 也可以将切换日期设置为那个 星期四 。但这不是示例的重点。

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

使用示例#

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

期货滚动示例

可选参数:
  -h, --help            显示此帮助消息并退出
  --no-cerebro          直接使用RollOver (默认: False)
  --rollover
  --checkdate           在到期周更改 (默认: False)
  --checkcondition      在满足给定条件时更改 (默认: False)
  --plot [kwargs], -p [kwargs]
                        绘制读取的数据并应用任何传递的kwargs参数。例如: --plot style="candle" (绘制蜡烛图) (默认: 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()