Files
catalyst/tests/test_continuous_futures.py
T
Eddie Hebert 2f16c08dcd ENH: Add history for continuous futures.
Enable unadjusted history for continuous futures.

The history array is filled by the values for the underlying contracts,
where the contract used changes based on rolls.

e.g., if a `1d` history window was over the range
`2016-01-20` -> `2016-02-29` with contracts with a suffix of `F16` that
rolls at the beginning of the session on `2016-01-26`, `G16` on
`2016-02-26`, and `H16` on `2016-03-26`. The `2016-01-20` ->
`2016-01-25` portion would use the values for `F16', the `2016-01-26` ->
`2016-02-25` portion would use `G16` and the `2016-02-26` ->
`2016-02-29` portion would use `H16`.

Using the same contracts as above, a `1m` history window over the range
(using a timezone of US/Eastern) `2016-01-25 4:00PM` -> `2016-01-25
7:00PM` would fill the `4:00PM` -> `6:00PM` portion with data for `F16`
and the `6:01PM` -> `7:00PM` portion with data for `G16`, since the
beginning of the `2016-01-26` session is `2016-01-25 6:01PM`.

Supports `1d` and `1m`.

Also adds the `sid` field to `history` to assist in showing the active
contract at each dt in the window.
2016-10-16 22:40:08 -04:00

568 lines
23 KiB
Python

#
# 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from textwrap import dedent
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')
END_DATE = pd.Timestamp('2016-10-19', tz='UTC')
SIM_PARAMS_START = pd.Timestamp('2016-01-25', tz='UTC')
SIM_PARAMS_END = pd.Timestamp('2016-01-27', tz='UTC')
SIM_PARAMS_DATA_FREQUENCY = 'minute'
TRADING_CALENDAR_STRS = ('us_futures',)
TRADING_CALENDAR_PRIMARY_CAL = 'us_futures'
@classmethod
def make_root_symbols_info(self):
return pd.DataFrame({
'root_symbol': ['FO'],
'root_symbol_id': [1],
'exchange': ['CME']})
@classmethod
def make_futures_info(self):
return DataFrame({
'symbol': ['FOF16', 'FOG16', 'FOH16', 'FOJ16', 'FOF22'],
'root_symbol': ['FO', 'FO', 'FO', 'FO', 'FO'],
'asset_name': ['Foo'] * 5,
'start_date': [Timestamp('2015-01-05', tz='UTC'),
Timestamp('2015-02-05', tz='UTC'),
Timestamp('2015-03-05', tz='UTC'),
Timestamp('2015-04-05', tz='UTC'),
Timestamp('2021-01-05', tz='UTC')],
'end_date': [Timestamp('2016-08-19', tz='UTC'),
Timestamp('2016-09-19', tz='UTC'),
Timestamp('2016-10-19', tz='UTC'),
Timestamp('2016-11-19', tz='UTC'),
Timestamp('2022-08-19', tz='UTC')],
'notice_date': [Timestamp('2016-01-26', tz='UTC'),
Timestamp('2016-02-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-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-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')
self.assertEqual(cf_primary.root_symbol, 'FO')
self.assertEqual(cf_primary.offset, 0)
self.assertEqual(cf_primary.roll_style, 'calendar')
retrieved_primary = self.asset_finder.retrieve_asset(
cf_primary.sid)
self.assertEqual(retrieved_primary, cf_primary)
cf_secondary = self.asset_finder.create_continuous_future(
'FO', 1, 'calendar')
self.assertEqual(cf_secondary.root_symbol, 'FO')
self.assertEqual(cf_secondary.offset, 1)
self.assertEqual(cf_secondary.roll_style, 'calendar')
retrieved = self.asset_finder.retrieve_asset(
cf_secondary.sid)
self.assertEqual(retrieved, cf_secondary)
self.assertNotEqual(cf_primary, cf_secondary)
def test_current_contract(self):
cf_primary = self.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
bar_data = self.create_bardata(
lambda: pd.Timestamp('2016-01-25', tz='UTC'))
contract = bar_data.current(cf_primary, 'contract')
self.assertEqual(contract.symbol, 'FOF16')
bar_data = self.create_bardata(
lambda: pd.Timestamp('2016-01-26', tz='UTC'))
contract = bar_data.current(cf_primary, 'contract')
self.assertEqual(contract.symbol, 'FOG16',
'Auto close at beginning of session so FOG16 is now '
'the current contract.')
bar_data = self.create_bardata(
lambda: pd.Timestamp('2016-01-27', tz='UTC'))
contract = bar_data.current(cf_primary, 'contract')
self.assertEqual(contract.symbol, 'FOG16')
def test_current_contract_in_algo(self):
code = dedent("""
from zipline.api import (
record,
continuous_future,
schedule_function,
get_datetime,
)
def initialize(algo):
algo.primary_cl = continuous_future('FO', 0, 'calendar')
algo.secondary_cl = continuous_future('FO', 1, 'calendar')
schedule_function(record_current_contract)
def record_current_contract(algo, data):
record(datetime=get_datetime())
record(primary=data.current(algo.primary_cl, 'contract'))
record(secondary=data.current(algo.secondary_cl, 'contract'))
""")
algo = TradingAlgorithm(script=code,
sim_params=self.sim_params,
trading_calendar=self.trading_calendar,
env=self.env)
results = algo.run(self.data_portal)
result = results.iloc[0]
self.assertEqual(result.primary.symbol,
'FOF16',
'Primary should be FOF16 on first session.')
self.assertEqual(result.secondary.symbol,
'FOG16',
'Secondary should be FOG16 on first session.')
result = results.iloc[1]
# Second day, primary should switch to FOG
self.assertEqual(result.primary.symbol,
'FOG16',
'Primary should be FOG16 on second session, auto '
'close is at beginning of the session.')
self.assertEqual(result.secondary.symbol,
'FOH16',
'Secondary should be FOH16 on second session, auto '
'close is at beginning of the session.')
result = results.iloc[2]
# Second day, primary should switch to FOG
self.assertEqual(result.primary.symbol,
'FOG16',
'Primary should remain as FOG16 on third session.')
self.assertEqual(result.secondary.symbol,
'FOH16',
'Secondary should remain as FOH16 on third session.')
def test_current_chain_in_algo(self):
code = dedent("""
from zipline.api import (
record,
continuous_future,
schedule_function,
get_datetime,
)
def initialize(algo):
algo.primary_cl = continuous_future('FO', 0, 'calendar')
algo.secondary_cl = continuous_future('FO', 1, 'calendar')
schedule_function(record_current_contract)
def record_current_contract(algo, data):
record(datetime=get_datetime())
primary_chain = data.current_chain(algo.primary_cl)
secondary_chain = data.current_chain(algo.secondary_cl)
record(primary_len=len(primary_chain))
record(primary_first=primary_chain[0].symbol)
record(primary_last=primary_chain[-1].symbol)
record(secondary_len=len(secondary_chain))
record(secondary_first=secondary_chain[0].symbol)
record(secondary_last=secondary_chain[-1].symbol)
""")
algo = TradingAlgorithm(script=code,
sim_params=self.sim_params,
trading_calendar=self.trading_calendar,
env=self.env)
results = algo.run(self.data_portal)
result = results.iloc[0]
self.assertEqual(result.primary_len,
4,
'There should be only 4 contracts in the chain for '
'the primary, there are 5 contracts defined in the '
'fixture, but one has a start after the simulation '
'date.')
self.assertEqual(result.secondary_len,
3,
'There should be only 3 contracts in the chain for '
'the primary, there are 5 contracts defined in the '
'fixture, but one has a start after the simulation '
'date. And the first is not included because it is '
'the primary on that date.')
self.assertEqual(result.primary_first,
'FOF16',
'Front of primary chain should be FOF16 on first '
'session.')
self.assertEqual(result.secondary_first,
'FOG16',
'Front of secondary chain should be FOG16 on first '
'session.')
self.assertEqual(result.primary_last,
'FOJ16',
'End of primary chain should be FOJ16 on first '
'session.')
self.assertEqual(result.secondary_last,
'FOJ16',
'End of secondary chain should be FOJ16 on first '
'session.')
# Second day, primary should switch to FOG
result = results.iloc[1]
self.assertEqual(result.primary_len,
3,
'There should be only 3 contracts in the chain for '
'the primary, there are 5 contracts defined in the '
'fixture, but one has a start after the simulation '
'date. The first is not included because of roll.')
self.assertEqual(result.secondary_len,
2,
'There should be only 2 contracts in the chain for '
'the primary, there are 5 contracts defined in the '
'fixture, but one has a start after the simulation '
'date. The first is not included because of roll, '
'the second is the primary on that date.')
self.assertEqual(result.primary_first,
'FOG16',
'Front of primary chain should be FOG16 on second '
'session.')
self.assertEqual(result.secondary_first,
'FOH16',
'Front of secondary chain should be FOH16 on second '
'session.')
# These values remain FOJ16 because fixture data is not exhaustive
# enough to move the end of the chain.
self.assertEqual(result.primary_last,
'FOJ16',
'End of primary chain should be FOJ16 on second '
'session.')
self.assertEqual(result.secondary_last,
'FOJ16',
'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):
def test_active_chain(self):
contract_sids = array([1, 2, 3, 4], dtype=int64)
start_dates = pd.date_range('2015-01-01', periods=4, tz="UTC")
auto_close_dates = pd.date_range('2016-04-01', periods=4, tz="UTC")
oc = OrderedContracts('FO',
contract_sids,
start_dates.astype('int64'),
auto_close_dates.astype('int64'))
# Test sid 1 as days increment, as the sessions march forward
# a contract should be added per day, until all defined contracts
# are returned.
chain = oc.active_chain(1, pd.Timestamp('2014-12-31', tz='UTC').value)
self.assertEquals([], list(chain),
"On session before first start date, no contracts "
"in chain should be active.")
chain = oc.active_chain(1, pd.Timestamp('2015-01-01', tz='UTC').value)
self.assertEquals([1], list(chain),
"[1] should be the active chain on 01-01, since all "
"other start dates occur after 01-01.")
chain = oc.active_chain(1, pd.Timestamp('2015-01-02', tz='UTC').value)
self.assertEquals([1, 2], list(chain),
"[1, 2] should be the active contracts on 01-02.")
chain = oc.active_chain(1, pd.Timestamp('2015-01-03', tz='UTC').value)
self.assertEquals([1, 2, 3], list(chain),
"[1, 2, 3] should be the active contracts on 01-03.")
chain = oc.active_chain(1, pd.Timestamp('2015-01-04', tz='UTC').value)
self.assertEquals(4, len(chain),
"[1, 2, 3, 4] should be the active contracts on "
"01-04, this is all defined contracts in the test "
"case.")
chain = oc.active_chain(1, pd.Timestamp('2015-01-05', tz='UTC').value)
self.assertEquals(4, len(chain),
"[1, 2, 3, 4] should be the active contracts on "
"01-05. This tests the case where all start dates "
"are before the query date.")
# Test querying each sid at a time when all should be alive.
chain = oc.active_chain(2, pd.Timestamp('2015-01-05', tz='UTC').value)
self.assertEquals([2, 3, 4], list(chain))
chain = oc.active_chain(3, pd.Timestamp('2015-01-05', tz='UTC').value)
self.assertEquals([3, 4], list(chain))
chain = oc.active_chain(4, pd.Timestamp('2015-01-05', tz='UTC').value)
self.assertEquals([4], list(chain))
# Test defined contract to check edge conditions.
chain = oc.active_chain(4, pd.Timestamp('2015-01-03', tz='UTC').value)
self.assertEquals([], list(chain),
"No contracts should be active, since 01-03 is "
"before 4's start date.")
chain = oc.active_chain(4, pd.Timestamp('2015-01-04', tz='UTC').value)
self.assertEquals([4], list(chain),
"[4] should be active beginning at its start date.")