ENH: Simplified implementation of FutureChain object (not user-facing API).

No longer auto-updates its internal as-of date, instead requires an explicit
as-of date from the consumer.

Take a static list of contracts (instead of needing an assetfinder).

Instead of the as_of method, the user-facing API now lets you pass in an
offset, which is defined as an integral number of sessions.
This commit is contained in:
Jean Bredeche
2016-08-29 11:30:25 -04:00
parent 35631f4882
commit 38ff7e5aa7
4 changed files with 153 additions and 244 deletions
+44 -2
View File
@@ -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')
+34 -105
View File
@@ -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 = {
+35 -10
View File
@@ -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):
+40 -127
View File
@@ -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