From 40605e3c5dab66482f02e6d699d250016486902f Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Fri, 18 Nov 2016 16:38:05 -0500 Subject: [PATCH] BUG: Fix bounds errors in roll finder. Fix common error condition which was triggered whenever the session at the end of the prefetched history window was a session where the back contract was active. When the back contract was the active contract, the next contract for consideration was the front contract at the end of the window, which definitionally always has an autoclose after the end of the window. Instead, just start seeking backwards from the end of the window. Also prevent lookahead bias in volume rolls, which was caused by the using the volume for a session to determine whether that session had rolled. Information that would not have been available at the beginning of the session. This change makes the volume rolls overly conservative, and may be improved by looking at vectors of the preceding volume and making the roll off of momentum. --- tests/test_continuous_futures.py | 40 +++++++++++++++++--------------- zipline/assets/roll_finder.py | 40 ++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/tests/test_continuous_futures.py b/tests/test_continuous_futures.py index a63778b6..fe3aaf8c 100644 --- a/tests/test_continuous_futures.py +++ b/tests/test_continuous_futures.py @@ -176,18 +176,20 @@ class ContinuousFuturesTestCase(WithCreateBarData, 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 checks for exacly - # 0 do not work, since there may be stragglers after a roll. + # 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] - sessions.freq + non_primary_end = sid_to_vol_stop_session[j] m_close = tc.open_and_close_for_session(non_primary_end)[1] - loc = dts.searchsorted(m_close) - # Add some volume before a roll, since a contracted may be - # entered earlier than when it is the primary. - df.volume.values[:loc] = 2000 + 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 yield i, df def test_create_continuous_future(self): @@ -311,14 +313,14 @@ class ContinuousFuturesTestCase(WithCreateBarData, lambda: pd.Timestamp('2016-01-26', tz='UTC')) contract = bar_data.current(cf_primary, 'contract') - self.assertEqual(contract.symbol, 'FOG16') + self.assertEqual(contract.symbol, 'FOF16') bar_data = self.create_bardata( - lambda: pd.Timestamp('2016-01-26', tz='UTC')) + 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 remains ' + 'Auto close at beginning of session. FOG16 is now ' 'the current contract.') bar_data = self.create_bardata( @@ -599,12 +601,12 @@ def record_current_contract(algo, data): # Volume cuts out for FOF16 on 2016-01-25 self.assertEqual(window.loc['2016-01-26', cf], - 1, - "Should be FOG16 at beginning of window.") + 0, + "Should be FOF16 at beginning of window.") self.assertEqual(window.loc['2016-01-27', cf], 1, - "Should have remained FOG16.") + "Should have rolled to FOG16.") self.assertEqual(window.loc['2016-02-25', cf], 1, @@ -630,24 +632,24 @@ def record_current_contract(algo, data): self.assertEqual(window.loc['2016-02-26', cf], 2, - "Should be FOH16 on session with roll.") + "Should be FOH16 on roll session.") self.assertEqual(window.loc['2016-02-29', cf], 2, - "Should be FOH16 on session after roll.") + "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], - 3, - "Should be FOJ16 on session where the volume of " - "FOH16 cuts out.") + 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 remained FOJ16.") + "Should have rolled to FOJ16.") self.assertEqual(window.loc['2016-03-28', cf], 3, diff --git a/zipline/assets/roll_finder.py b/zipline/assets/roll_finder.py index c4633b72..679ce9c5 100644 --- a/zipline/assets/roll_finder.py +++ b/zipline/assets/roll_finder.py @@ -96,23 +96,21 @@ class RollFinder(with_metaclass(ABCMeta, object)): i -= 1 else: i -= 2 - auto_close_date = Timestamp(oc.auto_close_dates[i], tz='UTC') - while auto_close_date > start and i > -1: - session_loc = sessions.searchsorted(auto_close_date) + curr = sessions[-1] + while curr > start and i > -1: + session_loc = sessions.searchsorted(curr) front = oc.contract_sids[i] back = oc.contract_sids[i + 1] - while session_loc > -1: + while session_loc > 0: session = sessions[session_loc] - if back != self._active_contract(oc, front, back, session): + prev = sessions[session_loc - 1] + if back != self._active_contract(oc, front, back, prev): + rolls.insert(0, (oc.contract_sids[i + offset], session)) break session_loc -= 1 - roll_session = sessions[session_loc + 1] - if roll_session > start: - rolls.insert(0, (oc.contract_sids[i + offset], - roll_session)) i -= 1 - auto_close_date = Timestamp(oc.auto_close_dates[i], - tz='UTC') + curr = Timestamp(oc.auto_close_dates[i], + tz='UTC') return rolls @@ -131,8 +129,8 @@ class CalendarRollFinder(RollFinder): if sid == front: break auto_close_date = Timestamp(oc.auto_close_dates[i], tz='UTC') - before_auto_close = dt < auto_close_date - return front if before_auto_close else back + auto_closed = dt >= auto_close_date + return back if auto_closed else front class VolumeRollFinder(RollFinder): @@ -149,7 +147,15 @@ class VolumeRollFinder(RollFinder): self.session_reader = session_reader def _active_contract(self, oc, front, back, dt): - # FIXME: Possible vector for look ahead bias. - front_vol = self.session_reader.get_value(front, dt, 'volume') - back_vol = self.session_reader.get_value(back, dt, 'volume') - return back if back_vol > front_vol else front + prev = dt - self.trading_calendar.day + front_vol = self.session_reader.get_value(front, prev, 'volume') + back_vol = self.session_reader.get_value(back, prev, 'volume') + if back_vol > front_vol: + return back + else: + for i, sid in enumerate(oc.contract_sids): + if sid == front: + break + auto_close_date = Timestamp(oc.auto_close_dates[i], tz='UTC') + auto_closed = dt >= auto_close_date + return back if auto_closed else front