diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index dc89c592..3735d2ab 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -726,9 +726,51 @@ def log_nyse_close(context, data): with self.assertRaises(TypeError): algo.future_symbol({'foo': 'bar'}) + def test_future_chain_offset(self): + # November 2006 December 2006 + # Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa + # 1 2 3 4 1 2 + # 5 6 7 8 9 10 11 3 4 5 6 7 8 9 + # 12 13 14 15 16 17 18 10 11 12 13 14 15 16 + # 19 20 21 22 23 24 25 17 18 19 20 21 22 23 + # 26 27 28 29 30 24 25 26 27 28 29 30 + # 31 + + algo = TradingAlgorithm(env=self.env) + algo.datetime = pd.Timestamp('2006-12-01', tz='UTC') + + self.assertEqual( + algo.future_chain('CL', offset=1).as_of_date, + pd.Timestamp("2006-12-04", tz='UTC') + ) + + self.assertEqual( + algo.future_chain("CL", offset=5).as_of_date, + pd.Timestamp("2006-12-08", tz='UTC') + ) + + self.assertEqual( + algo.future_chain("CL", offset=-10).as_of_date, + pd.Timestamp("2006-11-16", tz='UTC') + ) + + # September 2016 + # Su Mo Tu We Th Fr Sa + # 1 2 3 + # 4 5 6 7 8 9 10 + # 11 12 13 14 15 16 17 + # 18 19 20 21 22 23 24 + # 25 26 27 28 29 30 + self.assertEqual( + algo.future_chain( + "CL", + as_of_date=pd.Timestamp("2016-08-31", tz='UTC'), + offset=10 + ).as_of_date, + pd.Timestamp("2016-09-15", tz='UTC') + ) + def test_future_chain(self): - """ Tests the future_chain API function. - """ algo = TradingAlgorithm(env=self.env) algo.datetime = pd.Timestamp('2006-12-01', tz='UTC') diff --git a/tests/test_assets.py b/tests/test_assets.py index 50842a06..cf52be8d 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -17,7 +17,7 @@ Tests for the zipline.assets package """ from contextlib import contextmanager -from datetime import datetime, timedelta +from datetime import timedelta from functools import partial import pickle import sys @@ -1169,56 +1169,65 @@ class TestFutureChain(WithAssetFinder, ZiplineTestCase): } ]) + def _get_future_chain(self, date_str, symbol): + dt = pd.Timestamp(date_str, tz='UTC') + + return FutureChain( + symbol, + dt, + self.asset_finder.lookup_future_chain(symbol, dt) + ) + def test_len(self): """ Test the __len__ method of FutureChain. """ # Sids 0, 1, & 2 have started, 3 has not yet started, but all are in # the chain - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') + cl = self._get_future_chain('2005-12-01', 'CL') self.assertEqual(len(cl), 4) # Sid 0 is still valid on its notice date. - cl = FutureChain(self.asset_finder, lambda: '2005-12-20', 'CL') + cl = self._get_future_chain('2005-12-20', 'CL') self.assertEqual(len(cl), 4) # Sid 0 is now invalid, leaving Sids 1 & 2 valid (and 3 not started). - cl = FutureChain(self.asset_finder, lambda: '2005-12-21', 'CL') + cl = self._get_future_chain('2005-12-21', 'CL') self.assertEqual(len(cl), 3) # Sid 3 has started, so 1, 2, & 3 are now valid. - cl = FutureChain(self.asset_finder, lambda: '2006-02-01', 'CL') + cl = self._get_future_chain('2006-02-01', 'CL') self.assertEqual(len(cl), 3) # All contracts are no longer valid. - cl = FutureChain(self.asset_finder, lambda: '2006-09-21', 'CL') + cl = self._get_future_chain('2006-09-21', 'CL') self.assertEqual(len(cl), 0) def test_getitem(self): """ Test the __getitem__ method of FutureChain. """ - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') + cl = self._get_future_chain('2005-12-01', 'CL') self.assertEqual(cl[0], 0) self.assertEqual(cl[1], 1) self.assertEqual(cl[2], 2) - cl = FutureChain(self.asset_finder, lambda: '2005-12-20', 'CL') + cl = self._get_future_chain('2005-12-20', 'CL') self.assertEqual(cl[0], 0) - cl = FutureChain(self.asset_finder, lambda: '2005-12-21', 'CL') + cl = self._get_future_chain('2005-12-21', 'CL') self.assertEqual(cl[0], 1) - cl = FutureChain(self.asset_finder, lambda: '2006-02-01', 'CL') + cl = self._get_future_chain('2006-02-01', 'CL') self.assertEqual(cl[-1], 3) def test_iter(self): """ Test the __iter__ method of FutureChain. """ - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') + cl = self._get_future_chain('2005-12-01', 'CL') for i, contract in enumerate(cl): self.assertEqual(contract, i) # First contract is now invalid, so sids will be offset by one - cl = FutureChain(self.asset_finder, lambda: '2005-12-21', 'CL') + cl = self._get_future_chain('2005-12-21', 'CL') for i, contract in enumerate(cl): self.assertEqual(contract, i + 1) @@ -1227,116 +1236,36 @@ class TestFutureChain(WithAssetFinder, ZiplineTestCase): as expected. """ # Make sure this successfully gets the chain for CL. - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') + cl = self._get_future_chain('2005-12-01', 'CL') self.assertEqual(cl.root_symbol, 'CL') # These root symbols don't exist, so RootSymbolNotFound should # be raised immediately. with self.assertRaises(RootSymbolNotFound): - FutureChain(self.asset_finder, lambda: '2005-12-01', 'CLZ') + self._get_future_chain('2005-12-01', 'CLZ') with self.assertRaises(RootSymbolNotFound): - FutureChain(self.asset_finder, lambda: '2005-12-01', '') + self._get_future_chain('2005-12-01', '') def test_repr(self): """ Test the __repr__ method of FutureChain. """ - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') - cl_feb = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL', - as_of_date=pd.Timestamp('2006-02-01', tz='UTC')) + cl = self._get_future_chain('2005-12-01', 'CL') - # The default chain should not include the as of date. - self.assertEqual(repr(cl), "FutureChain(root_symbol='CL')") - - # An explicit as of date should show up in the repr. self.assertEqual( - repr(cl_feb), - ("FutureChain(root_symbol='CL', " - "as_of_date='2006-02-01 00:00:00+00:00')") + repr(cl), + "FutureChain('CL', '2005-12-01')" ) - def test_as_of(self): - """ Test the as_of method of FutureChain. - """ - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') + def test_contracts_returns_a_copy(self): + cl = self._get_future_chain('2005-12-01', 'CL') + self.assertEqual(len(cl), 4) - # Test that the as_of_date is set correctly to the future - feb = pd.Timestamp('2006-02-01', tz='UTC') - cl_feb = cl.as_of(feb) - self.assertEqual( - cl_feb.as_of_date, - pd.Timestamp(feb, tz='UTC') - ) + contracts = cl.contracts + contracts.pop(0) - # Test that the as_of_date is set correctly to the past, with - # args of str, datetime.datetime, and pd.Timestamp. - feb_prev = pd.Timestamp('2005-02-01', tz='UTC') - cl_feb_prev = cl.as_of(feb_prev) - self.assertEqual( - cl_feb_prev.as_of_date, - pd.Timestamp(feb_prev, tz='UTC') - ) - - feb_prev = pd.Timestamp(datetime(year=2005, month=2, day=1), tz='UTC') - cl_feb_prev = cl.as_of(feb_prev) - self.assertEqual( - cl_feb_prev.as_of_date, - pd.Timestamp(feb_prev, tz='UTC') - ) - - feb_prev = pd.Timestamp('2005-02-01', tz='UTC') - cl_feb_prev = cl.as_of(feb_prev) - self.assertEqual( - cl_feb_prev.as_of_date, - pd.Timestamp(feb_prev, tz='UTC') - ) - - # Test that the as_of() method works with str args - feb_str = '2006-02-01' - cl_feb = cl.as_of(feb_str) - self.assertEqual( - cl_feb.as_of_date, - pd.Timestamp(feb, tz='UTC') - ) - - # The chain as of the current dt should always be the same as - # the defualt chain. - self.assertEqual(cl[0], cl.as_of(pd.Timestamp('2005-12-01'))[0]) - - def test_offset(self): - """ Test the offset method of FutureChain. - """ - cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL') - - # Test that an offset forward sets as_of_date as expected - self.assertEqual( - cl.offset('3 days').as_of_date, - cl.as_of_date + pd.Timedelta(days=3) - ) - - # Test that an offset backward sets as_of_date as expected, with - # time delta given as str, datetime.timedelta, and pd.Timedelta. - self.assertEqual( - cl.offset('-1000 days').as_of_date, - cl.as_of_date + pd.Timedelta(days=-1000) - ) - self.assertEqual( - cl.offset(timedelta(days=-1000)).as_of_date, - cl.as_of_date + pd.Timedelta(days=-1000) - ) - self.assertEqual( - cl.offset(pd.Timedelta('-1000 days')).as_of_date, - cl.as_of_date + pd.Timedelta(days=-1000) - ) - - # An offset of zero should give the original chain. - self.assertEqual(cl[0], cl.offset(0)[0]) - self.assertEqual(cl[0], cl.offset("0 days")[0]) - - # A string that doesn't represent a time delta should raise a - # ValueError. - with self.assertRaises(ValueError): - cl.offset("blah") + self.assertEqual(len(contracts), 3) + self.assertEqual(len(cl), 4) def test_cme_code_to_month(self): codes = { diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 0eab36b3..0f134b1c 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -1242,17 +1242,20 @@ class TradingAlgorithm(object): @api_method @preprocess(root_symbol=ensure_upper_case) - def future_chain(self, root_symbol, as_of_date=None): - """Look up a future chain with the specified parameters. + def future_chain(self, root_symbol, as_of_date=None, offset=0): + """ + Look up a future chain. Parameters ---------- root_symbol : str The root symbol of a future chain. as_of_date : datetime.datetime or pandas.Timestamp or str, optional - Date at which the chain determination is rooted. I.e. the - existing contract whose notice date is first after this date is - the primary contract, etc. + Date at which the chain determination is rooted. If this date is + not passed in, the current simulation session (not minute) is used. + offset: int + Number of sessions to shift `as_of_date`. Positive values shift + forward in time. Negative values shift backward in time. Returns ------- @@ -1268,13 +1271,35 @@ class TradingAlgorithm(object): try: as_of_date = pd.Timestamp(as_of_date, tz='UTC') except ValueError: - raise UnsupportedDatetimeFormat(input=as_of_date, - method='future_chain') + raise UnsupportedDatetimeFormat( + input=as_of_date, + method='future_chain' + ) + else: + as_of_date = self.trading_calendar.minute_to_session_label( + self.get_datetime() + ) + + if offset != 0: + # move as_of_date by offset sessions + session_window = self.trading_calendar.sessions_window( + as_of_date, offset + ) + + if offset > 0: + as_of_date = session_window[-1] + else: + as_of_date = session_window[0] + + chain_of_contracts = self.asset_finder.lookup_future_chain( + root_symbol, + as_of_date + ) + return FutureChain( - asset_finder=self.asset_finder, - get_datetime=self.get_datetime, root_symbol=root_symbol, - as_of_date=as_of_date + as_of_date=as_of_date, + contracts=chain_of_contracts ) def _calculate_order_value_amount(self, asset, value): diff --git a/zipline/assets/futures.py b/zipline/assets/futures.py index 12dcae52..9dcaff63 100644 --- a/zipline/assets/futures.py +++ b/zipline/assets/futures.py @@ -1,5 +1,5 @@ # -# Copyright 2015 Quantopian, Inc. +# 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. @@ -13,173 +13,86 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pandas import Timestamp, Timedelta -from pandas.tseries.tools import normalize_date +from pandas import Timestamp + +from zipline.utils.input_validation import expect_types class FutureChain(object): - """ Allows users to look up future contracts. + """ + Allows users to look up future contracts. Parameters ---------- - asset_finder : AssetFinder - An AssetFinder for future contract lookups, in particular the - AssetFinder of the TradingAlgorithm instance. - get_datetime : function - A function that returns the simulation datetime, in particular - the get_datetime method of the TradingAlgorithm instance. root_symbol : str The root symbol of a future chain. - as_of_date : pandas.Timestamp, optional + as_of_date : pandas.Timestamp Date at which the chain determination is rooted. I.e. the existing contract whose notice date is first after this date is - the primary contract, etc. If not provided, the current - simulation date is used as the as_of_date. + the primary contract, etc. + chain: list + List of assets that represent the chain of contracts for the given + root symbol at the given as_of_date. Attributes ---------- root_symbol : str The root symbol of the future chain. - as_of_date + as_of_date: Timestamp The current as-of date of this future chain. - - Methods - ------- - as_of(dt) - offset(time_delta) - - Raises - ------ - RootSymbolNotFound - Raised when the FutureChain is initialized with a root symbol for which - a future chain could not be found. """ - def __init__(self, asset_finder, get_datetime, root_symbol, - as_of_date=None): - self.root_symbol = root_symbol - - # Reference to the algo's AssetFinder for contract lookups - self._asset_finder = asset_finder - # Reference to the algo's get_datetime to know the current dt - self._algorithm_get_datetime = get_datetime - - # If an as_of_date is provided, self._as_of_date uses that - # value, otherwise None. This attribute backs the as_of_date property. - if as_of_date: - self._as_of_date = normalize_date(as_of_date) - else: - self._as_of_date = None - - # Attribute to cache the most up-to-date chain, and the dt when it was - # last updated. - self._current_chain = [] - self._last_updated = None - - # Get the initial chain, since self._last_updated is None. - self._maybe_update_current_chain() + @expect_types(root_symbol=str, as_of_date=Timestamp) + def __init__(self, root_symbol, as_of_date, contracts): + self._root_symbol = root_symbol + self._as_of_date = as_of_date + self._contracts = contracts def __repr__(self): - # NOTE: The string returned cannot be used to instantiate this - # exact FutureChain, since we don't want to display the asset - # finder and get_datetime function to the user. - if self._as_of_date: - return "FutureChain(root_symbol='%s', as_of_date='%s')" % ( - self.root_symbol, self.as_of_date) - else: - return "FutureChain(root_symbol='%s')" % self.root_symbol + return "FutureChain('%s', '%s')" % ( + self.root_symbol, self.as_of_date.strftime('%Y-%m-%d')) - def _get_datetime(self): + @property + def root_symbol(self): """ - Returns the normalized simulation datetime. + The root symbol for this future chain. Returns ------- - pandas.Timestamp - The normalized datetime of FutureChain's TradingAlgorithm. + root_symbol: str + The root symbol for this chain. """ - return normalize_date( - Timestamp(self._algorithm_get_datetime(), tz='UTC') - ) + return self._root_symbol @property def as_of_date(self): """ - The current as-of date of this future chain. + The as-of date of this future chain. Returns ------- - pandas.Timestamp - The user-provided as_of_date if given, otherwise the - current datetime of the simulation. + as_of_date: pd.Timestamp + The as_of date for this chain. """ - if self._as_of_date is not None: - return self._as_of_date - else: - return self._get_datetime() + return self._as_of_date - def _maybe_update_current_chain(self): - """ Updates the current chain if it's out of date, then returns - it. - - Returns - ------- - list - The up-to-date current chain, a list of Future objects. + @property + def contracts(self): """ - if (self._last_updated is None)\ - or (self._last_updated != self.as_of_date): - self._current_chain = self._asset_finder.lookup_future_chain( - self.root_symbol, - self.as_of_date - ) - self._last_updated = self.as_of_date - - return self._current_chain + Returns + ------- + contracts: list + The contracts wrapped by this chain. + """ + return list(self._contracts) def __getitem__(self, key): - return self._maybe_update_current_chain()[key] + return self._contracts[key] def __len__(self): - return len(self._maybe_update_current_chain()) + return len(self._contracts) def __iter__(self): - return iter(self._maybe_update_current_chain()) - - def as_of(self, dt): - """ Get the future chain for this root symbol as of a specific date. - - Parameters - ---------- - dt : datetime.datetime or pandas.Timestamp or str, optional - The as_of_date for the new chain. - - Returns - ------- - FutureChain - - """ - return FutureChain( - asset_finder=self._asset_finder, - get_datetime=self._algorithm_get_datetime, - root_symbol=self.root_symbol, - as_of_date=Timestamp(dt, tz='UTC'), - ) - - def offset(self, time_delta): - """ Get the future chain for this root symbol with a given - offset from the current as_of_date. - - Parameters - ---------- - time_delta : datetime.timedelta or pandas.Timedelta or str - The offset from the current as_of_date for the new chain. - - Returns - ------- - FutureChain - - """ - return self.as_of(self.as_of_date + Timedelta(time_delta)) + return iter(self._contracts) # http://www.cmegroup.com/product-codes-listing/month-codes.html