Continuous_future returning incorrect front contract

We’re running the sample dual-moving average backtest with ES futures, and it appears that the current behavior for continuous_future() and current_chain() is broken.

This is what the current_chain() returns when running the backtest on 2020-03-19:

        quantrocket_zipline_1|cc  [Future(QF000000022581 [ESH0]), Future(QF000000026993 [ESH1]), Future(QF000000021829 [ESH9]), Future(QF000000022742 [ESM0]), Future(QF000000021972 [ESM9]), Future(QF000000022925 [ESU0]), Future(QF000000022143 [ESU9]), Future(QF000000023069 [ESZ0]), Future(QF000000022392 [ESZ9])]

The front contract should be ESM0 since the rollover date is 2020-03-18.

And this is on 2020-03-24:

        quantrocket_zipline_1|cc  [Future(QF000000026993 [ESH1]), Future(QF000000021829 [ESH9]), Future(QF000000022742 [ESM0]), Future(QF000000021972 [ESM9]), Future(QF000000022925 [ESU0]), Future(QF000000022143 [ESU9]), Future(QF000000023069 [ESZ0]), Future(QF000000022392 [ESZ9])]

The front contract should be ESM0.

We haven’t customized the rollover rules with quantrocket.master.rollover.yml , and we’ve confirmed the master database has the correct data:

jupyter:/codeload $ quantrocket master get --exchanges 'GLOBEX' --symbols 'ES' --sec-types 'FUT' --fields Sid Symbol LastTradeDate RolloverDate | csvlook
| Sid            | Symbol |       LastTradeDate | RolloverDate |
| -------------- | ------ | ------------------- | ------------ |
| QF000000021670 | ESZ8   | 2018-12-21 00:00:00 |   2018-12-19 |
| QF000000021829 | ESH9   | 2019-03-15 00:00:00 |   2019-03-13 |
| QF000000021972 | ESM9   | 2019-06-21 00:00:00 |   2019-06-19 |
| QF000000022143 | ESU9   | 2019-09-20 00:00:00 |   2019-09-18 |
| QF000000022392 | ESZ9   | 2019-12-20 00:00:00 |   2019-12-18 |
| QF000000022581 | ESH0   | 2020-03-20 00:00:00 |   2020-03-18 |
| QF000000022742 | ESM0   | 2020-06-19 00:00:00 |   2020-06-17 |
| QF000000022925 | ESU0   | 2020-09-18 00:00:00 |   2020-09-16 |
| QF000000023069 | ESZ0   | 2020-12-18 00:00:00 |   2020-12-16 |
| QF000000026993 | ESH1   | 2021-03-19 00:00:00 |   2021-03-17 |
| QF000000030268 | ESM1   | 2021-06-18 00:00:00 |   2021-06-16 |
| QF000000527303 | ESU1   | 2021-09-17 00:00:00 |   2021-09-15 |
| QF000000617954 | ESZ1   | 2021-12-17 00:00:00 |   2021-12-15 |

It looks like there’s a bug in regard to how the chain is constructed. The contracts are being returned in alphabetical order, and also the rollover date is being ignored even though we’re using continuous_future('ES', roll='calendar').

Let us know if you need any additional repro steps. We plan to work around this by reconstructing the chain ourselves from current_chain().

Investigating…

Thanks! We’re using this as a workaround for now:

# Workaround
def initialize_context(context, exchanges, sec_types, symbols):
    """
    Initialize algorithm context with QuantRocket's master file.
    """

    # FIXME: https://support.quantrocket.com/t/continuous-future-returning-incorrect-front-contract/1478
    master_file = StringIO()
    download_master_file(master_file, exchanges=exchanges,
                         sec_types=sec_types, symbols=symbols)

    context.master = pd.read_csv(master_file, parse_dates=[
                                 'LastTradeDate', 'RolloverDate']).sort_values(by=['RolloverDate'])

    return context


def get_active_contracts(context):
    dt = get_datetime()
    active_contracts = context.master[context.master.RolloverDate >= dt]
    return active_contracts


def get_front_contract(context):
    active_contracts = get_active_contracts(context)
    front_contract = active_contracts.iloc[0]

    return front_contract

# Algo code
context = initialize_context(context, 'GLOBEX', 'FUT', 'ES')
front_contract = get_front_contract(context)
context.contract = sid(front_contract.Sid)

Regarding the contracts being out of order in current_chain, Zipline loads the contracts from the database in order of Zipline sid, evidently assuming that futures will be inserted into the database in contract order. We will patch this in the next release to load by auto_close_date (= LastTradeDate) so they’ll be in the correct order.

Regarding rollover dates, Zipline doesn’t have a rollover date field, so the calendar roll is actually rolling on LastTradeDate. Rolling on RolloverDate would be better, so we will add that field to Zipline in the next release as well.

Thanks for the follow-up. Will roll=volume work too? We tried that one and it had the same ordering issue, and we don’t have an easy workaround for calculating that roll method ourselves.

Yes, fixing the contract order will fix roll=volume too.

The contract order is fixed in version 2.3.0, and “calendar” now rolls on RolloverDate.