Skip to content

Commit

Permalink
Merge pull request #1456 from quantopian/future-chain-cleanup
Browse files Browse the repository at this point in the history
ENH: Simplified implementation of FutureChain object (not user-facing API)
  • Loading branch information
jbredeche committed Aug 31, 2016
2 parents 35631f4 + 38ff7e5 commit 0952688
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 244 deletions.
46 changes: 44 additions & 2 deletions tests/test_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,9 +726,51 @@ def test_future_symbol(self):
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')

Expand Down
139 changes: 34 additions & 105 deletions tests/test_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1169,56 +1169,65 @@ def make_futures_info(cls):
}
])

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)

Expand All @@ -1227,116 +1236,36 @@ def test_root_symbols(self):
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'))

# 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')")
)

def test_as_of(self):
""" Test the as_of method of FutureChain.
"""
cl = FutureChain(self.asset_finder, lambda: '2005-12-01', 'CL')

# 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')
)

# 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')
)
cl = self._get_future_chain('2005-12-01', 'CL')

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')
repr(cl),
"FutureChain('CL', '2005-12-01')"
)

# 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)
)
def test_contracts_returns_a_copy(self):
cl = self._get_future_chain('2005-12-01', 'CL')
self.assertEqual(len(cl), 4)

# 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])
contracts = cl.contracts
contracts.pop(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 = {
Expand Down
45 changes: 35 additions & 10 deletions zipline/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,17 +1242,20 @@ def future_symbol(self, symbol):

@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
-------
Expand All @@ -1268,13 +1271,35 @@ def future_chain(self, root_symbol, as_of_date=None):
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):
Expand Down
Loading

0 comments on commit 0952688

Please sign in to comment.