Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Rolling over Futures

Not every provider offers acontinuous future for the instruments with whichone can trade. Sometimes the data offered is that of the still valid expirationdates, i.e.: those still being traded

This is not so helpful when it comes tobacktesting because the data isscattered over several different instruments which additionally …overlapin time.

Being able to properly join the data of those instruments, from the past, intoa continuous stream alleviates the pain. The problem:

  • There is no law as to how best join the different expiration dates into a continuous future

Some literature, courtesy ofSierraChart at:

  • MyLink

The RollOver Data Feed

backtrader has added with 1.8.10.99` the possibility to join futures’ datafrom different expiration dates into a continuous future:

import backtrader as btcerebro = 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()

Note

The possible\*\*kwargs are explained below

It can also be done by directly accessing theRollOver feed (which ishelpful if subclassing is done):

import backtrader as btcerebro = 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()

Note

The possible\*\*kwargs are explained below

Note

When usingRollOver the name is assigned usingdataname. Thisis the standard parameter used for all data feeds to pass thename/ticker. In this case it is reused to assign a common name tothe complete set of rolled over futures.

In the case ofcerebro.rolloverdata, the name is assigned to afeed usingname, which is already one named argument of that method

Bottomline:

  • Data Feeds are created as usual butARE NOT added tocerebro

  • Those data feeds are given as input tobt.feeds.RollOver

    Adataname is also given, mostly for identification purposes.

  • Thisroll over data feed is then added tocerebro

Options for the Roll-Over

Two parameters are provided to control the roll-over process

  • checkdate (default:None)

    This must be acallable with the following signature:

    checkdate(dt, d):

    Where:

    • dt is adatetime.datetime object

    • d is the current data feed for the active future

    Expected Return Values:

    • True: as long as the callable returns this, a switchover can happen to the next future

      If a commodity expires on the 3rd Friday of March,checkdate couldreturnTrue for the entire week in which the expiration takesplace.

    • False: the expiration cannot take place

  • checkcondition (default:None)

    Note: This will only be called ifcheckdate has returnedTrue

    IfNone this will evaluate toTrue (execute roll over) internally

    Else this must be acallable with this signature:

    checkcondition(d0, d1)

    Where:

    • d0 is the current data feed for the active future

    • d1 is the data feed for the next expiration

    Expected Return Values:

    • True: roll-over to the next future

      Following with the example fromcheckdate, this could say that theroll-over can only happend if thevolume fromd0 is already lessthan the volume fromd1

    • False: the expiration cannot take place

SubclassingRollOver

If specifying thecallables isn’t enough, there is always the chance tosubclassRollOver. The methods to subclass:

  • def _checkdate(self, dt, d):

    Which matches thesignature of the parameter of the same name above. Theexpected return values are also the saame.

  • def _checkcondition(self, d0, d1)

    Which matches thesignature of the parameter of the same name above. Theexpected return values are also the saame.

Let’s Roll

Note

The default behavior in the sample is to usecerebro.rolloverdata. This can be changed by passing the-no-cerebro flag. In this case the sample usesRollOver andcerebro.adddata

The implementation includes a sample which is available in thebacktradersources.

Futures concatenation

Let’s start by looking at a pure concatenation by running the sample with noarguments.

$ ./rollover.pyLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.00002, 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.00177, 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.00242, 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.00307, 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.00367, 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.00427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

This usescerebro.chaindata and the result should be clear:

  • Whenever adata feed is over the next one takes over

  • This happens always between aFriday andMonday: the futures in the samples always expire onFriday

Futures roll-over with no checks

Let’s execute with--rollover

$ ./rollover.py --rollover --plotLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.00002, 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.00177, 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.00242, 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.00307, 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.00367, 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.00427, FESX, 199FESXM5, 2015-06-19, Fri, 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 madeon the 3rd Friday of either Mar, Jun, Sep, Dec.

But this is mostly WRONG.backtradr cannot know it, but the author knows thattheEuroStoxx 50 futures stop trading at12:00 CET. So even if there is adaily bar for the 3rd Friday of the expiration month, the change is happeningtoo late.

image

Changing during the Week

Acheckdate callabe is implemented in the sample, which calculates the dateof expiration for the currently active contract.

checkdate will allow a roll over as soon as the week of the 3rd Friday ofthe month is reached (it may beTuesday if for exampleMonday is a bank holiday)

$ ./rollover.py --rollover --checkdate --plotLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.00002, 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.00172, 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.00237, 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.00302, 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.00362, 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.00427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

Much better. The roll over is now happening5 days before. A quick visualinspection of theLen indices show it. For example:

  • 199FESXM4 to199FESXU4 happens atlen171-172. Withoutcheckdate it happened at176-177

The roll over is happening on the Monday before the 3rd Friday of theexpiration month.

image

Adding a volume condition

Even with the improvement, the situation can be further improved in that notonly the date but also de negotiatedvolume will be taken into account. Doswitch when the new contract trades more volume than the currently active one.

Let’s add acheckcondition to the mix and run.

$ ./rollover.py --rollover --checkdate --checkcondition --plotLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.00002, 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.00176, 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.00241, 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.00306, 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.00366, 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.00427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0

Even better*. We have moved the switch date to theThursday before the wellknown3rd Friday of the expiration month

This should come to no surprise because the expiring future trades a lot lesshours on thatFriday and the volume must be small.

Note

The roll over date could have also been set to thatThursday by thecheckdate callable. But that isn’t the point of the sample.

image

Concluding

backtrader includes now a flexible mechanism to allow rolling over futures tocreate a continuous stream.

Sample Usage

$ ./rollover.py --helpusage: rollover.py [-h] [--no-cerebro] [--rollover] [--checkdate]                   [--checkcondition] [--plot [kwargs]]Sample for Roll Over of Futuresoptional 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)

Sample Code

from __future__ import (absolute_import, division, print_function,                        unicode_literals)import argparseimport bisectimport calendarimport datetimeimport backtrader as btclass 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 < d1def 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()

[8]ページ先頭

©2009-2025 Movatter.jp