#
# Copyright 2016 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import deque
from functools import partial
from textwrap import dedent

from numpy import (
    arange,
    array,
    int64,
    full,
    repeat,
    tile,
)
from numpy.testing import assert_almost_equal
import pandas as pd
from pandas import Timestamp, DataFrame

from zipline.assets.continuous_futures import (
    OrderedContracts,
    delivery_predicate
)
from zipline.assets.roll_finder import (
    ROLL_DAYS_FOR_CURRENT_CONTRACT,
    VolumeRollFinder,
)
from zipline.data.minute_bars import FUTURES_MINUTES_PER_DAY
from zipline.errors import SymbolNotFound
import zipline.testing.fixtures as zf


class ContinuousFuturesTestCase(zf.WithCreateBarData,
                                zf.WithMakeAlgo,
                                zf.ZiplineTestCase):

    START_DATE = pd.Timestamp('2015-01-05', tz='UTC')
    END_DATE = pd.Timestamp('2016-10-19', tz='UTC')

    SIM_PARAMS_START = pd.Timestamp('2016-01-26', tz='UTC')
    SIM_PARAMS_END = pd.Timestamp('2016-01-28', tz='UTC')
    SIM_PARAMS_DATA_FREQUENCY = 'minute'
    TRADING_CALENDAR_STRS = ('us_futures',)
    TRADING_CALENDAR_PRIMARY_CAL = 'us_futures'

    ASSET_FINDER_FUTURE_CHAIN_PREDICATES = {
        'BZ': partial(delivery_predicate, set(['F', 'H'])),
    }

    @classmethod
    def make_root_symbols_info(self):
        return pd.DataFrame({
            'root_symbol': ['FO', 'BZ', 'MA', 'DF'],
            'root_symbol_id': [1, 2, 3, 4],
            'exchange': ['CMES', 'CMES', 'CMES', 'CMES']})

    @classmethod
    def make_futures_info(self):
        fo_frame = DataFrame({
            'symbol': ['FOF16', 'FOG16', 'FOH16', 'FOJ16', 'FOK16', 'FOF22',
                       'FOG22'],
            'sid': range(0, 7),
            'root_symbol': ['FO'] * 7,
            'asset_name': ['Foo'] * 7,
            'start_date': [Timestamp('2015-01-05', tz='UTC'),
                           Timestamp('2015-02-05', tz='UTC'),
                           Timestamp('2015-03-05', tz='UTC'),
                           Timestamp('2015-04-05', tz='UTC'),
                           Timestamp('2015-05-05', tz='UTC'),
                           Timestamp('2021-01-05', tz='UTC'),
                           Timestamp('2015-01-05', tz='UTC')],
            'end_date': [Timestamp('2016-08-19', tz='UTC'),
                         Timestamp('2016-09-19', tz='UTC'),
                         Timestamp('2016-10-19', tz='UTC'),
                         Timestamp('2016-11-19', tz='UTC'),
                         Timestamp('2022-08-19', tz='UTC'),
                         Timestamp('2022-09-19', tz='UTC'),
                         # Set the last contract's end date (which is the last
                         # date for which there is data to a value that is
                         # within the range of the dates being tested.  This
                         # models real life scenarios where the end date of the
                         # furthest out contract is not necessarily the
                         # greatest end date all contracts in the chain.
                         Timestamp('2015-02-05', tz='UTC')],
            'notice_date': [Timestamp('2016-01-27', tz='UTC'),
                            Timestamp('2016-02-26', tz='UTC'),
                            Timestamp('2016-03-24', tz='UTC'),
                            Timestamp('2016-04-26', tz='UTC'),
                            Timestamp('2016-05-26', tz='UTC'),
                            Timestamp('2022-01-26', tz='UTC'),
                            Timestamp('2022-02-26', tz='UTC')],
            'expiration_date': [Timestamp('2016-01-27', tz='UTC'),
                                Timestamp('2016-02-26', tz='UTC'),
                                Timestamp('2016-03-24', tz='UTC'),
                                Timestamp('2016-04-26', tz='UTC'),
                                Timestamp('2016-05-26', tz='UTC'),
                                Timestamp('2022-01-26', tz='UTC'),
                                Timestamp('2022-02-26', tz='UTC')],
            'auto_close_date': [Timestamp('2016-01-27', tz='UTC'),
                                Timestamp('2016-02-26', tz='UTC'),
                                Timestamp('2016-03-24', tz='UTC'),
                                Timestamp('2016-04-26', tz='UTC'),
                                Timestamp('2016-05-26', tz='UTC'),
                                Timestamp('2022-01-26', tz='UTC'),
                                Timestamp('2022-02-26', tz='UTC')],
            'tick_size': [0.001] * 7,
            'multiplier': [1000.0] * 7,
            'exchange': ['CMES'] * 7,
        })

        # BZ is set up to test chain predicates, for futures such as PL which
        # only use a subset of contracts for the roll chain.
        bz_frame = DataFrame({
            'symbol': ['BZF16', 'BZG16', 'BZH16'],
            'root_symbol': ['BZ'] * 3,
            'asset_name': ['Baz'] * 3,
            'sid': range(10, 13),
            'start_date': [Timestamp('2005-01-01', tz='UTC'),
                           Timestamp('2005-01-21', tz='UTC'),
                           Timestamp('2005-01-21', tz='UTC')],
            'end_date': [Timestamp('2016-08-19', tz='UTC'),
                         Timestamp('2016-11-21', tz='UTC'),
                         Timestamp('2016-10-19', tz='UTC')],
            'notice_date': [Timestamp('2016-01-11', tz='UTC'),
                            Timestamp('2016-02-08', tz='UTC'),
                            Timestamp('2016-03-09', tz='UTC')],
            'expiration_date': [Timestamp('2016-01-11', tz='UTC'),
                                Timestamp('2016-02-08', tz='UTC'),
                                Timestamp('2016-03-09', tz='UTC')],
            'auto_close_date': [Timestamp('2016-01-11', tz='UTC'),
                                Timestamp('2016-02-08', tz='UTC'),
                                Timestamp('2016-03-09', tz='UTC')],
            'tick_size': [0.001] * 3,
            'multiplier': [1000.0] * 3,
            'exchange': ['CMES'] * 3,
        })

        # MA is set up to test a contract which is has no active volume.
        ma_frame = DataFrame({
            'symbol': ['MAG16', 'MAH16', 'MAJ16'],
            'root_symbol': ['MA'] * 3,
            'asset_name': ['Most Active'] * 3,
            'sid': range(14, 17),
            'start_date': [Timestamp('2005-01-01', tz='UTC'),
                           Timestamp('2005-01-21', tz='UTC'),
                           Timestamp('2005-01-21', tz='UTC')],
            'end_date': [Timestamp('2016-08-19', tz='UTC'),
                         Timestamp('2016-11-21', tz='UTC'),
                         Timestamp('2016-10-19', tz='UTC')],
            'notice_date': [Timestamp('2016-02-17', tz='UTC'),
                            Timestamp('2016-03-16', tz='UTC'),
                            Timestamp('2016-04-13', tz='UTC')],
            'expiration_date': [Timestamp('2016-02-17', tz='UTC'),
                                Timestamp('2016-03-16', tz='UTC'),
                                Timestamp('2016-04-13', tz='UTC')],
            'auto_close_date': [Timestamp('2016-02-17', tz='UTC'),
                                Timestamp('2016-03-16', tz='UTC'),
                                Timestamp('2016-04-13', tz='UTC')],
            'tick_size': [0.001] * 3,
            'multiplier': [1000.0] * 3,
            'exchange': ['CMES'] * 3,
        })

        # DF is set up to have a double volume flip between the 'F' and 'G'
        # contracts, and then a really early temporary volume flip between the
        # 'G' and 'H' contracts.
        df_frame = DataFrame({
            'symbol': ['DFF16', 'DFG16', 'DFH16'],
            'root_symbol': ['DF'] * 3,
            'asset_name': ['Double Flip'] * 3,
            'sid': range(17, 20),
            'start_date': [Timestamp('2005-01-01', tz='UTC'),
                           Timestamp('2005-02-01', tz='UTC'),
                           Timestamp('2005-03-01', tz='UTC')],
            'end_date': [Timestamp('2016-08-19', tz='UTC'),
                         Timestamp('2016-09-19', tz='UTC'),
                         Timestamp('2016-10-19', tz='UTC')],
            'notice_date': [Timestamp('2016-02-19', tz='UTC'),
                            Timestamp('2016-03-18', tz='UTC'),
                            Timestamp('2016-04-22', tz='UTC')],
            'expiration_date': [Timestamp('2016-02-19', tz='UTC'),
                                Timestamp('2016-03-18', tz='UTC'),
                                Timestamp('2016-04-22', tz='UTC')],
            'auto_close_date': [Timestamp('2016-02-17', tz='UTC'),
                                Timestamp('2016-03-16', tz='UTC'),
                                Timestamp('2016-04-20', tz='UTC')],
            'tick_size': [0.001] * 3,
            'multiplier': [1000.0] * 3,
            'exchange': ['CMES'] * 3,
        })

        return pd.concat([fo_frame, bz_frame, ma_frame, df_frame])

    @classmethod
    def make_future_minute_bar_data(cls):
        tc = cls.trading_calendar
        start = pd.Timestamp('2016-01-26', tz='UTC')
        end = pd.Timestamp('2016-04-29', tz='UTC')
        dts = tc.minutes_for_sessions_in_range(start, end)
        sessions = tc.sessions_in_range(start, end)
        # Generate values in the XXY.YYY space, with XX representing the
        # session and Y.YYY representing the minute within the session.
        # e.g. the close of the 23rd session would be 231.440.
        r = 10.0
        day_markers = repeat(
            arange(r, r * len(sessions) + r, r),
            FUTURES_MINUTES_PER_DAY)
        r = 0.001
        min_markers = tile(
            arange(r, r * FUTURES_MINUTES_PER_DAY + r, r),
            len(sessions))

        markers = day_markers + min_markers

        # Volume uses a similar scheme as above but times 1000.
        r = 10.0 * 1000
        vol_day_markers = repeat(
            arange(r, r * len(sessions) + r, r, dtype=int64),
            FUTURES_MINUTES_PER_DAY)
        r = 0.001 * 1000
        vol_min_markers = tile(
            arange(r, r * FUTURES_MINUTES_PER_DAY + r, r, dtype=int64),
            len(sessions))
        vol_markers = vol_day_markers + vol_min_markers
        base_df = pd.DataFrame(
            {
                'open': full(len(dts), 102000.0) + markers,
                'high': full(len(dts), 109000.0) + markers,
                'low': full(len(dts), 101000.0) + markers,
                'close': full(len(dts), 105000.0) + markers,
                'volume': full(len(dts), 10000, dtype=int64) + vol_markers,
            },
            index=dts)
        # Add the sid to the ones place of the prices, so that the ones
        # place can be used to eyeball the source contract.

        # For volume roll tests end sid volume early.
        # FOF16 cuts out day before autoclose of 01-26
        # FOG16 cuts out on autoclose
        # FOH16 cuts out 4 days before autoclose
        # FOJ16 cuts out 3 days before autoclose
        # Make FOG22 have a blip of trading, but not be the actively trading,
        # so that it does not particpate in volume rolls.

        sid_to_vol_stop_session = {
            0: Timestamp('2016-01-26', tz='UTC'),
            1: Timestamp('2016-02-26', tz='UTC'),
            2: Timestamp('2016-03-18', tz='UTC'),
            3: Timestamp('2016-04-20', tz='UTC'),
            6: Timestamp('2016-01-27', tz='UTC'),
        }
        for i in range(20):
            df = base_df.copy()
            df += i * 10000
            if i in sid_to_vol_stop_session:
                vol_stop_session = sid_to_vol_stop_session[i]
                m_open = tc.open_and_close_for_session(vol_stop_session)[0]
                loc = dts.searchsorted(m_open)
                # Add a little bit of noise to roll. So that predicates that
                # check for exactly 0 do not work, since there may be
                # stragglers after a roll.
                df.volume.values[loc] = 1000
                df.volume.values[loc + 1:] = 0
            j = i - 1
            if j in sid_to_vol_stop_session:
                non_primary_end = sid_to_vol_stop_session[j]
                m_close = tc.open_and_close_for_session(non_primary_end)[1]
                if m_close > dts[0]:
                    loc = dts.get_loc(m_close)
                    # Add some volume before a roll, since a contract may be
                    # entered earlier than when it is the primary.
                    df.volume.values[:loc + 1] = 10
            if i == 15:  # No volume for MAH16
                df.volume.values[:] = 0
            if i == 17:
                end_loc = dts.searchsorted('2016-02-16 23:00:00+00:00')
                df.volume.values[:end_loc] = 10
                df.volume.values[end_loc:] = 0
            if i == 18:
                cross_loc_1 = dts.searchsorted('2016-02-09 23:01:00+00:00')
                cross_loc_2 = dts.searchsorted('2016-02-11 23:01:00+00:00')
                cross_loc_3 = dts.searchsorted('2016-02-15 23:01:00+00:00')
                end_loc = dts.searchsorted('2016-03-16 23:01:00+00:00')
                df.volume.values[:cross_loc_1] = 5
                df.volume.values[cross_loc_1:cross_loc_2] = 15
                df.volume.values[cross_loc_2:cross_loc_3] = 5
                df.volume.values[cross_loc_3:end_loc] = 15
                df.volume.values[end_loc:] = 0
            if i == 19:
                early_cross_1 = dts.searchsorted('2016-03-01 23:01:00+00:00')
                early_cross_2 = dts.searchsorted('2016-03-03 23:01:00+00:00')
                end_loc = dts.searchsorted('2016-04-19 23:01:00+00:00')
                df.volume.values[:early_cross_1] = 1
                df.volume.values[early_cross_1:early_cross_2] = 20
                df.volume.values[early_cross_2:end_loc] = 10
                df.volume.values[end_loc:] = 0
            yield i, df

    def test_double_volume_switch(self):
        """
        Test that when a double volume switch occurs we treat the first switch
        as the roll, assuming it is within a certain distance of the next auto
        close date. See `VolumeRollFinder._active_contract` for a full
        explanation and example.
        """
        cf = self.asset_finder.create_continuous_future(
            'DF', 0, 'volume', None,
        )

        sessions = self.trading_calendar.sessions_in_range(
            '2016-02-09', '2016-02-17',
        )
        for session in sessions:
            bar_data = self.create_bardata(lambda: session)
            contract = bar_data.current(cf, 'contract')

            # The 'G' contract surpasses the 'F' contract in volume on
            # 2016-02-10, which means that the 'G' contract should become the
            # front contract starting on 2016-02-11.
            if session < pd.Timestamp('2016-02-11', tz='UTC'):
                self.assertEqual(contract.symbol, 'DFF16')
            else:
                self.assertEqual(contract.symbol, 'DFG16')

        # This test asserts behavior about a back contract briefly spiking in
        # volume, but more than a week before the front contract's auto close
        # date, meaning it does not fall in the 'grace' period used by
        # `VolumeRollFinder._active_contract`. Therefore we should not roll to
        # the back contract and the front contract should remain current until
        # its auto close date.
        sessions = self.trading_calendar.sessions_in_range(
            '2016-03-01', '2016-03-21',
        )
        for session in sessions:
            bar_data = self.create_bardata(lambda: session)
            contract = bar_data.current(cf, 'contract')

            if session < pd.Timestamp('2016-03-17', tz='UTC'):
                self.assertEqual(contract.symbol, 'DFG16')
            else:
                self.assertEqual(contract.symbol, 'DFH16')

    def test_create_continuous_future(self):
        cf_primary = self.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)

        self.assertEqual(cf_primary.root_symbol, 'FO')
        self.assertEqual(cf_primary.offset, 0)
        self.assertEqual(cf_primary.roll_style, 'calendar')
        self.assertEqual(cf_primary.start_date,
                         Timestamp('2015-01-05', tz='UTC'))
        self.assertEqual(cf_primary.end_date,
                         Timestamp('2022-09-19', tz='UTC'))

        retrieved_primary = self.asset_finder.retrieve_asset(
            cf_primary.sid)

        self.assertEqual(retrieved_primary, cf_primary)

        cf_secondary = self.asset_finder.create_continuous_future(
            'FO', 1, 'calendar', None)

        self.assertEqual(cf_secondary.root_symbol, 'FO')
        self.assertEqual(cf_secondary.offset, 1)
        self.assertEqual(cf_secondary.roll_style, 'calendar')
        self.assertEqual(cf_primary.start_date,
                         Timestamp('2015-01-05', tz='UTC'))
        self.assertEqual(cf_primary.end_date,
                         Timestamp('2022-09-19', tz='UTC'))

        retrieved = self.asset_finder.retrieve_asset(
            cf_secondary.sid)

        self.assertEqual(retrieved, cf_secondary)

        self.assertNotEqual(cf_primary, cf_secondary)

        # Assert that the proper exception is raised if the given root symbol
        # does not exist.
        with self.assertRaises(SymbolNotFound):
            self.asset_finder.create_continuous_future(
                'NO', 0, 'calendar', None)

    def test_current_contract(self):
        cf_primary = self.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        bar_data = self.create_bardata(
            lambda: pd.Timestamp('2016-01-26', tz='UTC'))
        contract = bar_data.current(cf_primary, 'contract')

        self.assertEqual(contract.symbol, 'FOF16')

        bar_data = self.create_bardata(
            lambda: pd.Timestamp('2016-01-27', tz='UTC'))
        contract = bar_data.current(cf_primary, 'contract')

        self.assertEqual(contract.symbol, 'FOG16',
                         'Auto close at beginning of session so FOG16 is now '
                         'the current contract.')

    def test_get_value_contract_daily(self):
        cf_primary = self.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)

        contract = self.data_portal.get_spot_value(
            cf_primary,
            'contract',
            pd.Timestamp('2016-01-26', tz='UTC'),
            'daily',
        )

        self.assertEqual(contract.symbol, 'FOF16')

        contract = self.data_portal.get_spot_value(
            cf_primary,
            'contract',
            pd.Timestamp('2016-01-27', tz='UTC'),
            'daily',
        )

        self.assertEqual(contract.symbol, 'FOG16',
                         'Auto close at beginning of session so FOG16 is now '
                         'the current contract.')

        # Test that the current contract outside of the continuous future's
        # start and end dates is None.
        contract = self.data_portal.get_spot_value(
            cf_primary,
            'contract',
            self.START_DATE - self.trading_calendar.day,
            'daily',
        )
        self.assertIsNone(contract)

    def test_get_value_close_daily(self):
        cf_primary = self.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)

        value = self.data_portal.get_spot_value(
            cf_primary,
            'close',
            pd.Timestamp('2016-01-26', tz='UTC'),
            'daily',
        )

        self.assertEqual(value, 105011.44)

        value = self.data_portal.get_spot_value(
            cf_primary,
            'close',
            pd.Timestamp('2016-01-27', tz='UTC'),
            'daily',
        )

        self.assertEqual(value, 115021.44,
                         'Auto close at beginning of session so FOG16 is now '
                         'the current contract.')

        # Check a value which occurs after the end date of the last known
        # contract, to prevent a regression where the end date of the last
        # contract was used instead of the max date of all contracts.
        value = self.data_portal.get_spot_value(
            cf_primary,
            'close',
            pd.Timestamp('2016-03-26', tz='UTC'),
            'daily',
        )

        self.assertEqual(value, 135441.44,
                         'Value should be for FOJ16, even though last '
                         'contract ends before query date.')

    def test_current_contract_volume_roll(self):
        cf_primary = self.asset_finder.create_continuous_future(
            'FO', 0, 'volume', None)
        bar_data = self.create_bardata(
            lambda: pd.Timestamp('2016-01-26', tz='UTC'))
        contract = bar_data.current(cf_primary, 'contract')

        self.assertEqual(contract.symbol, 'FOF16')

        bar_data = self.create_bardata(
            lambda: pd.Timestamp('2016-01-27', tz='UTC'))
        contract = bar_data.current(cf_primary, 'contract')

        self.assertEqual(contract.symbol, 'FOG16',
                         'Auto close at beginning of session. FOG16 is now '
                         'the current contract.')

        bar_data = self.create_bardata(
            lambda: pd.Timestamp('2016-02-29', tz='UTC'))
        contract = bar_data.current(cf_primary, 'contract')
        self.assertEqual(contract.symbol, 'FOH16',
                         'Volume switch to FOH16, should have triggered roll.')

    def test_current_contract_in_algo(self):
        code = dedent("""
from zipline.api import (
    record,
    continuous_future,
    schedule_function,
    get_datetime,
)

def initialize(algo):
    algo.primary_cl = continuous_future('FO', 0, 'calendar', None)
    algo.secondary_cl = continuous_future('FO', 1, 'calendar', None)
    schedule_function(record_current_contract)

def record_current_contract(algo, data):
    record(datetime=get_datetime())
    record(primary=data.current(algo.primary_cl, 'contract'))
    record(secondary=data.current(algo.secondary_cl, 'contract'))
""")
        results = self.run_algorithm(script=code)
        result = results.iloc[0]

        self.assertEqual(result.primary.symbol,
                         'FOF16',
                         'Primary should be FOF16 on first session.')
        self.assertEqual(result.secondary.symbol,
                         'FOG16',
                         'Secondary should be FOG16 on first session.')

        result = results.iloc[1]
        # Second day, primary should switch to FOG
        self.assertEqual(result.primary.symbol,
                         'FOG16',
                         'Primary should be FOG16 on second session, auto '
                         'close is at beginning of the session.')
        self.assertEqual(result.secondary.symbol,
                         'FOH16',
                         'Secondary should be FOH16 on second session, auto '
                         'close is at beginning of the session.')

        result = results.iloc[2]
        # Second day, primary should switch to FOG
        self.assertEqual(result.primary.symbol,
                         'FOG16',
                         'Primary should remain as FOG16 on third session.')
        self.assertEqual(result.secondary.symbol,
                         'FOH16',
                         'Secondary should remain as FOH16 on third session.')

    def test_current_chain_in_algo(self):
        code = dedent("""
from zipline.api import (
    record,
    continuous_future,
    schedule_function,
    get_datetime,
)

def initialize(algo):
    algo.primary_cl = continuous_future('FO', 0, 'calendar', None)
    algo.secondary_cl = continuous_future('FO', 1, 'calendar', None)
    schedule_function(record_current_contract)

def record_current_contract(algo, data):
    record(datetime=get_datetime())
    primary_chain = data.current_chain(algo.primary_cl)
    secondary_chain = data.current_chain(algo.secondary_cl)
    record(primary_len=len(primary_chain))
    record(primary_first=primary_chain[0].symbol)
    record(primary_last=primary_chain[-1].symbol)
    record(secondary_len=len(secondary_chain))
    record(secondary_first=secondary_chain[0].symbol)
    record(secondary_last=secondary_chain[-1].symbol)
""")
        results = self.run_algorithm(script=code)
        result = results.iloc[0]

        self.assertEqual(result.primary_len,
                         6,
                         'There should be only 6 contracts in the chain for '
                         'the primary, there are 7 contracts defined in the '
                         'fixture, but one has a start after the simulation '
                         'date.')
        self.assertEqual(result.secondary_len,
                         5,
                         'There should be only 5 contracts in the chain for '
                         'the primary, there are 7 contracts defined in the '
                         'fixture, but one has a start after the simulation '
                         'date. And the first is not included because it is '
                         'the primary on that date.')

        self.assertEqual(result.primary_first,
                         'FOF16',
                         'Front of primary chain should be FOF16 on first '
                         'session.')
        self.assertEqual(result.secondary_first,
                         'FOG16',
                         'Front of secondary chain should be FOG16 on first '
                         'session.')

        self.assertEqual(result.primary_last,
                         'FOG22',
                         'End of primary chain should be FOK16 on first '
                         'session.')
        self.assertEqual(result.secondary_last,
                         'FOG22',
                         'End of secondary chain should be FOK16 on first '
                         'session.')

        # Second day, primary should switch to FOG
        result = results.iloc[1]

        self.assertEqual(result.primary_len,
                         5,
                         'There should be only 5 contracts in the chain for '
                         'the primary, there are 7 contracts defined in the '
                         'fixture, but one has a start after the simulation '
                         'date. The first is not included because of roll.')
        self.assertEqual(result.secondary_len,
                         4,
                         'There should be only 4 contracts in the chain for '
                         'the primary, there are 7 contracts defined in the '
                         'fixture, but one has a start after the simulation '
                         'date. The first is not included because of roll, '
                         'the second is the primary on that date.')

        self.assertEqual(result.primary_first,
                         'FOG16',
                         'Front of primary chain should be FOG16 on second '
                         'session.')
        self.assertEqual(result.secondary_first,
                         'FOH16',
                         'Front of secondary chain should be FOH16 on second '
                         'session.')

        # These values remain FOJ16 because fixture data is not exhaustive
        # enough to move the end of the chain.
        self.assertEqual(result.primary_last,
                         'FOG22',
                         'End of primary chain should be FOK16 on second '
                         'session.')
        self.assertEqual(result.secondary_last,
                         'FOG22',
                         'End of secondary chain should be FOK16 on second '
                         'session.')

    def test_history_sid_session(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-03-04 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1d', 'sid', 'minute')

        self.assertEqual(window.loc['2016-01-26', cf],
                         0,
                         "Should be FOF16 at beginning of window.")

        self.assertEqual(window.loc['2016-01-27', cf],
                         1,
                         "Should be FOG16 after first roll.")

        self.assertEqual(window.loc['2016-02-25', cf],
                         1,
                         "Should be FOG16 on session before roll.")

        self.assertEqual(window.loc['2016-02-26', cf],
                         2,
                         "Should be FOH16 on session with roll.")

        self.assertEqual(window.loc['2016-02-29', cf],
                         2,
                         "Should be FOH16 on session after roll.")

        # Advance the window a month.
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-04-06 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1d', 'sid', 'minute')

        self.assertEqual(window.loc['2016-02-25', cf],
                         1,
                         "Should be FOG16 at beginning of window.")

        self.assertEqual(window.loc['2016-02-26', cf],
                         2,
                         "Should be FOH16 on session with roll.")

        self.assertEqual(window.loc['2016-02-29', cf],
                         2,
                         "Should be FOH16 on session after roll.")

        self.assertEqual(window.loc['2016-03-24', cf],
                         3,
                         "Should be FOJ16 on session with roll.")

        self.assertEqual(window.loc['2016-03-28', cf],
                         3,
                         "Should be FOJ16 on session after roll.")

    def test_history_sid_session_delivery_predicate(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'BZ', 0, 'calendar', None)
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-01-11 18:01', tz='US/Eastern').tz_convert('UTC'),
            3, '1d', 'sid', 'minute')

        self.assertEqual(window.loc['2016-01-08', cf],
                         10,
                         "Should be BZF16 at beginning of window.")

        self.assertEqual(window.loc['2016-01-11', cf],
                         12,
                         "Should be BZH16 after first roll, having skipped "
                         "over BZG16.")

        self.assertEqual(window.loc['2016-01-12', cf],
                         12,
                         "Should have remained BZG16")

    def test_history_sid_session_secondary(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 1, 'calendar', None)
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-03-04 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1d', 'sid', 'minute')

        self.assertEqual(window.loc['2016-01-26', cf],
                         1,
                         "Should be FOG16 at beginning of window.")

        self.assertEqual(window.loc['2016-01-27', cf],
                         2,
                         "Should be FOH16 after first roll.")

        self.assertEqual(window.loc['2016-02-25', cf],
                         2,
                         "Should be FOH16 on session before roll.")

        self.assertEqual(window.loc['2016-02-26', cf],
                         3,
                         "Should be FOJ16 on session with roll.")

        self.assertEqual(window.loc['2016-02-29', cf],
                         3,
                         "Should be FOJ16 on session after roll.")

        # Advance the window a month.
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-04-06 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1d', 'sid', 'minute')

        self.assertEqual(window.loc['2016-02-25', cf],
                         2,
                         "Should be FOH16 at beginning of window.")

        self.assertEqual(window.loc['2016-02-26', cf],
                         3,
                         "Should be FOJ16 on session with roll.")

        self.assertEqual(window.loc['2016-02-29', cf],
                         3,
                         "Should be FOJ16 on session after roll.")

        self.assertEqual(window.loc['2016-03-24', cf],
                         4,
                         "Should be FOK16 on session with roll.")

        self.assertEqual(window.loc['2016-03-28', cf],
                         4,
                         "Should be FOK16 on session after roll.")

    def test_history_sid_session_volume_roll(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'volume', None)
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-03-04 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1d', 'sid', 'minute')

        # Volume cuts out for FOF16 on 2016-01-25
        self.assertEqual(window.loc['2016-01-26', cf],
                         0,
                         "Should be FOF16 at beginning of window.")

        self.assertEqual(window.loc['2016-01-27', cf],
                         1,
                         "Should have rolled to FOG16.")

        self.assertEqual(window.loc['2016-02-26', cf],
                         1,
                         "Should be FOG16 on session before roll.")

        self.assertEqual(window.loc['2016-02-29', cf],
                         2,
                         "Should be FOH16 on session with roll.")

        self.assertEqual(window.loc['2016-03-01', cf],
                         2,
                         "Should be FOH16 on session after roll.")

        # Advance the window a month.
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-04-06 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1d', 'sid', 'minute')

        self.assertEqual(window.loc['2016-02-26', cf],
                         1,
                         "Should be FOG16 at beginning of window.")

        self.assertEqual(window.loc['2016-02-29', cf],
                         2,
                         "Should be FOH16 on roll session.")

        self.assertEqual(window.loc['2016-03-01', cf],
                         2,
                         "Should remain FOH16.")

        self.assertEqual(window.loc['2016-03-17', cf],
                         2,
                         "Should be FOH16 on session before volume cuts out.")

        self.assertEqual(window.loc['2016-03-18', cf],
                         2,
                         "Should be FOH16 on session where the volume of "
                         "FOH16 cuts out, the roll is upcoming.")

        self.assertEqual(window.loc['2016-03-24', cf],
                         3,
                         "Should have rolled to FOJ16.")

        self.assertEqual(window.loc['2016-03-28', cf],
                         3,
                         "Should have remained FOJ16.")

    def test_history_sid_minute(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        window = self.data_portal.get_history_window(
            [cf.sid],
            Timestamp('2016-01-26 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'sid', 'minute')

        self.assertEqual(window.loc['2016-01-26 22:32', cf],
                         0,
                         "Should be FOF16 at beginning of window. A minute "
                         "which is in the 01-26 session, before the roll.")

        self.assertEqual(window.loc['2016-01-26 23:00', cf],
                         0,
                         "Should be FOF16 on on minute before roll minute.")

        self.assertEqual(window.loc['2016-01-26 23:01', cf],
                         1,
                         "Should be FOG16 on minute after roll.")

        # Advance the window a day.
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-01-27 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'sid', 'minute')

        self.assertEqual(window.loc['2016-01-27 22:32', cf],
                         1,
                         "Should be FOG16 at beginning of window.")

        self.assertEqual(window.loc['2016-01-27 23:01', cf],
                         1,
                         "Should remain FOG16 on next session.")

    def test_history_close_session(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        window = self.data_portal.get_history_window(
            [cf.sid],
            Timestamp('2016-03-06', tz='UTC'),
            30, '1d', 'close', 'daily')

        assert_almost_equal(
            window.loc['2016-01-26', cf],
            105011.440,
            err_msg="At beginning of window, should be FOG16's first value.")

        assert_almost_equal(
            window.loc['2016-02-26', cf],
            125241.440,
            err_msg="On session with roll, should be FOH16's 24th value.")

        assert_almost_equal(
            window.loc['2016-02-29', cf],
            125251.440,
            err_msg="After roll, Should be FOH16's 25th value.")

        # Advance the window a month.
        window = self.data_portal.get_history_window(
            [cf.sid],
            Timestamp('2016-04-06', tz='UTC'),
            30, '1d', 'close', 'daily')

        assert_almost_equal(
            window.loc['2016-02-24', cf],
            115221.440,
            err_msg="At beginning of window, should be FOG16's 22nd value.")

        assert_almost_equal(
            window.loc['2016-02-26', cf],
            125241.440,
            err_msg="On session with roll, should be FOH16's 24th value.")

        assert_almost_equal(
            window.loc['2016-02-29', cf],
            125251.440,
            err_msg="On session after roll, should be FOH16's 25th value.")

        assert_almost_equal(
            window.loc['2016-03-24', cf],
            135431.440,
            err_msg="On session with roll, should be FOJ16's 43rd value.")

        assert_almost_equal(
            window.loc['2016-03-28', cf],
            135441.440,
            err_msg="On session after roll, Should be FOJ16's 44th value.")

    def test_history_close_session_skip_volume(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'MA', 0, 'volume', None)
        window = self.data_portal.get_history_window(
            [cf.sid],
            Timestamp('2016-03-06', tz='UTC'),
            30, '1d', 'close', 'daily')

        assert_almost_equal(
            window.loc['2016-01-26', cf],
            245011.440,
            err_msg="At beginning of window, should be MAG16's first value.")

        assert_almost_equal(
            window.loc['2016-02-26', cf],
            265241.440,
            err_msg="Should have skipped MAH16 to MAJ16.")

        assert_almost_equal(
            window.loc['2016-02-29', cf],
            265251.440,
            err_msg="Should have remained MAJ16.")

        # Advance the window a month.
        window = self.data_portal.get_history_window(
            [cf.sid],
            Timestamp('2016-04-06', tz='UTC'),
            30, '1d', 'close', 'daily')

        assert_almost_equal(
            window.loc['2016-02-24', cf],
            265221.440,
            err_msg="Should be MAJ16, having skipped MAH16.")

        assert_almost_equal(
            window.loc['2016-02-29', cf],
            265251.440,
            err_msg="Should be MAJ1 for rest of window.")

        assert_almost_equal(
            window.loc['2016-03-24', cf],
            265431.440,
            err_msg="Should be MAJ16 for rest of window.")

    def test_history_close_session_adjusted(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        cf_mul = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', 'mul')
        cf_add = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', 'add')
        window = self.data_portal.get_history_window(
            [cf, cf_mul, cf_add],
            Timestamp('2016-03-06', tz='UTC'),
            30, '1d', 'close', 'daily')

        # Unadjusted value is: 115011.44
        # Adjustment is based on hop from 115231.44 to 125231.44
        # a ratio of ~0.920
        assert_almost_equal(
            window.loc['2016-01-26', cf_mul],
            124992.348,
            err_msg="At beginning of window, should be FOG16's first value, "
            "adjusted.")

        # Difference of 7008.561
        assert_almost_equal(
            window.loc['2016-01-26', cf_add],
            125011.44,
            err_msg="At beginning of window, should be FOG16's first value, "
            "adjusted.")

        assert_almost_equal(
            window.loc['2016-02-26', cf_mul],
            125241.440,
            err_msg="On session with roll, should be FOH16's 24th value, "
            "unadjusted.")

        assert_almost_equal(
            window.loc['2016-02-26', cf_add],
            125241.440,
            err_msg="On session with roll, should be FOH16's 24th value, "
            "unadjusted.")

        assert_almost_equal(
            window.loc['2016-02-29', cf_mul],
            125251.440,
            err_msg="After roll, Should be FOH16's 25th value, unadjusted.")

        assert_almost_equal(
            window.loc['2016-02-29', cf_add],
            125251.440,
            err_msg="After roll, Should be FOH16's 25th value, unadjusted.")

        # Advance the window a month.
        window = self.data_portal.get_history_window(
            [cf, cf_mul, cf_add],
            Timestamp('2016-04-06', tz='UTC'),
            30, '1d', 'close', 'daily')

        # Unadjusted value: 115221.44
        # Adjustments based on hops:
        # 2016-02-25 00:00:00+00:00
        # front 115231.440
        # back  125231.440
        # ratio: ~0.920
        # difference: 10000.0
        # and
        # 2016-03-23 00:00:00+00:00
        # front 125421.440
        # back  135421.440
        # ratio: ~1.080
        # difference: 10000.00
        assert_almost_equal(
            window.loc['2016-02-24', cf_mul],
            135236.905,
            err_msg="At beginning of window, should be FOG16's 22nd value, "
            "with two adjustments.")

        assert_almost_equal(
            window.loc['2016-02-24', cf_add],
            135251.44,
            err_msg="At beginning of window, should be FOG16's 22nd value, "
            "with two adjustments")

        # Unadjusted: 125241.44
        assert_almost_equal(
            window.loc['2016-02-26', cf_mul],
            135259.442,
            err_msg="On session with roll, should be FOH16's 24th value, "
            "with one adjustment.")

        assert_almost_equal(
            window.loc['2016-02-26', cf_add],
            135271.44,
            err_msg="On session with roll, should be FOH16's 24th value, "
            "with one adjustment.")

        # Unadjusted: 125251.44
        assert_almost_equal(
            window.loc['2016-02-29', cf_mul],
            135270.241,
            err_msg="On session after roll, should be FOH16's 25th value, "
            "with one adjustment.")

        assert_almost_equal(
            window.loc['2016-02-29', cf_add],
            135281.44,
            err_msg="On session after roll, should be FOH16's 25th value, "
            "unadjusted.")

        # Unadjusted: 135431.44
        assert_almost_equal(
            window.loc['2016-03-24', cf_mul],
            135431.44,
            err_msg="On session with roll, should be FOJ16's 43rd value, "
            "unadjusted.")

        assert_almost_equal(
            window.loc['2016-03-24', cf_add],
            135431.44,
            err_msg="On session with roll, should be FOJ16's 43rd value.")

        # Unadjusted: 135441.44
        assert_almost_equal(
            window.loc['2016-03-28', cf_mul],
            135441.44,
            err_msg="On session after roll, Should be FOJ16's 44th value.")

        assert_almost_equal(
            window.loc['2016-03-28', cf_add],
            135441.44,
            err_msg="On session after roll, Should be FOJ16's 44th value.")

    def test_history_close_minute(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        window = self.data_portal.get_history_window(
            [cf.sid],
            Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'close', 'minute')

        self.assertEqual(window.loc['2016-02-25 22:32', cf],
                         115231.412,
                         "Should be FOG16 at beginning of window. A minute "
                         "which is in the 02-25 session, before the roll.")

        self.assertEqual(window.loc['2016-02-25 23:00', cf],
                         115231.440,
                         "Should be FOG16 on on minute before roll minute.")

        self.assertEqual(window.loc['2016-02-25 23:01', cf],
                         125240.001,
                         "Should be FOH16 on minute after roll.")

        # Advance the window a session.
        window = self.data_portal.get_history_window(
            [cf],
            Timestamp('2016-02-28 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'close', 'minute')

        self.assertEqual(window.loc['2016-02-26 22:32', cf],
                         125241.412,
                         "Should be FOH16 at beginning of window.")

        self.assertEqual(window.loc['2016-02-28 23:01', cf],
                         125250.001,
                         "Should remain FOH16 on next session.")

    def test_history_close_minute_adjusted(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', None)
        cf_mul = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', 'mul')
        cf_add = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'calendar', 'add')
        window = self.data_portal.get_history_window(
            [cf, cf_mul, cf_add],
            Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'close', 'minute')

        # Unadjusted: 115231.412
        # Adjustment based on roll:
        # 2016-02-25 23:00:00+00:00
        # front: 115231.440
        # back:  125231.440
        # Ratio: ~0.920
        # Difference: 10000.00
        self.assertEqual(window.loc['2016-02-25 22:32', cf_mul],
                         125231.41,
                         "Should be FOG16 at beginning of window. A minute "
                         "which is in the 02-25 session, before the roll.")

        self.assertEqual(window.loc['2016-02-25 22:32', cf_add],
                         125231.412,
                         "Should be FOG16 at beginning of window. A minute "
                         "which is in the 02-25 session, before the roll.")

        # Unadjusted: 115231.44
        # Should use same ratios as above.
        self.assertEqual(window.loc['2016-02-25 23:00', cf_mul],
                         125231.44,
                         "Should be FOG16 on on minute before roll minute, "
                         "adjusted.")

        self.assertEqual(window.loc['2016-02-25 23:00', cf_add],
                         125231.44,
                         "Should be FOG16 on on minute before roll minute, "
                         "adjusted.")

        self.assertEqual(window.loc['2016-02-25 23:01', cf_mul],
                         125240.001,
                         "Should be FOH16 on minute after roll, unadjusted.")

        self.assertEqual(window.loc['2016-02-25 23:01', cf_add],
                         125240.001,
                         "Should be FOH16 on minute after roll, unadjusted.")

        # Advance the window a session.
        window = self.data_portal.get_history_window(
            [cf, cf_mul, cf_add],
            Timestamp('2016-02-28 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'close', 'minute')

        # No adjustments in this window.
        self.assertEqual(window.loc['2016-02-26 22:32', cf_mul],
                         125241.412,
                         "Should be FOH16 at beginning of window.")

        self.assertEqual(window.loc['2016-02-28 23:01', cf_mul],
                         125250.001,
                         "Should remain FOH16 on next session.")

    def test_history_close_minute_adjusted_volume_roll(self):
        cf = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'volume', None)
        cf_mul = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'volume', 'mul')
        cf_add = self.data_portal.asset_finder.create_continuous_future(
            'FO', 0, 'volume', 'add')
        window = self.data_portal.get_history_window(
            [cf, cf_mul, cf_add],
            Timestamp('2016-02-28 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'close', 'minute')

        # Unadjusted: 115241.412
        # Adjustment based on roll:
        # 2016-02-25 23:00:00+00:00
        # front: 115241.440 (FOG16)
        # back:  125241.440 (FOH16)
        # Ratio: ~0.920
        # Difference: 10000.00
        self.assertEqual(window.loc['2016-02-26 22:32', cf_mul],
                         125242.973,
                         "Should be FOG16 at beginning of window. A minute "
                         "which is in the 02-25 session, before the roll.")

        self.assertEqual(window.loc['2016-02-26 22:32', cf_add],
                         125242.851,
                         "Should be FOG16 at beginning of window. A minute "
                         "which is in the 02-25 session, before the roll.")

        # Unadjusted: 115231.44
        # Should use same ratios as above.
        self.assertEqual(window.loc['2016-02-26 23:00', cf_mul],
                         125243.004,
                         "Should be FOG16 on minute before roll minute, "
                         "adjusted.")

        self.assertEqual(window.loc['2016-02-26 23:00', cf_add],
                         125242.879,
                         "Should be FOG16 on minute before roll minute, "
                         "adjusted.")

        self.assertEqual(window.loc['2016-02-28 23:01', cf_mul],
                         125250.001,
                         "Should be FOH16 on minute after roll, unadjusted.")

        self.assertEqual(window.loc['2016-02-28 23:01', cf_add],
                         125250.001,
                         "Should be FOH16 on minute after roll, unadjusted.")

        # Advance the window a session.
        window = self.data_portal.get_history_window(
            [cf, cf_mul, cf_add],
            Timestamp('2016-02-29 18:01', tz='US/Eastern').tz_convert('UTC'),
            30, '1m', 'close', 'minute')

        # No adjustments in this window.
        self.assertEqual(window.loc['2016-02-29 22:32', cf_mul],
                         125251.412,
                         "Should be FOH16 at beginning of window.")

        self.assertEqual(window.loc['2016-02-29 23:01', cf_mul],
                         125260.001,
                         "Should remain FOH16 on next session.")


class RollFinderTestCase(zf.WithBcolzFutureDailyBarReader,
                         zf.ZiplineTestCase):

    START_DATE = pd.Timestamp('2017-01-03', tz='UTC')
    END_DATE = pd.Timestamp('2017-05-23', tz='UTC')

    TRADING_CALENDAR_STRS = ('us_futures',)
    TRADING_CALENDAR_PRIMARY_CAL = 'us_futures'

    @classmethod
    def init_class_fixtures(cls):
        super(RollFinderTestCase, cls).init_class_fixtures()

        cls.volume_roll_finder = VolumeRollFinder(
            cls.trading_calendar,
            cls.asset_finder,
            cls.bcolz_future_daily_bar_reader,
        )

    @classmethod
    def make_futures_info(cls):
        day = cls.trading_calendar.day
        two_days = 2 * day
        end_buffer_days = ROLL_DAYS_FOR_CURRENT_CONTRACT * day

        cls.first_end_date = pd.Timestamp('2017-01-20', tz='UTC')
        cls.second_end_date = pd.Timestamp('2017-02-17', tz='UTC')
        cls.third_end_date = pd.Timestamp('2017-03-17', tz='UTC')
        cls.third_auto_close_date = cls.third_end_date - two_days
        cls.fourth_start_date = cls.third_auto_close_date - two_days
        cls.fourth_end_date = pd.Timestamp('2017-04-17', tz='UTC')
        cls.fourth_auto_close_date = cls.fourth_end_date + two_days
        cls.fifth_start_date = pd.Timestamp('2017-03-15', tz='UTC')
        cls.fifth_end_date = cls.END_DATE
        cls.fifth_auto_close_date = cls.fifth_end_date - two_days
        cls.last_start_date = cls.fourth_end_date

        return pd.DataFrame.from_dict(
            {
                1000: {
                    'symbol': 'CLF17',
                    'root_symbol': 'CL',
                    'start_date': cls.START_DATE,
                    'end_date': cls.first_end_date,
                    'auto_close_date': cls.first_end_date - two_days,
                    'exchange': 'CMES',
                },
                1001: {
                    'symbol': 'CLG17',
                    'root_symbol': 'CL',
                    'start_date': cls.START_DATE,
                    'end_date': cls.second_end_date,
                    'auto_close_date': cls.second_end_date - two_days,
                    'exchange': 'CMES',
                },
                1002: {
                    'symbol': 'CLH17',
                    'root_symbol': 'CL',
                    'start_date': cls.START_DATE,
                    'end_date': cls.third_end_date,
                    'auto_close_date': cls.third_auto_close_date,
                    'exchange': 'CMES',
                },
                1003: {
                    'symbol': 'CLJ17',
                    'root_symbol': 'CL',
                    'start_date': cls.fourth_start_date,
                    'end_date': cls.fourth_end_date,
                    'auto_close_date': cls.fourth_auto_close_date,
                    'exchange': 'CMES',
                },
                1004: {
                    'symbol': 'CLK17',
                    'root_symbol': 'CL',
                    'start_date': cls.fifth_start_date,
                    'end_date': cls.fifth_end_date,
                    'auto_close_date': cls.fifth_auto_close_date,
                    'exchange': 'CMES',
                },
                1005: {
                    'symbol': 'CLM17',
                    'root_symbol': 'CL',
                    'start_date': cls.last_start_date,
                    'end_date': cls.END_DATE,
                    'auto_close_date': cls.END_DATE + two_days,
                    'exchange': 'CMES',
                },
                1006: {
                    'symbol': 'CLN17',
                    'root_symbol': 'CL',
                    'start_date': cls.last_start_date,
                    'end_date': cls.END_DATE,
                    'auto_close_date': cls.END_DATE + two_days,
                    'exchange': 'CMES',
                },
                2000: {
                    # Using a placeholder month of 'A' to mean this is the
                    # first contract in the chain.
                    'symbol': 'FVA17',
                    'root_symbol': 'FV',
                    'start_date': cls.START_DATE,
                    'end_date': cls.END_DATE + end_buffer_days,
                    'auto_close_date': cls.END_DATE + two_days,
                    'exchange': 'CMES',
                },
                2001: {
                    # Using a placeholder month of 'B' to mean this is the
                    # second contract in the chain.
                    'symbol': 'FVB17',
                    'root_symbol': 'FV',
                    'start_date': cls.START_DATE,
                    'end_date': cls.END_DATE + end_buffer_days,
                    'auto_close_date': cls.END_DATE + end_buffer_days,
                    'exchange': 'CMES',
                },
            },
            orient='index',
        )

    @classmethod
    def make_future_daily_bar_data(cls):
        """
        Volume data should look like this:

                     CLF17    CLG17    CLH17    CLJ17    CLK17    CLM17   CLN17
       2017-01-03     2000     1000        5        0        0        0       0
       2017-01-04     2000     1000        5        0        0        0       0
           ...
       2017-01-16     2000     1000        5        0        0        0       0
       2017-01-17     2000     1000        5        0        0        0       0
ACD -> 2017-01-18     2000_    1000        5        0        0        0       0
       2017-01-19     2000 `-> 1000        5        0        0        0       0
       2017-01-20     2000     1000        5        0        0        0       0
       2017-01-23        0     1000        5        0        0        0       0
           ...
       2017-02-09        0     1000        5        0        0        0       0
       2017-02-10        0     1000_    5000        0        0        0       0
       2017-02-13        0     1000 `-> 5000        0        0        0       0
       2017-02-14        0     1000     5000        0        0        0       0
ACD -> 2017-02-15        0     1000     5000        0        0        0       0
       2017-02-16        0     1000     5000        0        0        0       0
       2017-02-17        0     1000     5000        0        0        0       0
       2017-02-20        0        0     5000        0        0        0       0
           ...
       2017-03-10        0        0     5000        0        0        0       0
       2017-03-13        0        0     5000     4000        0        0       0
       2017-03-14        0        0     5000     4000        0        0       0
ACD -> 2017-03-15        0        0     5000_    4000     3000        0       0
       2017-03-16        0        0     5000 `-> 4000     3000        0       0
       2017-03-17        0        0     5000     4000     3000        0       0
       2017-03-20        0        0        0     4000     3000        0       0
           ...
       2017-04-14        0        0        0     4000     3000        0       0
       2017-04-17        0        0        0     4000_    3000        0       0
       2017-04-18        0        0        0        0 `-> 3000        0       0
ACD -> 2017-04-19        0        0        0        0     3000     1000    2000
       2017-04-20        0        0        0        0     3000     1000    2000
       2017-04-21        0        0        0        0     3000     1000    2000
           ...
       2017-05-16        0        0        0        0     3000     1000    2000
       2017-05-17        0        0        0        0     3000     1000    2000
       2017-05-18        0        0        0        0     3000_    1000    2000
ACD -> 2017-05-19        0        0        0        0     3000 `---1000--> 2000
       2017-05-22        0        0        0        0     3000     1000    2000
       2017-05-23        0        0        0        0     3000     1000    2000

        The first roll occurs because we reach the auto close date of CLF17.
        The second roll occurs because the volume of CLH17 overtakes CLG17.
        The third roll is testing the fact that CLJ17 has no data in the grace
        period before CLH17's auto close date.
        The fourth roll is testing that we properly handle the case where a
        contract's auto close date is *after* its end date.
        The fifth roll occurs on the auto close date of CLK17, but we skip over
        CLM17 because of it's low volume, and roll directly to CLN17. This is
        used to cover an edge case where the window passed to get_rolls end on
        the auto close date of CLK17.

        A volume of zero here is used to represent the fact that a contract no
        longer exists.
        """
        date_index = cls.trading_calendar.sessions_in_range(
            cls.START_DATE, cls.END_DATE,
        )

        def create_contract_data(volume):
            # The prices used here are arbitrary as they are irrelevant for the
            # purpose of testing roll behavior.
            return DataFrame(
                {'open': 5, 'high': 6, 'low': 4, 'close': 5, 'volume': volume},
                index=date_index,
            )

        # Make a copy because we are taking a slice of a data frame.
        first_contract_data = create_contract_data(2000)
        yield 1000, first_contract_data.copy().loc[:cls.first_end_date]

        # Make a copy because we are taking a slice of a data frame.
        second_contract_data = create_contract_data(1000)
        yield 1001, second_contract_data.copy().loc[:cls.second_end_date]

        third_contract_data = create_contract_data(5)
        volume_flip_date = pd.Timestamp('2017-02-10', tz='UTC')
        third_contract_data.loc[volume_flip_date:, 'volume'] = 5000
        yield 1002, third_contract_data

        # Make a copy because we are taking a slice of a data frame.
        fourth_contract_data = create_contract_data(4000)
        yield (
            1003,
            fourth_contract_data.copy().loc[
                cls.fourth_start_date:cls.fourth_end_date
            ]
        )

        # Make a copy because we are taking a slice of a data frame.
        fifth_contract_data = create_contract_data(3000)
        yield 1004, fifth_contract_data.copy().loc[cls.fifth_start_date:]

        sixth_contract_data = create_contract_data(1000)
        yield 1005, sixth_contract_data.copy().loc[cls.last_start_date:]

        seventh_contract_data = create_contract_data(2000)
        yield 1006, seventh_contract_data.copy().loc[cls.last_start_date:]

        # The data for FV does not really matter except that contract 2000 has
        # higher volume than contract 2001.
        yield 2000, create_contract_data(200)
        yield 2001, create_contract_data(100)

    def test_volume_roll(self):
        """
        Test normally behaving rolls.
        """
        rolls = self.volume_roll_finder.get_rolls(
            root_symbol='CL',
            start=self.START_DATE + self.trading_calendar.day,
            end=self.second_end_date,
            offset=0,
        )
        self.assertEqual(
            rolls,
            [
                (1000, pd.Timestamp('2017-01-19', tz='UTC')),
                (1001, pd.Timestamp('2017-02-13', tz='UTC')),
                (1002, None),
            ],
        )

    def test_no_roll(self):
        # If we call 'get_rolls' with start and end dates that do not have any
        # rolls between them, we should still expect the last roll date to be
        # computed successfully.
        date_not_near_roll = pd.Timestamp('2017-02-01', tz='UTC')
        rolls = self.volume_roll_finder.get_rolls(
            root_symbol='CL',
            start=date_not_near_roll,
            end=date_not_near_roll + self.trading_calendar.day,
            offset=0,
        )
        self.assertEqual(rolls, [(1001, None)])

    def test_roll_in_grace_period(self):
        """
        The volume roll finder can look for data up to a week before the given
        date. This test asserts that we not only return the correct active
        contract during that previous week (grace period), but also that we do
        not go into exception if one of the contracts does not exist.
        """
        rolls = self.volume_roll_finder.get_rolls(
            root_symbol='CL',
            start=self.second_end_date,
            end=self.third_end_date,
            offset=0,
        )
        self.assertEqual(
            rolls,
            [
                (1002, pd.Timestamp('2017-03-16', tz='UTC')),
                (1003, None),
            ],
        )

    def test_end_before_auto_close(self):
        # Test that we correctly roll from CLJ17 (1003) to CLK17 (1004) even
        # though CLJ17 has an auto close date after its end date.
        rolls = self.volume_roll_finder.get_rolls(
            root_symbol='CL',
            start=self.fourth_start_date,
            end=self.fourth_auto_close_date,
            offset=0,
        )
        self.assertEqual(
            rolls,
            [
                (1002, pd.Timestamp('2017-03-16', tz='UTC')),
                (1003, pd.Timestamp('2017-04-18', tz='UTC')),
                (1004, None),
            ],
        )

    def test_roll_window_ends_on_auto_close(self):
        """
        Test that when skipping over a low volume contract (CLM17), we use the
        correct roll date for the previous contract (CLK17) when that
        contract's auto close date falls on the end date of the roll window.
        """
        rolls = self.volume_roll_finder.get_rolls(
            root_symbol='CL',
            start=self.last_start_date,
            end=self.fifth_auto_close_date,
            offset=0,
        )
        self.assertEqual(
            rolls,
            [
                (1003, pd.Timestamp('2017-04-18', tz='UTC')),
                (1004, pd.Timestamp('2017-05-19', tz='UTC')),
                (1006, None),
            ],
        )

    def test_get_contract_center(self):
        asset_finder = self.asset_finder
        get_contract_center = partial(
            self.volume_roll_finder.get_contract_center, offset=0,
        )

        # Test that the current contract adheres to the rolls.
        self.assertEqual(
            get_contract_center('CL', dt=pd.Timestamp('2017-01-18', tz='UTC')),
            asset_finder.retrieve_asset(1000),
        )
        self.assertEqual(
            get_contract_center('CL', dt=pd.Timestamp('2017-01-19', tz='UTC')),
            asset_finder.retrieve_asset(1001),
        )

        # Test that we still get the correct current contract close to or at
        # the max day boundary. Contracts 2000 and 2001 both have auto close
        # dates after `self.END_DATE` so 2000 should always be the current
        # contract. However, they do not have any volume data after this point
        # so this test ensures that we do not fail to calculate the forward
        # looking rolls required for `VolumeRollFinder.get_contract_center`.
        near_end = self.END_DATE - self.trading_calendar.day
        self.assertEqual(
            get_contract_center('FV', dt=near_end),
            asset_finder.retrieve_asset(2000),
        )
        self.assertEqual(
            get_contract_center('FV', dt=self.END_DATE),
            asset_finder.retrieve_asset(2000),
        )


class OrderedContractsTestCase(zf.WithAssetFinder, zf.ZiplineTestCase):

    @classmethod
    def make_root_symbols_info(self):
        return pd.DataFrame({
            'root_symbol': ['FO', 'BA', 'BZ'],
            'root_symbol_id': [1, 2, 3],
            'exchange': ['CMES', 'CMES', 'CMES']})

    @classmethod
    def make_futures_info(self):
        fo_frame = DataFrame({
            'root_symbol': ['FO'] * 4,
            'asset_name': ['Foo'] * 4,
            'symbol': ['FOF16', 'FOG16', 'FOH16', 'FOJ16'],
            'sid': range(1, 5),
            'start_date': pd.date_range('2015-01-01', periods=4, tz="UTC"),
            'end_date': pd.date_range('2016-01-01', periods=4, tz="UTC"),
            'notice_date': pd.date_range('2016-01-01', periods=4, tz="UTC"),
            'expiration_date': pd.date_range(
                '2016-01-01', periods=4, tz="UTC"),
            'auto_close_date': pd.date_range(
                '2016-01-01', periods=4, tz="UTC"),
            'tick_size': [0.001] * 4,
            'multiplier': [1000.0] * 4,
            'exchange': ['CMES'] * 4,
        })
        # BA is set up to test a quarterly roll, to test Eurodollar-like
        # behavior
        # The roll should go from BAH16 -> BAM16
        ba_frame = DataFrame({
            'root_symbol': ['BA'] * 3,
            'asset_name': ['Bar'] * 3,
            'symbol': ['BAF16', 'BAG16', 'BAH16'],
            'sid': range(5, 8),
            'start_date': pd.date_range('2015-01-01', periods=3, tz="UTC"),
            'end_date': pd.date_range('2016-01-01', periods=3, tz="UTC"),
            'notice_date': pd.date_range('2016-01-01', periods=3, tz="UTC"),
            'expiration_date': pd.date_range(
                '2016-01-01', periods=3, tz="UTC"),
            'auto_close_date': pd.date_range(
                '2016-01-01', periods=3, tz="UTC"),
            'tick_size': [0.001] * 3,
            'multiplier': [1000.0] * 3,
            'exchange': ['CMES'] * 3,
        })
        # BZ is set up to test the case where the first contract in a chain has
        # an auto close date before its start date. It also tests the case
        # where a contract in the chain has a start date after the auto close
        # date of the previous contract, leaving a gap with no active contract.
        bz_frame = DataFrame({
            'root_symbol': ['BZ'] * 4,
            'asset_name': ['Baz'] * 4,
            'symbol': ['BZF15', 'BZG15', 'BZH15', 'BZJ16'],
            'sid': range(8, 12),
            'start_date': [
                pd.Timestamp('2015-01-02', tz='UTC'),
                pd.Timestamp('2015-01-03', tz='UTC'),
                pd.Timestamp('2015-02-23', tz='UTC'),
                pd.Timestamp('2015-02-24', tz='UTC'),
            ],
            'end_date': pd.date_range(
                '2015-02-01', periods=4, freq='MS', tz='UTC',
            ),
            'notice_date': [
                pd.Timestamp('2014-12-31', tz='UTC'),
                pd.Timestamp('2015-02-18', tz='UTC'),
                pd.Timestamp('2015-03-18', tz='UTC'),
                pd.Timestamp('2015-04-17', tz='UTC'),
            ],
            'expiration_date': pd.date_range(
                '2015-02-01', periods=4, freq='MS', tz='UTC',
            ),
            'auto_close_date': [
                pd.Timestamp('2014-12-29', tz='UTC'),
                pd.Timestamp('2015-02-16', tz='UTC'),
                pd.Timestamp('2015-03-16', tz='UTC'),
                pd.Timestamp('2015-04-15', tz='UTC'),
            ],
            'tick_size': [0.001] * 4,
            'multiplier': [1000.0] * 4,
            'exchange': ['CMES'] * 4,
        })

        return pd.concat([fo_frame, ba_frame, bz_frame])

    def test_contract_at_offset(self):
        contract_sids = array([1, 2, 3, 4], dtype=int64)
        start_dates = pd.date_range('2015-01-01', periods=4, tz="UTC")

        contracts = deque(self.asset_finder.retrieve_all(contract_sids))

        oc = OrderedContracts('FO', contracts)

        self.assertEquals(1,
                          oc.contract_at_offset(1, 0, start_dates[-1].value),
                          "Offset of 0 should return provided sid")

        self.assertEquals(2,
                          oc.contract_at_offset(1, 1, start_dates[-1].value),
                          "Offset of 1 should return next sid in chain.")

        self.assertEquals(None,
                          oc.contract_at_offset(4, 1, start_dates[-1].value),
                          "Offset at end of chain should not crash.")

    def test_active_chain(self):
        contract_sids = array([1, 2, 3, 4], dtype=int64)

        contracts = deque(self.asset_finder.retrieve_all(contract_sids))

        oc = OrderedContracts('FO', contracts)

        # Test sid 1 as days increment, as the sessions march forward
        # a contract should be added per day, until all defined contracts
        # are returned.
        chain = oc.active_chain(1, pd.Timestamp('2014-12-31', tz='UTC').value)
        self.assertEquals([], list(chain),
                          "On session before first start date, no contracts "
                          "in chain should be active.")
        chain = oc.active_chain(1, pd.Timestamp('2015-01-01', tz='UTC').value)
        self.assertEquals([1], list(chain),
                          "[1] should be the active chain on 01-01, since all "
                          "other start dates occur after 01-01.")

        chain = oc.active_chain(1, pd.Timestamp('2015-01-02', tz='UTC').value)
        self.assertEquals([1, 2], list(chain),
                          "[1, 2] should be the active contracts on 01-02.")

        chain = oc.active_chain(1, pd.Timestamp('2015-01-03', tz='UTC').value)
        self.assertEquals([1, 2, 3], list(chain),
                          "[1, 2, 3] should be the active contracts on 01-03.")

        chain = oc.active_chain(1, pd.Timestamp('2015-01-04', tz='UTC').value)
        self.assertEquals(4, len(chain),
                          "[1, 2, 3, 4] should be the active contracts on "
                          "01-04, this is all defined contracts in the test "
                          "case.")

        chain = oc.active_chain(1, pd.Timestamp('2015-01-05', tz='UTC').value)
        self.assertEquals(4, len(chain),
                          "[1, 2, 3, 4] should be the active contracts on "
                          "01-05. This tests the case where all start dates "
                          "are before the query date.")

        # Test querying each sid at a time when all should be alive.
        chain = oc.active_chain(2, pd.Timestamp('2015-01-05', tz='UTC').value)
        self.assertEquals([2, 3, 4], list(chain))

        chain = oc.active_chain(3, pd.Timestamp('2015-01-05', tz='UTC').value)
        self.assertEquals([3, 4], list(chain))

        chain = oc.active_chain(4, pd.Timestamp('2015-01-05', tz='UTC').value)
        self.assertEquals([4], list(chain))

        # Test defined contract to check edge conditions.
        chain = oc.active_chain(4, pd.Timestamp('2015-01-03', tz='UTC').value)
        self.assertEquals([], list(chain),
                          "No contracts should be active, since 01-03 is "
                          "before 4's start date.")

        chain = oc.active_chain(4, pd.Timestamp('2015-01-04', tz='UTC').value)
        self.assertEquals([4], list(chain),
                          "[4] should be active beginning at its start date.")

    def test_delivery_predicate(self):
        contract_sids = range(5, 8)
        contracts = deque(self.asset_finder.retrieve_all(contract_sids))

        oc = OrderedContracts('BA', contracts,
                              chain_predicate=partial(delivery_predicate,
                                                      set(['F', 'H'])))

        # Test sid 1 as days increment, as the sessions march forward
        # a contract should be added per day, until all defined contracts
        # are returned.
        chain = oc.active_chain(5, pd.Timestamp('2015-01-05', tz='UTC').value)
        self.assertEquals(
            [5, 7], list(chain),
            "Contract BAG16 (sid=6) should be ommitted from chain, since "
            "it does not satisfy the roll predicate.")

    def test_auto_close_before_start(self):
        contract_sids = array([8, 9, 10, 11], dtype=int64)
        contracts = self.asset_finder.retrieve_all(contract_sids)
        oc = OrderedContracts('BZ', deque(contracts))

        # The OrderedContracts chain should omit BZF16 and start with BZG16.
        self.assertEqual(oc.start_date, contracts[1].start_date)
        self.assertEqual(oc.end_date, contracts[-1].end_date)
        self.assertEqual(oc.contract_before_auto_close(oc.start_date.value), 9)

        # The OrderedContracts chain should end on the last contract even
        # though there is a gap between the auto close date of BZG16 and the
        # start date of BZH16. During this period, BZH16 should be considered
        # the center contract, as a placeholder of sorts.
        self.assertEqual(
            oc.contract_before_auto_close(contracts[1].notice_date.value),
            10,
        )
        self.assertEqual(
            oc.contract_before_auto_close(contracts[2].start_date.value),
            10,
        )


class NoPrefetchContinuousFuturesTestCase(ContinuousFuturesTestCase):
    DATA_PORTAL_MINUTE_HISTORY_PREFETCH = 0
    DATA_PORTAL_DAILY_HISTORY_PREFETCH = 0
