Skip to content

Commit

Permalink
Merge d42a38d into 4386895
Browse files Browse the repository at this point in the history
  • Loading branch information
ehebert committed Oct 17, 2016
2 parents 4386895 + d42a38d commit 2bed3d3
Show file tree
Hide file tree
Showing 10 changed files with 680 additions and 19 deletions.
8 changes: 5 additions & 3 deletions tests/test_algorithm.py
Expand Up @@ -1440,7 +1440,7 @@ class TestAlgoScript(WithLogger,
STRING_TYPE_NAMES)
ARG_TYPE_TEST_CASES = (
('history__assets', (bad_type_history_assets,
ASSET_OR_STRING_TYPE_NAMES,
ASSET_OR_STRING_OR_CF_TYPE_NAMES,
True)),
('history__fields', (bad_type_history_fields,
STRING_TYPE_NAMES_STRING,
Expand All @@ -1458,10 +1458,12 @@ class TestAlgoScript(WithLogger,
('is_stale__assets', (bad_type_is_stale_assets, 'Asset', True)),
('can_trade__assets', (bad_type_can_trade_assets, 'Asset', True)),
('history_kwarg__assets',
(bad_type_history_assets_kwarg, ASSET_OR_STRING_TYPE_NAMES, True)),
(bad_type_history_assets_kwarg,
ASSET_OR_STRING_OR_CF_TYPE_NAMES,
True)),
('history_kwarg_bad_list__assets',
(bad_type_history_assets_kwarg_list,
ASSET_OR_STRING_TYPE_NAMES,
ASSET_OR_STRING_OR_CF_TYPE_NAMES,
True)),
('history_kwarg__fields',
(bad_type_history_fields_kwarg, STRING_TYPE_NAMES_STRING, True)),
Expand Down
222 changes: 218 additions & 4 deletions tests/test_continuous_futures.py
Expand Up @@ -15,21 +15,31 @@

from textwrap import dedent

from numpy import array, int64
from numpy import (
arange,
array,
int64,
full,
repeat,
)
from numpy.testing import assert_almost_equal
import pandas as pd
from pandas import Timestamp, DataFrame

from zipline import TradingAlgorithm
from zipline.assets.continuous_futures import OrderedContracts
from zipline.data.minute_bars import FUTURES_MINUTES_PER_DAY
from zipline.testing.fixtures import (
WithCreateBarData,
WithBcolzFutureMinuteBarReader,
WithSimParams,
ZiplineTestCase,
)


class ContinuousFuturesTestCase(WithCreateBarData,
WithSimParams,
WithBcolzFutureMinuteBarReader,
ZiplineTestCase):

START_DATE = pd.Timestamp('2015-01-05', tz='UTC')
Expand Down Expand Up @@ -66,24 +76,54 @@ def make_futures_info(self):
Timestamp('2022-08-19', tz='UTC')],
'notice_date': [Timestamp('2016-01-26', tz='UTC'),
Timestamp('2016-02-26', tz='UTC'),
Timestamp('2016-03-26', tz='UTC'),
Timestamp('2016-03-24', tz='UTC'),
Timestamp('2016-04-26', tz='UTC'),
Timestamp('2022-01-26', tz='UTC')],
'expiration_date': [Timestamp('2016-01-26', tz='UTC'),
Timestamp('2016-02-26', tz='UTC'),
Timestamp('2016-03-26', tz='UTC'),
Timestamp('2016-03-24', tz='UTC'),
Timestamp('2016-04-26', tz='UTC'),
Timestamp('2022-01-26', tz='UTC')],
'auto_close_date': [Timestamp('2016-01-26', tz='UTC'),
Timestamp('2016-02-26', tz='UTC'),
Timestamp('2016-03-26', tz='UTC'),
Timestamp('2016-03-24', tz='UTC'),
Timestamp('2016-04-26', tz='UTC'),
Timestamp('2022-01-26', tz='UTC')],
'tick_size': [0.001] * 5,
'multiplier': [1000.0] * 5,
'exchange': ['CME'] * 5,
})

@classmethod
def make_future_minute_bar_data(cls):
tc = cls.trading_calendar
start = pd.Timestamp('2016-01-26', tz='UTC')
end = pd.Timestamp('2016-04-29', tz='UTC')
dts = tc.minutes_for_sessions_in_range(start, end)
sessions = tc.sessions_in_range(start, end)
# Generate values in the .0XX space such that the first session
# has 0.001 added to all values, the second session has 0.002,
# etc.
markers = repeat(
arange(0.001, 0.001 * (len(sessions) + 1), 0.001),
FUTURES_MINUTES_PER_DAY)
vol_markers = repeat(
arange(1, (len(sessions) + 1), 1, dtype=int64),
FUTURES_MINUTES_PER_DAY)
base_df = pd.DataFrame(
{
'open': full(len(dts), 100.2) + markers,
'high': full(len(dts), 100.9) + markers,
'low': full(len(dts), 100.1) + markers,
'close': full(len(dts), 100.5) + markers,
'volume': full(len(dts), 1000, dtype=int64) + vol_markers,
},
index=dts)
# Add the sid to the ones place of the prices, so that the ones
# place can be used to eyeball the source contract.
for i in range(5):
yield i, base_df + i

def test_create_continuous_future(self):
cf_primary = self.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
Expand Down Expand Up @@ -287,6 +327,180 @@ def record_current_contract(algo, data):
'End of secondary chain should be FOJ16 on second '
'session.')

def test_history_sid_session(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
window = self.data_portal.get_history_window(
[cf],
Timestamp('2016-03-03 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1d', 'sid')

self.assertEqual(window.loc['2016-01-25', cf],
0,
"Should be FOF16 at beginning of window.")

self.assertEqual(window.loc['2016-01-26', cf],
1,
"Should be FOG16 after first roll.")

self.assertEqual(window.loc['2016-02-25', cf],
1,
"Should be FOF16 on session before roll.")

self.assertEqual(window.loc['2016-02-26', cf],
2,
"Should be FOH16 on session with roll.")

self.assertEqual(window.loc['2016-02-29', cf],
2,
"Should be FOH16 on session after roll.")

# Advance the window a month.
window = self.data_portal.get_history_window(
[cf],
Timestamp('2016-04-06 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1d', 'sid')

self.assertEqual(window.loc['2016-02-25', cf],
1,
"Should be FOG16 at beginning of window.")

self.assertEqual(window.loc['2016-02-26', cf],
2,
"Should be FOH16 on session with roll.")

self.assertEqual(window.loc['2016-02-29', cf],
2,
"Should be FOH16 on session after roll.")

self.assertEqual(window.loc['2016-03-24', cf],
3,
"Should be FOJ16 on session with roll.")

self.assertEqual(window.loc['2016-03-28', cf],
3,
"Should be FOJ16 on session after roll.")

def test_history_sid_minute(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
window = self.data_portal.get_history_window(
[cf.sid],
Timestamp('2016-01-25 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1m', 'sid')

self.assertEqual(window.loc['2016-01-25 22:32', cf],
0,
"Should be FOF16 at beginning of window. A minute "
"which is in the 01-25 session, before the roll.")

self.assertEqual(window.loc['2016-01-25 23:00', cf],
0,
"Should be FOF16 on on minute before roll minute.")

self.assertEqual(window.loc['2016-01-25 23:01', cf],
1,
"Should be FOG16 on minute after roll.")

# Advance the window a day.
window = self.data_portal.get_history_window(
[cf],
Timestamp('2016-01-26 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1m', 'sid')

self.assertEqual(window.loc['2016-01-26 22:32', cf],
1,
"Should be FOG16 at beginning of window.")

self.assertEqual(window.loc['2016-01-26 23:01', cf],
1,
"Should remain FOG16 on next session.")

def test_history_close_session(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
window = self.data_portal.get_history_window(
[cf.sid], Timestamp('2016-03-06', tz='UTC'), 30, '1d', 'close')

assert_almost_equal(
window.loc['2016-01-26', cf],
101.501,
err_msg="At beginning of window, should be FOG16's first value.")

assert_almost_equal(
window.loc['2016-02-26', cf],
102.524,
err_msg="On session with roll, should be FOH16's 24th value.")

assert_almost_equal(
window.loc['2016-02-29', cf],
102.525,
err_msg="After roll, Should be FOH16's 25th value.")

# Advance the window a month.
window = self.data_portal.get_history_window(
[cf.sid], Timestamp('2016-04-06', tz='UTC'), 30, '1d', 'close')

assert_almost_equal(
window.loc['2016-02-24', cf],
101.522,
err_msg="At beginning of window, should be FOG16's 22nd value.")

assert_almost_equal(
window.loc['2016-02-26', cf],
102.524,
err_msg="On session with roll, should be FOH16's 24th value.")

assert_almost_equal(
window.loc['2016-02-29', cf],
102.525,
err_msg="On session after roll, should be FOH16's 25th value.")

assert_almost_equal(
window.loc['2016-03-24', cf],
103.543,
err_msg="On session with roll, should be FOJ16's 43rd value.")

assert_almost_equal(
window.loc['2016-03-28', cf],
103.544,
err_msg="On session after roll, Should be FOJ16's 44th value.")

def test_history_close_minute(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
window = self.data_portal.get_history_window(
[cf.sid],
Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1m', 'close')

self.assertEqual(window.loc['2016-02-25 22:32', cf],
101.523,
"Should be FOG16 at beginning of window. A minute "
"which is in the 02-25 session, before the roll.")

self.assertEqual(window.loc['2016-02-25 23:00', cf],
101.523,
"Should be FOG16 on on minute before roll minute.")

self.assertEqual(window.loc['2016-02-25 23:01', cf],
102.524,
"Should be FOH16 on minute after roll.")

# Advance the window a session.
window = self.data_portal.get_history_window(
[cf],
Timestamp('2016-02-28 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1m', 'close')

self.assertEqual(window.loc['2016-02-26 22:32', cf],
102.524,
"Should be FOH16 at beginning of window.")

self.assertEqual(window.loc['2016-02-28 23:01', cf],
102.525,
"Should remain FOH16 on next session.")


class OrderedContractsTestCase(ZiplineTestCase):

Expand Down
3 changes: 2 additions & 1 deletion zipline/_protocol.pyx
Expand Up @@ -588,7 +588,8 @@ cdef class BarData:

@check_parameters(('assets', 'fields', 'bar_count',
'frequency'),
((Asset,) + string_types, string_types, int,
((Asset, ContinuousFuture) + string_types, string_types,
int,
string_types))
def history(self, assets, fields, bar_count, frequency):
"""
Expand Down
2 changes: 1 addition & 1 deletion zipline/assets/continuous_futures.pyx
Expand Up @@ -106,7 +106,7 @@ cdef class ContinuousFuture:
Cython rich comparison method. This is used in place of various
equality checkers in pure python.
"""
cdef int x_as_int, y_as_int
cdef long_t x_as_int, y_as_int

try:
x_as_int = PyNumber_Index(x)
Expand Down
49 changes: 49 additions & 0 deletions zipline/assets/roll_finder.py
Expand Up @@ -15,6 +15,8 @@
from abc import ABCMeta, abstractmethod
from six import with_metaclass

from pandas import Timestamp


class RollFinder(with_metaclass(ABCMeta, object)):
"""
Expand Down Expand Up @@ -42,6 +44,33 @@ def get_contract_center(self, root_symbol, dt, offset):
"""
raise NotImplemented

@abstractmethod
def get_rolls(self, root_symbol, start, end, offset):
"""
Get the rolls, i.e. the session at which to hop from contract to
contract in the chain.
Parameters
----------
root_symbol : str
The root symbol for which to calculate rolls.
start : Timestamp
Start of the date range.
end : Timestamp
End of the date range.
offset : int
Offset from the primary.
Returns
-------
rolls - list[tuple(sid, roll_date)]
A list of rolls, where first value is the first active `sid`,
and the `roll_date` on which to hop to the next contract.
The last pair in the chain has a value of `None` since the roll
is after the range.
"""
raise NotImplemented


class CalendarRollFinder(RollFinder):
"""
Expand All @@ -61,3 +90,23 @@ def get_contract_center(self, root_symbol, dt, offset):
# Here is where a volume check would be.
primary = primary_candidate
return oc.contract_at_offset(primary, offset)

def get_rolls(self, root_symbol, start, end, offset):
oc = self.asset_finder.get_ordered_contracts(root_symbol)
primary_at_end = self.get_contract_center(root_symbol, end, 0)
for i, sid in enumerate(oc.contract_sids):
if sid == primary_at_end:
break
i += offset
first = oc.contract_sids[i]
rolls = [(first, None)]
i -= 1
auto_close_date = Timestamp(oc.auto_close_dates[i - offset], tz='UTC')
while auto_close_date > start and i > -1:
rolls.insert(0, (oc.contract_sids[i - offset],
auto_close_date))
i -= 1
auto_close_date = Timestamp(oc.auto_close_dates[i - offset],
tz='UTC')

return rolls

0 comments on commit 2bed3d3

Please sign in to comment.