Skip to content

Commit

Permalink
BUG: Check contract exists when using futures daily bar reader
Browse files Browse the repository at this point in the history
  • Loading branch information
dmichalowicz committed Jul 20, 2017
1 parent 15fe38c commit 75fc0fc
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 5 deletions.
149 changes: 148 additions & 1 deletion tests/test_continuous_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
OrderedContracts,
delivery_predicate
)
from zipline.assets.roll_finder import VolumeRollFinder
from zipline.data.minute_bars import FUTURES_MINUTES_PER_DAY
from zipline.errors import SymbolNotFound
from zipline.testing.fixtures import (
WithAssetFinder,
WithBcolzFutureDailyBarReader,
WithBcolzFutureMinuteBarReader,
WithCreateBarData,
WithDataPortal,
WithBcolzFutureMinuteBarReader,
WithSimParams,
ZiplineTestCase,
)
Expand Down Expand Up @@ -1284,6 +1286,151 @@ def test_history_close_minute_adjusted_volume_roll(self):
"Should remain FOH16 on next session.")


class RollFinderTestCase(WithBcolzFutureDailyBarReader, ZiplineTestCase):

START_DATE = pd.Timestamp('2017-01-03', tz='UTC')
END_DATE = pd.Timestamp('2017-03-17', 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):
two_days = 2 * cls.trading_calendar.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 = cls.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': 'CME',
},
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': 'CME',
},
1002: {
'symbol': 'CLH17',
'root_symbol': 'CL',
'start_date': cls.START_DATE,
'end_date': cls.third_end_date,
'auto_close_date': cls.third_end_date - two_days,
'exchange': 'CME',
},
},
orient='index',
)

@classmethod
def make_future_daily_bar_data(cls):
"""
Volume data should look like this:
CLF17 CLG17 CLH17
2017-01-03 2000 1000 5
2017-01-04 2000 1000 5
...
2017-01-16 2000 1000 5
2017-01-17 2000__ 1000 5
ACD --> 2017-01-18 2000 `--> 1000 5
2017-01-19 2000 1000 5
2017-01-20 2000 1000 5
2017-01-23 0 1000 5
...
2017-02-09 0 1000 5
2017-02-10 0 1000__ 5000
2017-02-13 0 1000 `--> 5000
2017-02-14 0 1000 5000
ACD --> 2017-02-15 0 1000 5000
2017-02-16 0 1000 5000
2017-02-17 0 1000 5000
2017-02-20 0 0 5000
...
2017-03-16 0 0 5000
2017-03-17 0 0 5000
The first roll occurs because we reach the auto close date of CLF17.
The second roll occurs because the volume of CLH17 overtakes CLG17.
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

def test_volume_roll(self):
rolls = self.volume_roll_finder.get_rolls(
root_symbol='CL',
start=self.START_DATE + self.trading_calendar.day,
end=self.END_DATE,
offset=0,
)
self.assertEqual(
rolls,
[
(1000, pd.Timestamp('2017-01-18', 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)])


class OrderedContractsTestCase(WithAssetFinder,
ZiplineTestCase):

Expand Down
24 changes: 21 additions & 3 deletions zipline/assets/roll_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ def get_rolls(self, root_symbol, start, end, offset):
tc.minute_to_session_label(end))
freq = sessions.freq
if first == front:
# This is a bit tricky to grasp. Once we have the active contract
# on the given end date, we want to start walking backwards towards
# the start date and checking for rolls. For this, we treat the
# previous month's contract as the 'first' contract, and the
# contract we just found to be active as the 'back'. As we walk
# towards the start date, if the 'back' is no longer active, we add
# that date as a roll.
curr = first_contract << 1
else:
curr = first_contract << 2
Expand Down Expand Up @@ -169,15 +176,26 @@ def _active_contract(self, oc, front, back, dt):
means that for every date after 'a', `data.current(cf, 'contract')`
should return the 'G' contract.
"""
front_contract = oc.sid_to_contract[front].contract
back_contract = oc.sid_to_contract[back].contract

# If the front contract has reached its auto close date, the back
# contract must be the active one, so return it immediately. Similarly,
# in the rare case that the back contract has not even started yet,
# short circuit here and return the front contract.
if dt >= front_contract.auto_close_date:
return back
elif dt < back_contract.start_date:
return front

tc = self.trading_calendar
trading_day = tc.day
prev = dt - trading_day
get_value = self.session_reader.get_value

front_vol = get_value(front, prev, 'volume')
back_vol = get_value(back, prev, 'volume')
front_contract = oc.sid_to_contract[front].contract

if dt >= front_contract.auto_close_date or back_vol > front_vol:
if back_vol > front_vol:
return back

gap_start = \
Expand Down
4 changes: 3 additions & 1 deletion zipline/testing/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,8 @@ class level fixtures.
# options are: 'warn', 'raise', 'ignore'
BCOLZ_FUTURE_DAILY_BAR_INVALID_DATA_BEHAVIOR = 'warn'

BCOLZ_FUTURE_DAILY_BAR_WRITE_METHOD_NAME = 'write'

@classmethod
def make_bcolz_future_daily_bar_rootdir_path(cls):
return cls.tmpdir.makedir(cls.BCOLZ_FUTURE_DAILY_BAR_PATH)
Expand All @@ -1073,7 +1075,7 @@ def init_class_fixtures(cls):
trading_calendar = cls.trading_calendars[Future]
cls.future_bcolz_daily_bar_ctable = t = getattr(
BcolzDailyBarWriter(p, trading_calendar, days[0], days[-1]),
cls._write_method_name,
cls.BCOLZ_FUTURE_DAILY_BAR_WRITE_METHOD_NAME,
)(
cls.make_future_daily_bar_data(),
invalid_data_behavior=(
Expand Down

0 comments on commit 75fc0fc

Please sign in to comment.