From 6f1d4b4a5f080ee3fe2eb0a52ee2197a5342339b Mon Sep 17 00:00:00 2001 From: dmichalowicz Date: Thu, 6 Apr 2017 17:46:56 -0400 Subject: [PATCH] BUG: OrderedContracts chain could sometimes terminate on first contract --- tests/test_assets.py | 1 + tests/test_continuous_futures.py | 42 ++++++++++++++++++++++++--- zipline/assets/continuous_futures.pyx | 12 ++++++-- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/tests/test_assets.py b/tests/test_assets.py index a1249426..3a194956 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -139,6 +139,7 @@ def build_lookup_generic_cases(asset_finder_type): 'root_symbol': 'FO', 'start_date': unique_start.value, 'end_date': unique_end.value, + 'auto_close_date': unique_end.value, 'exchange': 'FUT', }, ], diff --git a/tests/test_continuous_futures.py b/tests/test_continuous_futures.py index 3559f43e..91c4eeb7 100644 --- a/tests/test_continuous_futures.py +++ b/tests/test_continuous_futures.py @@ -1329,9 +1329,9 @@ class OrderedContractsTestCase(WithAssetFinder, @classmethod def make_root_symbols_info(self): return pd.DataFrame({ - 'root_symbol': ['FO', 'BA'], - 'root_symbol_id': [1, 2], - 'exchange': ['CME', 'CME']}) + 'root_symbol': ['FO', 'BA', 'BZ'], + 'root_symbol_id': [1, 2, 3], + 'exchange': ['CME', 'CME', 'CME']}) @classmethod def make_futures_info(self): @@ -1372,8 +1372,32 @@ class OrderedContractsTestCase(WithAssetFinder, 'multiplier': [1000.0] * 3, 'exchange': ['CME'] * 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. + bz_frame = DataFrame({ + 'root_symbol': ['BZ'] * 3, + 'asset_name': ['Baz'] * 3, + 'symbol': ['BZF16', 'BZG16', 'BZH16'], + 'sid': range(8, 11), + 'start_date': pd.date_range('2015-01-02', periods=3, tz='UTC'), + 'end_date': pd.date_range( + '2015-01-15', periods=3, freq='M', tz='UTC', + ), + 'notice_date': pd.date_range( + '2014-12-31', periods=3, freq='M', tz='UTC', + ), + 'expiration_date': pd.date_range( + '2015-01-15', periods=3, freq='M', tz='UTC', + ), + 'auto_close_date': pd.date_range( + '2014-12-29', periods=3, freq='M', tz='UTC', + ), + 'tick_size': [0.001] * 3, + 'multiplier': [1000.0] * 3, + 'exchange': ['CME'] * 3, + }) - return pd.concat([fo_frame, ba_frame]) + return pd.concat([fo_frame, ba_frame, bz_frame]) def test_contract_at_offset(self): contract_sids = array([1, 2, 3, 4], dtype=int64) @@ -1471,6 +1495,16 @@ class OrderedContractsTestCase(WithAssetFinder, "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], 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) + class NoPrefetchContinuousFuturesTestCase(ContinuousFuturesTestCase): DATA_PORTAL_MINUTE_HISTORY_PREFETCH = 0 diff --git a/zipline/assets/continuous_futures.pyx b/zipline/assets/continuous_futures.pyx index de95eab5..026e329a 100644 --- a/zipline/assets/continuous_futures.pyx +++ b/zipline/assets/continuous_futures.pyx @@ -333,6 +333,12 @@ cdef class OrderedContracts(object): while contracts: contract = contracts.popleft() + # It is possible that the first contract in our list has a start + # date on or after its auto close date. In that case the contract + # is not tradable, so do not include it in the chain. + if prev is None and contract.start_date >= contract.auto_close_date: + continue + # Prevent contract chains with gaps between auto close and start of # next contract. # This is in lieu of more explicit support for @@ -345,7 +351,7 @@ cdef class OrderedContracts(object): self._start_date = min(contract.start_date.value, self._start_date) self._end_date = max(contract.end_date.value, self._end_date) - + curr = ContractNode(contract) self.sid_to_contract[contract.sid] = curr if self._head_contract is None: @@ -355,7 +361,7 @@ cdef class OrderedContracts(object): curr.prev = prev prev.next = curr prev = curr - + cpdef long_t contract_before_auto_close(self, long_t dt_value): """ Get the contract with next upcoming auto close date. @@ -393,7 +399,7 @@ cdef class OrderedContracts(object): if curr.contract.start_date.value <= dt_value: contracts.append(curr.contract.sid) curr = curr.next - + return array(contracts, dtype='int64') property start_date: