diff --git a/tests/test_assets.py b/tests/test_assets.py index dbe9eef3..a1249426 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -161,7 +161,10 @@ def build_lookup_generic_cases(asset_finder_type): ] fof14 = finder.retrieve_asset(fof14_sid) cf = finder.create_continuous_future( - root_symbol=fof14.root_symbol, offset=0, roll_style='volume', + root_symbol=fof14.root_symbol, + offset=0, + roll_style='volume', + adjustment=None, ) dupe_0_start = dupe_0.start_date diff --git a/tests/test_continuous_futures.py b/tests/test_continuous_futures.py index 3530f8da..28ddaa38 100644 --- a/tests/test_continuous_futures.py +++ b/tests/test_continuous_futures.py @@ -348,7 +348,9 @@ class ContinuousFuturesTestCase(WithCreateBarData, close date. See `VolumeRollFinder._active_contract` for a full explanation and example. """ - cf = self.asset_finder.create_continuous_future('DF', 0, 'volume') + cf = self.asset_finder.create_continuous_future( + 'DF', 0, 'volume', None, + ) sessions = self.trading_calendar.sessions_in_range( '2016-02-09', '2016-02-17', @@ -385,7 +387,7 @@ class ContinuousFuturesTestCase(WithCreateBarData, def test_create_continuous_future(self): cf_primary = self.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) self.assertEqual(cf_primary.root_symbol, 'FO') self.assertEqual(cf_primary.offset, 0) @@ -401,7 +403,7 @@ class ContinuousFuturesTestCase(WithCreateBarData, self.assertEqual(retrieved_primary, cf_primary) cf_secondary = self.asset_finder.create_continuous_future( - 'FO', 1, 'calendar') + 'FO', 1, 'calendar', None) self.assertEqual(cf_secondary.root_symbol, 'FO') self.assertEqual(cf_secondary.offset, 1) @@ -421,11 +423,12 @@ class ContinuousFuturesTestCase(WithCreateBarData, # Assert that the proper exception is raised if the given root symbol # does not exist. with self.assertRaises(SymbolNotFound): - self.asset_finder.create_continuous_future('NO', 0, 'calendar') + self.asset_finder.create_continuous_future( + 'NO', 0, 'calendar', None) def test_current_contract(self): cf_primary = self.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) bar_data = self.create_bardata( lambda: pd.Timestamp('2016-01-26', tz='UTC')) contract = bar_data.current(cf_primary, 'contract') @@ -442,7 +445,7 @@ class ContinuousFuturesTestCase(WithCreateBarData, def test_get_value_contract_daily(self): cf_primary = self.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) contract = self.data_portal.get_spot_value( cf_primary, @@ -466,7 +469,7 @@ class ContinuousFuturesTestCase(WithCreateBarData, def test_get_value_close_daily(self): cf_primary = self.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) value = self.data_portal.get_spot_value( cf_primary, @@ -504,7 +507,7 @@ class ContinuousFuturesTestCase(WithCreateBarData, def test_current_contract_volume_roll(self): cf_primary = self.asset_finder.create_continuous_future( - 'FO', 0, 'volume') + 'FO', 0, 'volume', None) bar_data = self.create_bardata( lambda: pd.Timestamp('2016-01-26', tz='UTC')) contract = bar_data.current(cf_primary, 'contract') @@ -535,8 +538,8 @@ from zipline.api import ( ) def initialize(algo): - algo.primary_cl = continuous_future('FO', 0, 'calendar') - algo.secondary_cl = continuous_future('FO', 1, 'calendar') + algo.primary_cl = continuous_future('FO', 0, 'calendar', None) + algo.secondary_cl = continuous_future('FO', 1, 'calendar', None) schedule_function(record_current_contract) def record_current_contract(algo, data): @@ -588,8 +591,8 @@ from zipline.api import ( ) def initialize(algo): - algo.primary_cl = continuous_future('FO', 0, 'calendar') - algo.secondary_cl = continuous_future('FO', 1, 'calendar') + algo.primary_cl = continuous_future('FO', 0, 'calendar', None) + algo.secondary_cl = continuous_future('FO', 1, 'calendar', None) schedule_function(record_current_contract) def record_current_contract(algo, data): @@ -681,7 +684,7 @@ def record_current_contract(algo, data): def test_history_sid_session(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) window = self.data_portal.get_history_window( [cf], Timestamp('2016-03-04 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -735,7 +738,7 @@ def record_current_contract(algo, data): def test_history_sid_session_quarter_rolls(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'BA', 0, 'calendar') + 'BA', 0, 'calendar', None) window = self.data_portal.get_history_window( [cf], Timestamp('2016-03-13 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -756,7 +759,7 @@ def record_current_contract(algo, data): def test_history_sid_session_delivery_predicate(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'BZ', 0, 'calendar') + 'BZ', 0, 'calendar', None) window = self.data_portal.get_history_window( [cf], Timestamp('2016-01-11 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -777,7 +780,7 @@ def record_current_contract(algo, data): def test_history_sid_session_secondary(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 1, 'calendar') + 'FO', 1, 'calendar', None) window = self.data_portal.get_history_window( [cf], Timestamp('2016-03-04 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -831,7 +834,7 @@ def record_current_contract(algo, data): def test_history_sid_session_volume_roll(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'volume') + 'FO', 0, 'volume', None) window = self.data_portal.get_history_window( [cf], Timestamp('2016-03-04 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -895,7 +898,7 @@ def record_current_contract(algo, data): def test_history_sid_minute(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) window = self.data_portal.get_history_window( [cf.sid], Timestamp('2016-01-26 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -930,7 +933,7 @@ def record_current_contract(algo, data): def test_history_close_session(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) window = self.data_portal.get_history_window( [cf.sid], Timestamp('2016-03-06', tz='UTC'), 30, '1d', 'close') @@ -980,7 +983,7 @@ def record_current_contract(algo, data): def test_history_close_session_skip_volume(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'MA', 0, 'volume') + 'MA', 0, 'volume', None) window = self.data_portal.get_history_window( [cf.sid], Timestamp('2016-03-06', tz='UTC'), 30, '1d', 'close') @@ -1020,11 +1023,11 @@ def record_current_contract(algo, data): def test_history_close_session_adjusted(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) cf_mul = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar').adj('mul') + 'FO', 0, 'calendar', 'mul') cf_add = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar').adj('add') + 'FO', 0, 'calendar', 'add') window = self.data_portal.get_history_window( [cf, cf_mul, cf_add], Timestamp('2016-03-06', tz='UTC'), 30, '1d', 'close') @@ -1148,7 +1151,7 @@ def record_current_contract(algo, data): def test_history_close_minute(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) window = self.data_portal.get_history_window( [cf.sid], Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -1183,11 +1186,11 @@ def record_current_contract(algo, data): def test_history_close_minute_adjusted(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar') + 'FO', 0, 'calendar', None) cf_mul = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar').adj('mul') + 'FO', 0, 'calendar', 'mul') cf_add = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'calendar').adj('add') + 'FO', 0, 'calendar', 'add') window = self.data_portal.get_history_window( [cf, cf_mul, cf_add], Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'), @@ -1247,11 +1250,11 @@ def record_current_contract(algo, data): def test_history_close_minute_adjusted_volume_roll(self): cf = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'volume') + 'FO', 0, 'volume', None) cf_mul = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'volume').adj('mul') + 'FO', 0, 'volume', 'mul') cf_add = self.data_portal.asset_finder.create_continuous_future( - 'FO', 0, 'volume').adj('add') + 'FO', 0, 'volume', 'add') window = self.data_portal.get_history_window( [cf, cf_mul, cf_add], Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'), diff --git a/zipline/algorithm.py b/zipline/algorithm.py index ee324174..b64e7fdc 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -1201,7 +1201,11 @@ class TradingAlgorithm(object): @api_method @preprocess(root_symbol_str=ensure_upper_case) - def continuous_future(self, root_symbol_str, offset, roll): + def continuous_future(self, + root_symbol_str, + offset, + roll, + adjustment='mul'): """Create a specifier for a continuous contract. Parameters @@ -1215,6 +1219,10 @@ class TradingAlgorithm(object): roll_style : str How rolls are determined. + adjustment : str + Method for adjusting lookback prices between rolls. Options are + 'mul', 'add', and None. Defaults to 'mul'. + Returns ------- continuous_future : ContinuousFuture @@ -1224,6 +1232,7 @@ class TradingAlgorithm(object): root_symbol_str, offset, roll, + adjustment, ) @api_method diff --git a/zipline/api.pyi b/zipline/api.pyi index 312990d3..821784d5 100644 --- a/zipline/api.pyi +++ b/zipline/api.pyi @@ -60,7 +60,7 @@ def cancel_order(order_param): The order_id or order object to cancel. """ -def continuous_future(root_symbol_str, offset, roll): +def continuous_future(root_symbol_str, offset, roll, adjustment='mul'): """Create a specifier for a continuous contract. Parameters @@ -74,6 +74,10 @@ def continuous_future(root_symbol_str, offset, roll): roll_style : str How rolls are determined. + adjustment : str + Method for adjusting lookback prices between rolls. Options are + 'mul', 'add', and None. Defaults to 'mul'. + Returns ------- continuous_future : ContinuousFuture diff --git a/zipline/assets/assets.py b/zipline/assets/assets.py index c7bbbecf..3e557119 100644 --- a/zipline/assets/assets.py +++ b/zipline/assets/assets.py @@ -16,6 +16,7 @@ from abc import ABCMeta import array import binascii from collections import deque, namedtuple +from functools import partial from numbers import Integral from operator import itemgetter, attrgetter import struct @@ -54,9 +55,10 @@ from . import ( Asset, Equity, Future, ) from . continuous_futures import ( - OrderedContracts, + ADJUSTMENT_STYLES, + CHAIN_PREDICATES, ContinuousFuture, - CHAIN_PREDICATES + OrderedContracts, ) from .asset_writer import ( check_version_info, @@ -1016,7 +1018,17 @@ class AssetFinder(object): self._ordered_contracts[root_symbol] = oc return oc - def create_continuous_future(self, root_symbol, offset, roll_style): + def create_continuous_future(self, + root_symbol, + offset, + roll_style, + adjustment): + if adjustment not in ADJUSTMENT_STYLES: + raise ValueError( + 'Invalid adjustment style {!r}. Allowed adjustment styles are ' + '{}.'.format(adjustment, list(ADJUSTMENT_STYLES)) + ) + oc = self.get_ordered_contracts(root_symbol) exchange = self._get_root_symbol_exchange(root_symbol) @@ -1029,37 +1041,26 @@ class AssetFinder(object): add_sid = _encode_continuous_future_sid(root_symbol, offset, roll_style, 'add') - mul_cf = ContinuousFuture(mul_sid, - root_symbol, - offset, - roll_style, - oc.start_date, - oc.end_date, - exchange, - 'mul') - add_cf = ContinuousFuture(add_sid, - root_symbol, - offset, - roll_style, - oc.start_date, - oc.end_date, - exchange, - 'add') - cf = ContinuousFuture(sid, - root_symbol, - offset, - roll_style, - oc.start_date, - oc.end_date, - exchange, - adjustment_children={ - 'mul': mul_cf, - 'add': add_cf - }) + + cf_template = partial( + ContinuousFuture, + root_symbol=root_symbol, + offset=offset, + roll_style=roll_style, + start_date=oc.start_date, + end_date=oc.end_date, + exchange=exchange, + ) + + cf = cf_template(sid=sid) + mul_cf = cf_template(sid=mul_sid, adjustment='mul') + add_cf = cf_template(sid=add_sid, adjustment='add') + self._asset_cache[cf.sid] = cf - self._asset_cache[add_cf.sid] = add_cf self._asset_cache[mul_cf.sid] = mul_cf - return cf + self._asset_cache[add_cf.sid] = add_cf + + return {None: cf, 'mul': mul_cf, 'add': add_cf}[adjustment] def _make_sids(tblattr): def _(self): diff --git a/zipline/assets/continuous_futures.pyx b/zipline/assets/continuous_futures.pyx index b5017846..f2021d4d 100644 --- a/zipline/assets/continuous_futures.pyx +++ b/zipline/assets/continuous_futures.pyx @@ -53,6 +53,8 @@ CHAIN_PREDICATES = { 'PA': partial(delivery_predicate, set(['H', 'M', 'U', 'Z'])) } +ADJUSTMENT_STYLES = {'add', 'mul', None} + cdef class ContinuousFuture: """