From a19ec84a1df1410f6704adbff5d0cec48dc5563a Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Mon, 5 Dec 2016 17:10:17 -0500 Subject: [PATCH] BUG: Allow rolls to skip over contracts. For futures that behave like GC, use the latest roll as the back contract when walking backwards over the window, so that when the front contract is skipped because it never has more volume between its auto close date and the previous auto close date, the back contract which did have volume is still used when making comparisons to construct the chain. --- tests/test_continuous_futures.py | 78 ++++++++++++++++++++++++++++++-- zipline/assets/roll_finder.py | 6 ++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/tests/test_continuous_futures.py b/tests/test_continuous_futures.py index 3b3eb168..68389afb 100644 --- a/tests/test_continuous_futures.py +++ b/tests/test_continuous_futures.py @@ -66,9 +66,9 @@ class ContinuousFuturesTestCase(WithCreateBarData, @classmethod def make_root_symbols_info(self): return pd.DataFrame({ - 'root_symbol': ['FO', 'BA', 'BZ'], - 'root_symbol_id': [1, 2, 3], - 'exchange': ['CME', 'CME', 'CME']}) + 'root_symbol': ['FO', 'BA', 'BZ', 'MA'], + 'root_symbol_id': [1, 2, 3, 4], + 'exchange': ['CME', 'CME', 'CME', 'CME']}) @classmethod def make_futures_info(self): @@ -179,7 +179,33 @@ class ContinuousFuturesTestCase(WithCreateBarData, 'exchange': ['CME'] * 3, }) - return pd.concat([fo_frame, ba_frame, bz_frame]) + # 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': ['CME'] * 3, + }) + + return pd.concat([fo_frame, ba_frame, bz_frame, ma_frame]) @classmethod def make_future_minute_bar_data(cls): @@ -239,7 +265,7 @@ class ContinuousFuturesTestCase(WithCreateBarData, 3: Timestamp('2016-04-20', tz='UTC'), 6: Timestamp('2016-01-27', tz='UTC'), } - for i in range(7): + for i in range(17): df = base_df.copy() df += i * 10000 if i in sid_to_vol_stop_session: @@ -260,6 +286,8 @@ class ContinuousFuturesTestCase(WithCreateBarData, # 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 yield i, df def test_create_continuous_future(self): @@ -852,6 +880,46 @@ def record_current_contract(algo, data): 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') + window = self.data_portal.get_history_window( + [cf.sid], Timestamp('2016-03-06', tz='UTC'), 30, '1d', 'close') + + 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') + + 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') diff --git a/zipline/assets/roll_finder.py b/zipline/assets/roll_finder.py index 808bd0ec..b2b2d29a 100644 --- a/zipline/assets/roll_finder.py +++ b/zipline/assets/roll_finder.py @@ -98,9 +98,13 @@ class RollFinder(with_metaclass(ABCMeta, object)): while session > start and curr is not None: front = curr.contract.sid - back = curr.next.contract.sid + back = rolls[0][0] + prev_c = curr.prev while session > start: prev = session - freq + if prev_c is not None: + if prev < prev_c.contract.auto_close_date: + break if back != self._active_contract(oc, front, back, prev): rolls.insert(0, ((curr >> offset).contract.sid, session)) break