mirror of
https://github.com/wassname/catalyst.git
synced 2026-08-13 12:00:16 +08:00
Merge pull request #1529 from quantopian/current-contract
ENH: Add continuous future current contract.
This commit is contained in:
@@ -80,6 +80,8 @@ class LazyBuildExtCommandClass(dict):
|
|||||||
|
|
||||||
ext_modules = [
|
ext_modules = [
|
||||||
Extension('zipline.assets._assets', ['zipline/assets/_assets.pyx']),
|
Extension('zipline.assets._assets', ['zipline/assets/_assets.pyx']),
|
||||||
|
Extension('zipline.assets.continuous_futures',
|
||||||
|
['zipline/assets/continuous_futures.pyx']),
|
||||||
Extension('zipline.lib.adjustment', ['zipline/lib/adjustment.pyx']),
|
Extension('zipline.lib.adjustment', ['zipline/lib/adjustment.pyx']),
|
||||||
Extension('zipline.lib._factorize', ['zipline/lib/_factorize.pyx']),
|
Extension('zipline.lib._factorize', ['zipline/lib/_factorize.pyx']),
|
||||||
Extension(
|
Extension(
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from zipline import run_algorithm
|
|||||||
from zipline import TradingAlgorithm
|
from zipline import TradingAlgorithm
|
||||||
from zipline.api import FixedSlippage
|
from zipline.api import FixedSlippage
|
||||||
from zipline.assets import Equity, Future, Asset
|
from zipline.assets import Equity, Future, Asset
|
||||||
|
from zipline.assets.continuous_futures import ContinuousFuture
|
||||||
from zipline.assets.synthetic import (
|
from zipline.assets.synthetic import (
|
||||||
make_jagged_equity_info,
|
make_jagged_equity_info,
|
||||||
make_simple_equity_info,
|
make_simple_equity_info,
|
||||||
@@ -1431,8 +1432,12 @@ class TestAlgoScript(WithLogger,
|
|||||||
STRING_TYPE_NAMES = [s.__name__ for s in string_types]
|
STRING_TYPE_NAMES = [s.__name__ for s in string_types]
|
||||||
STRING_TYPE_NAMES_STRING = ', '.join(STRING_TYPE_NAMES)
|
STRING_TYPE_NAMES_STRING = ', '.join(STRING_TYPE_NAMES)
|
||||||
ASSET_TYPE_NAME = Asset.__name__
|
ASSET_TYPE_NAME = Asset.__name__
|
||||||
|
CONTINUOUS_FUTURE_NAME = ContinuousFuture.__name__
|
||||||
ASSET_OR_STRING_TYPE_NAMES = ', '.join([ASSET_TYPE_NAME] +
|
ASSET_OR_STRING_TYPE_NAMES = ', '.join([ASSET_TYPE_NAME] +
|
||||||
STRING_TYPE_NAMES)
|
STRING_TYPE_NAMES)
|
||||||
|
ASSET_OR_STRING_OR_CF_TYPE_NAMES = ', '.join([ASSET_TYPE_NAME,
|
||||||
|
CONTINUOUS_FUTURE_NAME] +
|
||||||
|
STRING_TYPE_NAMES)
|
||||||
ARG_TYPE_TEST_CASES = (
|
ARG_TYPE_TEST_CASES = (
|
||||||
('history__assets', (bad_type_history_assets,
|
('history__assets', (bad_type_history_assets,
|
||||||
ASSET_OR_STRING_TYPE_NAMES,
|
ASSET_OR_STRING_TYPE_NAMES,
|
||||||
@@ -1445,7 +1450,7 @@ class TestAlgoScript(WithLogger,
|
|||||||
STRING_TYPE_NAMES_STRING,
|
STRING_TYPE_NAMES_STRING,
|
||||||
False)),
|
False)),
|
||||||
('current__assets', (bad_type_current_assets,
|
('current__assets', (bad_type_current_assets,
|
||||||
ASSET_OR_STRING_TYPE_NAMES,
|
ASSET_OR_STRING_OR_CF_TYPE_NAMES,
|
||||||
True)),
|
True)),
|
||||||
('current__fields', (bad_type_current_fields,
|
('current__fields', (bad_type_current_fields,
|
||||||
STRING_TYPE_NAMES_STRING,
|
STRING_TYPE_NAMES_STRING,
|
||||||
@@ -1465,7 +1470,9 @@ class TestAlgoScript(WithLogger,
|
|||||||
('history_kwarg__frequency',
|
('history_kwarg__frequency',
|
||||||
(bad_type_history_frequency_kwarg, STRING_TYPE_NAMES_STRING, False)),
|
(bad_type_history_frequency_kwarg, STRING_TYPE_NAMES_STRING, False)),
|
||||||
('current_kwarg__assets',
|
('current_kwarg__assets',
|
||||||
(bad_type_current_assets_kwarg, ASSET_OR_STRING_TYPE_NAMES, True)),
|
(bad_type_current_assets_kwarg,
|
||||||
|
ASSET_OR_STRING_OR_CF_TYPE_NAMES,
|
||||||
|
True)),
|
||||||
('current_kwarg__fields',
|
('current_kwarg__fields',
|
||||||
(bad_type_current_fields_kwarg, STRING_TYPE_NAMES_STRING, True)),
|
(bad_type_current_fields_kwarg, STRING_TYPE_NAMES_STRING, True)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from pandas import Timestamp, DataFrame
|
||||||
|
|
||||||
|
from zipline import TradingAlgorithm
|
||||||
|
from zipline.testing.fixtures import (
|
||||||
|
WithCreateBarData,
|
||||||
|
WithSimParams,
|
||||||
|
ZiplineTestCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContinuousFuturesTestCase(WithCreateBarData,
|
||||||
|
WithSimParams,
|
||||||
|
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': ['FOF', 'FOG', 'FOH'],
|
||||||
|
'root_symbol': ['FO', 'FO', 'FO'],
|
||||||
|
'asset_name': ['Foo'] * 3,
|
||||||
|
'start_date': [Timestamp('2015-01-05', tz='UTC'),
|
||||||
|
Timestamp('2015-02-05', tz='UTC'),
|
||||||
|
Timestamp('2015-03-05', tz='UTC')],
|
||||||
|
'end_date': [Timestamp('2016-08-19', tz='UTC'),
|
||||||
|
Timestamp('2016-09-19', tz='UTC'),
|
||||||
|
Timestamp('2016-10-19', tz='UTC')],
|
||||||
|
'notice_date': [Timestamp('2016-01-26', tz='UTC'),
|
||||||
|
Timestamp('2016-02-26', tz='UTC'),
|
||||||
|
Timestamp('2016-03-26', tz='UTC')],
|
||||||
|
'expiration_date': [Timestamp('2016-01-26', tz='UTC'),
|
||||||
|
Timestamp('2016-02-26', tz='UTC'),
|
||||||
|
Timestamp('2016-03-26', tz='UTC')],
|
||||||
|
'auto_close_date': [Timestamp('2016-01-26', tz='UTC'),
|
||||||
|
Timestamp('2016-02-26', tz='UTC'),
|
||||||
|
Timestamp('2016-03-26', tz='UTC')],
|
||||||
|
'tick_size': [0.001] * 3,
|
||||||
|
'multiplier': [1000.0] * 3,
|
||||||
|
'exchange': ['CME'] * 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
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, 'FOF')
|
||||||
|
|
||||||
|
bar_data = self.create_bardata(
|
||||||
|
lambda: pd.Timestamp('2016-01-26', tz='UTC'))
|
||||||
|
contract = bar_data.current(cf_primary, 'contract')
|
||||||
|
|
||||||
|
self.assertEqual(contract.symbol, 'FOG',
|
||||||
|
'Auto close at beginning of session so FOG 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, 'FOG')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertEqual(results.iloc[0].primary.symbol,
|
||||||
|
'FOF',
|
||||||
|
'Primary should be FOF on first session.')
|
||||||
|
self.assertEqual(results.iloc[0].secondary.symbol,
|
||||||
|
'FOG',
|
||||||
|
'Secondary should be FOG on first session.')
|
||||||
|
|
||||||
|
# Second day, primary should switch to FOG
|
||||||
|
self.assertEqual(results.iloc[1].primary.symbol,
|
||||||
|
'FOG',
|
||||||
|
'Primary should be FOG on second session, auto close '
|
||||||
|
'is at beginning of the session.')
|
||||||
|
self.assertEqual(results.iloc[1].secondary.symbol,
|
||||||
|
'FOH',
|
||||||
|
'Secondary should be FOH on second session, auto '
|
||||||
|
'close is at beginning of the session.')
|
||||||
|
|
||||||
|
# Second day, primary should switch to FOG
|
||||||
|
self.assertEqual(results.iloc[2].primary.symbol,
|
||||||
|
'FOG',
|
||||||
|
'Primary should remain as FOG on third session.')
|
||||||
|
self.assertEqual(results.iloc[2].secondary.symbol,
|
||||||
|
'FOH',
|
||||||
|
'Secondary should remain as FOG on third session.')
|
||||||
@@ -25,6 +25,7 @@ from cpython cimport bool
|
|||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
|
|
||||||
from zipline.assets import Asset, Future
|
from zipline.assets import Asset, Future
|
||||||
|
from zipline.assets.continuous_futures import ContinuousFuture
|
||||||
from zipline.zipline_warnings import ZiplineDeprecationWarning
|
from zipline.zipline_warnings import ZiplineDeprecationWarning
|
||||||
|
|
||||||
|
|
||||||
@@ -254,7 +255,7 @@ cdef class BarData:
|
|||||||
return dt
|
return dt
|
||||||
|
|
||||||
@check_parameters(('assets', 'fields'),
|
@check_parameters(('assets', 'fields'),
|
||||||
((Asset,) + string_types, string_types))
|
((Asset, ContinuousFuture) + string_types, string_types))
|
||||||
def current(self, assets, fields):
|
def current(self, assets, fields):
|
||||||
"""
|
"""
|
||||||
Returns the current value of the given assets for the given fields
|
Returns the current value of the given assets for the given fields
|
||||||
|
|||||||
@@ -1185,6 +1185,33 @@ class TradingAlgorithm(object):
|
|||||||
as_of_date=_lookup_date,
|
as_of_date=_lookup_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
@preprocess(root_symbol_str=ensure_upper_case)
|
||||||
|
def continuous_future(self, root_symbol_str, offset, roll):
|
||||||
|
"""Create a specifier for a continuous contract.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
root_symbol_str : str
|
||||||
|
The root symbol for the future chain.
|
||||||
|
|
||||||
|
offset : int
|
||||||
|
The distance from the primary contract.
|
||||||
|
|
||||||
|
roll_style : str
|
||||||
|
How rolls are determined.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
continuous_future : ContinuousFuture
|
||||||
|
The continuous future specifier.
|
||||||
|
"""
|
||||||
|
return self.asset_finder.create_continuous_future(
|
||||||
|
root_symbol_str,
|
||||||
|
offset,
|
||||||
|
roll,
|
||||||
|
)
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
def symbols(self, *args):
|
def symbols(self, *args):
|
||||||
"""Lookup multuple Equities as a list.
|
"""Lookup multuple Equities as a list.
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from abc import ABCMeta
|
from abc import ABCMeta
|
||||||
|
import array
|
||||||
|
import binascii
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from numbers import Integral
|
from numbers import Integral
|
||||||
from operator import itemgetter, attrgetter
|
from operator import itemgetter, attrgetter
|
||||||
|
import struct
|
||||||
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -46,6 +49,7 @@ from zipline.errors import (
|
|||||||
from . import (
|
from . import (
|
||||||
Asset, Equity, Future,
|
Asset, Equity, Future,
|
||||||
)
|
)
|
||||||
|
from . continuous_futures import OrderedContracts, ContinuousFuture
|
||||||
from .asset_writer import (
|
from .asset_writer import (
|
||||||
check_version_info,
|
check_version_info,
|
||||||
split_delimited_symbol,
|
split_delimited_symbol,
|
||||||
@@ -119,6 +123,41 @@ def _convert_asset_timestamp_fields(dict_):
|
|||||||
return dict_
|
return dict_
|
||||||
|
|
||||||
|
|
||||||
|
SID_TYPE_IDS = {
|
||||||
|
# Asset would be 0,
|
||||||
|
ContinuousFuture: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTINUOUS_FUTURE_ROLL_STYLE_IDS = {
|
||||||
|
'calendar': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_continuous_future_sid(root_symbol, offset, roll_style):
|
||||||
|
s = struct.Struct("B 2B B B 3B")
|
||||||
|
# B - sid type
|
||||||
|
# 2B - root symbol
|
||||||
|
# B - offset (could be packed smaller since offsets of greater than 12 are
|
||||||
|
# probably unneeded.)
|
||||||
|
# B - roll type
|
||||||
|
# 3B - empty space left for parameterized roll types
|
||||||
|
|
||||||
|
# The root symbol currently supports 2 characters. If 3 char root symbols
|
||||||
|
# are needed, the size of the root symbol does not need to change, however
|
||||||
|
# writing the string directly will need to change to a scheme of writing
|
||||||
|
# the A-Z values in 5-bit chunks.
|
||||||
|
a = array.array('B', [0] * s.size)
|
||||||
|
rs = bytearray(root_symbol, 'ascii')
|
||||||
|
values = (SID_TYPE_IDS[ContinuousFuture],
|
||||||
|
rs[0],
|
||||||
|
rs[1],
|
||||||
|
offset,
|
||||||
|
CONTINUOUS_FUTURE_ROLL_STYLE_IDS[roll_style],
|
||||||
|
0, 0, 0,)
|
||||||
|
s.pack_into(a, 0, *values)
|
||||||
|
return int(binascii.hexlify(a), 16)
|
||||||
|
|
||||||
|
|
||||||
class AssetFinder(object):
|
class AssetFinder(object):
|
||||||
"""
|
"""
|
||||||
An AssetFinder is an interface to a database of Asset metadata written by
|
An AssetFinder is an interface to a database of Asset metadata written by
|
||||||
@@ -162,6 +201,8 @@ class AssetFinder(object):
|
|||||||
# retrieve_asset will populate the cache on first retrieval.
|
# retrieve_asset will populate the cache on first retrieval.
|
||||||
self._caches = (self._asset_cache, self._asset_type_cache) = {}, {}
|
self._caches = (self._asset_cache, self._asset_type_cache) = {}, {}
|
||||||
|
|
||||||
|
self._ordered_contracts = {}
|
||||||
|
|
||||||
# Populated on first call to `lifetimes`.
|
# Populated on first call to `lifetimes`.
|
||||||
self._asset_lifetimes = None
|
self._asset_lifetimes = None
|
||||||
|
|
||||||
@@ -821,6 +862,63 @@ class AssetFinder(object):
|
|||||||
|
|
||||||
return sids
|
return sids
|
||||||
|
|
||||||
|
def _get_contract_info(self, root_symbol):
|
||||||
|
fc_cols = self.futures_contracts.c
|
||||||
|
|
||||||
|
fields = (fc_cols.sid, fc_cols.start_date, fc_cols.auto_close_date)
|
||||||
|
|
||||||
|
return list(sa.select(fields).where(
|
||||||
|
(fc_cols.root_symbol == root_symbol) &
|
||||||
|
(fc_cols.start_date != pd.NaT.value))
|
||||||
|
.order_by(fc_cols.auto_close_date).execute().fetchall())
|
||||||
|
|
||||||
|
def _get_root_symbol_exchange(self, root_symbol):
|
||||||
|
fc_cols = self.futures_root_symbols.c
|
||||||
|
|
||||||
|
fields = (fc_cols.exchange,)
|
||||||
|
|
||||||
|
return sa.select(fields).where(
|
||||||
|
fc_cols.root_symbol == root_symbol).execute().fetchone()[0]
|
||||||
|
|
||||||
|
def get_ordered_contracts(self, root_symbol):
|
||||||
|
try:
|
||||||
|
return self._ordered_contracts[root_symbol]
|
||||||
|
except KeyError:
|
||||||
|
contract_info = self._get_contract_info(root_symbol)
|
||||||
|
size = len(contract_info)
|
||||||
|
sids = np.full(size, 0, dtype=np.int64)
|
||||||
|
start_dates = np.full(size, 0, dtype=np.int64)
|
||||||
|
auto_close_dates = np.full(size, 0, dtype=np.int64)
|
||||||
|
self._size = size
|
||||||
|
for i, info in enumerate(contract_info):
|
||||||
|
sid, start_date, auto_close_date = info
|
||||||
|
sids[i] = sid
|
||||||
|
start_dates[i] = start_date
|
||||||
|
auto_close_dates[i] = auto_close_date
|
||||||
|
oc = OrderedContracts(root_symbol,
|
||||||
|
sids,
|
||||||
|
start_dates,
|
||||||
|
auto_close_dates)
|
||||||
|
self._ordered_contracts[root_symbol] = oc
|
||||||
|
return oc
|
||||||
|
|
||||||
|
def create_continuous_future(self, root_symbol, offset, roll_style):
|
||||||
|
oc = self.get_ordered_contracts(root_symbol)
|
||||||
|
start_date = self.retrieve_asset(oc.contract_sids[0]).start_date
|
||||||
|
end_date = self.retrieve_asset(oc.contract_sids[-1]).end_date
|
||||||
|
exchange = self._get_root_symbol_exchange(root_symbol)
|
||||||
|
|
||||||
|
sid = _encode_continuous_future_sid(root_symbol, offset, roll_style)
|
||||||
|
cf = ContinuousFuture(sid,
|
||||||
|
root_symbol,
|
||||||
|
offset,
|
||||||
|
roll_style,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
exchange)
|
||||||
|
self._asset_cache[cf.sid] = cf
|
||||||
|
return cf
|
||||||
|
|
||||||
def _make_sids(tblattr):
|
def _make_sids(tblattr):
|
||||||
def _(self):
|
def _(self):
|
||||||
return tuple(map(
|
return tuple(map(
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
# cython: embedsignature=True
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Cythonized ContinuousFutures object.
|
||||||
|
"""
|
||||||
|
cimport cython
|
||||||
|
from cpython.number cimport PyNumber_Index
|
||||||
|
from cpython.object cimport (
|
||||||
|
Py_EQ,
|
||||||
|
Py_NE,
|
||||||
|
Py_GE,
|
||||||
|
Py_LE,
|
||||||
|
Py_GT,
|
||||||
|
Py_LT,
|
||||||
|
)
|
||||||
|
from cpython cimport bool
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from numpy cimport long_t, int64_t
|
||||||
|
import warnings
|
||||||
|
cimport numpy as np
|
||||||
|
|
||||||
|
from zipline.utils.calendars import get_calendar
|
||||||
|
|
||||||
|
|
||||||
|
cdef class ContinuousFuture:
|
||||||
|
"""
|
||||||
|
Represents a specifier for a chain of future contracts, where the
|
||||||
|
coordinates for the chain are:
|
||||||
|
root_symbol : str
|
||||||
|
The root symbol of the contracts.
|
||||||
|
offset : int
|
||||||
|
The distance from the primary chain.
|
||||||
|
e.g. 0 specifies the primary chain, 1 the secondary, etc.
|
||||||
|
roll_style : str
|
||||||
|
How rolls from contract to contract should be calculated.
|
||||||
|
Currently supports 'calendar'.
|
||||||
|
|
||||||
|
Instances of this class are exposed to the algorithm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cdef readonly long_t sid
|
||||||
|
# Cached hash of self.sid
|
||||||
|
cdef long_t sid_hash
|
||||||
|
|
||||||
|
cdef readonly object root_symbol
|
||||||
|
cdef readonly int offset
|
||||||
|
cdef readonly object roll_style
|
||||||
|
|
||||||
|
cdef readonly object start_date
|
||||||
|
cdef readonly object end_date
|
||||||
|
|
||||||
|
cdef readonly object exchange
|
||||||
|
|
||||||
|
_kwargnames = frozenset({
|
||||||
|
'sid',
|
||||||
|
'root_symbol',
|
||||||
|
'offset',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'exchange',
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
long_t sid, # sid is required
|
||||||
|
object root_symbol,
|
||||||
|
int offset,
|
||||||
|
object roll_style,
|
||||||
|
object start_date,
|
||||||
|
object end_date,
|
||||||
|
object exchange):
|
||||||
|
|
||||||
|
self.sid = sid
|
||||||
|
self.sid_hash = hash(sid)
|
||||||
|
self.root_symbol = root_symbol
|
||||||
|
self.roll_style = roll_style
|
||||||
|
self.offset = offset
|
||||||
|
self.exchange = exchange
|
||||||
|
self.start_date = start_date
|
||||||
|
self.end_date = end_date
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.sid
|
||||||
|
|
||||||
|
def __index__(self):
|
||||||
|
return self.sid
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.sid_hash
|
||||||
|
|
||||||
|
def __richcmp__(x, y, int op):
|
||||||
|
"""
|
||||||
|
Cython rich comparison method. This is used in place of various
|
||||||
|
equality checkers in pure python.
|
||||||
|
"""
|
||||||
|
cdef int x_as_int, y_as_int
|
||||||
|
|
||||||
|
try:
|
||||||
|
x_as_int = PyNumber_Index(x)
|
||||||
|
except (TypeError, OverflowError):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
try:
|
||||||
|
y_as_int = PyNumber_Index(y)
|
||||||
|
except (TypeError, OverflowError):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
compared = x_as_int - y_as_int
|
||||||
|
|
||||||
|
# Handle == and != first because they're significantly more common
|
||||||
|
# operations.
|
||||||
|
if op == Py_EQ:
|
||||||
|
return compared == 0
|
||||||
|
elif op == Py_NE:
|
||||||
|
return compared != 0
|
||||||
|
elif op == Py_LT:
|
||||||
|
return compared < 0
|
||||||
|
elif op == Py_LE:
|
||||||
|
return compared <= 0
|
||||||
|
elif op == Py_GT:
|
||||||
|
return compared > 0
|
||||||
|
elif op == Py_GE:
|
||||||
|
return compared >= 0
|
||||||
|
else:
|
||||||
|
raise AssertionError('%d is not an operator' % op)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s(%d [%s, %s, %s])' % (
|
||||||
|
type(self).__name__,
|
||||||
|
self.sid,
|
||||||
|
self.root_symbol,
|
||||||
|
self.offset,
|
||||||
|
self.roll_style
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
attrs = ('root_symbol', 'offset', 'roll_style')
|
||||||
|
tuples = ((attr, repr(getattr(self, attr, None)))
|
||||||
|
for attr in attrs)
|
||||||
|
strings = ('%s=%s' % (t[0], t[1]) for t in tuples)
|
||||||
|
params = ', '.join(strings)
|
||||||
|
return 'ContinuousFuture(%d, %s)' % (self.sid, params)
|
||||||
|
|
||||||
|
cpdef __reduce__(self):
|
||||||
|
"""
|
||||||
|
Function used by pickle to determine how to serialize/deserialize this
|
||||||
|
class. Should return a tuple whose first element is self.__class__,
|
||||||
|
and whose second element is a tuple of all the attributes that should
|
||||||
|
be serialized/deserialized during pickling.
|
||||||
|
"""
|
||||||
|
return (self.__class__, (self.sid,
|
||||||
|
self.root_symbol,
|
||||||
|
self.start_date,
|
||||||
|
self.end_date,
|
||||||
|
self.offset,
|
||||||
|
self.roll_style,
|
||||||
|
self.exchange))
|
||||||
|
|
||||||
|
cpdef to_dict(self):
|
||||||
|
"""
|
||||||
|
Convert to a python dict.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'sid': self.sid,
|
||||||
|
'root_symbol': self.root_symbol,
|
||||||
|
'start_date': self.start_date,
|
||||||
|
'end_date': self.end_date,
|
||||||
|
'offset': self.offset,
|
||||||
|
'roll_style': self.roll_style,
|
||||||
|
'exchange': self.exchange,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, dict_):
|
||||||
|
"""
|
||||||
|
Build an ContinuousFuture instance from a dict.
|
||||||
|
"""
|
||||||
|
return cls(**dict_)
|
||||||
|
|
||||||
|
def is_alive_for_session(self, session_label):
|
||||||
|
"""
|
||||||
|
Returns whether the continuous future is alive at the given dt.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
session_label: pd.Timestamp
|
||||||
|
The desired session label to check. (midnight UTC)
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
boolean: whether the continuous is alive at the given dt.
|
||||||
|
"""
|
||||||
|
cdef int64_t ref_start
|
||||||
|
cdef int64_t ref_end
|
||||||
|
|
||||||
|
ref_start = self.start_date.value
|
||||||
|
ref_end = self.end_date.value
|
||||||
|
|
||||||
|
return ref_start <= session_label.value <= ref_end
|
||||||
|
|
||||||
|
def is_exchange_open(self, dt_minute):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt_minute: pd.Timestamp (UTC, tz-aware)
|
||||||
|
The minute to check.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
boolean: whether the continuous futures's exchange is open at the
|
||||||
|
given minute.
|
||||||
|
"""
|
||||||
|
calendar = get_calendar(self.exchange)
|
||||||
|
return calendar.is_open_on_minute(dt_minute)
|
||||||
|
|
||||||
|
|
||||||
|
cdef class OrderedContracts(object):
|
||||||
|
"""
|
||||||
|
A container for aligned values of a future contract chain, in sorted order
|
||||||
|
of their occurrence.
|
||||||
|
Used to get answers about contracts in relation to their auto close
|
||||||
|
dates and start dates.
|
||||||
|
|
||||||
|
The number of contracts for a given root symbol is ~250,
|
||||||
|
which is why search by comparison over the range of contracts is
|
||||||
|
used. At this size, this is faster than using an index or np.searchsorted.
|
||||||
|
|
||||||
|
Members
|
||||||
|
-------
|
||||||
|
root_symbol : str
|
||||||
|
The root symbol of the future contract chain.
|
||||||
|
contract_sids : long[:]
|
||||||
|
The contract sids in sorted order of occurrence.
|
||||||
|
start_dates : long[:]
|
||||||
|
The start dates of the contracts in the chain.
|
||||||
|
Corresponds by index with contract_sids.
|
||||||
|
auto_close_dates : long[:]
|
||||||
|
The auto close dates of the contracts in the chain.
|
||||||
|
Corresponds by index with contract_sids.
|
||||||
|
|
||||||
|
Instances of this class are used by the simulation engine, but not
|
||||||
|
exposed to the algorithm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cdef readonly object root_symbol
|
||||||
|
cdef int _size
|
||||||
|
cdef readonly long_t[:] contract_sids
|
||||||
|
cdef readonly long_t[:] start_dates
|
||||||
|
cdef readonly long_t[:] auto_close_dates
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
object root_symbol,
|
||||||
|
long_t[:] contract_sids,
|
||||||
|
long_t[:] start_dates,
|
||||||
|
long_t[:] auto_close_dates):
|
||||||
|
self._size = len(contract_sids)
|
||||||
|
self.root_symbol = root_symbol
|
||||||
|
self.contract_sids = contract_sids
|
||||||
|
self.start_dates = start_dates
|
||||||
|
self.auto_close_dates = auto_close_dates
|
||||||
|
|
||||||
|
cpdef long contract_before_auto_close(self, long_t dt_value):
|
||||||
|
"""
|
||||||
|
Get the contract with next upcoming auto close date.
|
||||||
|
"""
|
||||||
|
cdef Py_ssize_t i, auto_close_date
|
||||||
|
for i, auto_close_date in enumerate(self.auto_close_dates):
|
||||||
|
if auto_close_date > dt_value:
|
||||||
|
break
|
||||||
|
return self.contract_sids[i]
|
||||||
|
|
||||||
|
cpdef long contract_at_offset(self, long_t sid, Py_ssize_t offset):
|
||||||
|
"""
|
||||||
|
Get the sid which is the given sid plus the offset distance.
|
||||||
|
An offset of 0 should be reflexive.
|
||||||
|
"""
|
||||||
|
cdef Py_ssize_t i
|
||||||
|
cdef long_t[:] sids
|
||||||
|
sids = self.contract_sids
|
||||||
|
for i in range(self._size):
|
||||||
|
if sid == sids[i]:
|
||||||
|
return sids[i + offset]
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#
|
||||||
|
# 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 abc import ABCMeta, abstractmethod
|
||||||
|
from six import with_metaclass
|
||||||
|
|
||||||
|
|
||||||
|
class RollFinder(with_metaclass(ABCMeta, object)):
|
||||||
|
"""
|
||||||
|
Abstract base class for calculating when futures contracts are the active
|
||||||
|
contract.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_contract_center(self, root_symbol, dt, offset):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
root_symbol : str
|
||||||
|
The root symbol for the contract chain.
|
||||||
|
dt : Timestamp
|
||||||
|
The datetime for which to retrieve the current contract.
|
||||||
|
offset : int
|
||||||
|
The offset from the primary contract.
|
||||||
|
0 is the primary, 1 is the secondary, etc.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Future
|
||||||
|
The active future contract at the given dt.
|
||||||
|
"""
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarRollFinder(RollFinder):
|
||||||
|
"""
|
||||||
|
The CalendarRollFinder calculates contract rolls based purely on the
|
||||||
|
contract's auto close date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, trading_calendar, asset_finder):
|
||||||
|
self.trading_calendar = trading_calendar
|
||||||
|
self.asset_finder = asset_finder
|
||||||
|
|
||||||
|
def get_contract_center(self, root_symbol, dt, offset):
|
||||||
|
oc = self.asset_finder.get_ordered_contracts(root_symbol)
|
||||||
|
session = self.trading_calendar.minute_to_session_label(dt)
|
||||||
|
primary_candidate = oc.contract_before_auto_close(session.value)
|
||||||
|
|
||||||
|
# Here is where a volume check would be.
|
||||||
|
primary = primary_candidate
|
||||||
|
return oc.contract_at_offset(primary, offset)
|
||||||
@@ -23,6 +23,8 @@ from six import iteritems
|
|||||||
from six.moves import reduce
|
from six.moves import reduce
|
||||||
|
|
||||||
from zipline.assets import Asset, Future, Equity
|
from zipline.assets import Asset, Future, Equity
|
||||||
|
from zipline.assets.continuous_futures import ContinuousFuture
|
||||||
|
from zipline.assets.roll_finder import CalendarRollFinder
|
||||||
from zipline.data.dispatch_bar_reader import (
|
from zipline.data.dispatch_bar_reader import (
|
||||||
AssetDispatchMinuteBarReader,
|
AssetDispatchMinuteBarReader,
|
||||||
AssetDispatchSessionBarReader
|
AssetDispatchSessionBarReader
|
||||||
@@ -54,7 +56,14 @@ from zipline.errors import (
|
|||||||
log = Logger('DataPortal')
|
log = Logger('DataPortal')
|
||||||
|
|
||||||
BASE_FIELDS = frozenset([
|
BASE_FIELDS = frozenset([
|
||||||
"open", "high", "low", "close", "volume", "price", "last_traded"
|
"open",
|
||||||
|
"high",
|
||||||
|
"low",
|
||||||
|
"close",
|
||||||
|
"volume",
|
||||||
|
"price",
|
||||||
|
"contract",
|
||||||
|
"last_traded",
|
||||||
])
|
])
|
||||||
|
|
||||||
OHLCV_FIELDS = frozenset([
|
OHLCV_FIELDS = frozenset([
|
||||||
@@ -149,6 +158,11 @@ class DataPortal(object):
|
|||||||
else:
|
else:
|
||||||
self._last_trading_session = None
|
self._last_trading_session = None
|
||||||
|
|
||||||
|
self._roll_finders = {
|
||||||
|
'calendar': CalendarRollFinder(self.trading_calendar,
|
||||||
|
self.asset_finder)
|
||||||
|
}
|
||||||
|
|
||||||
aligned_equity_minute_reader = self._ensure_reader_aligned(
|
aligned_equity_minute_reader = self._ensure_reader_aligned(
|
||||||
equity_minute_reader)
|
equity_minute_reader)
|
||||||
aligned_equity_session_reader = self._ensure_reader_aligned(
|
aligned_equity_session_reader = self._ensure_reader_aligned(
|
||||||
@@ -350,7 +364,8 @@ class DataPortal(object):
|
|||||||
# at it if it's on something like palladium and not AAPL (since our
|
# at it if it's on something like palladium and not AAPL (since our
|
||||||
# own price data always wins when dealing with assets).
|
# own price data always wins when dealing with assets).
|
||||||
|
|
||||||
return not (field in BASE_FIELDS and isinstance(asset, Asset))
|
return not (field in BASE_FIELDS and
|
||||||
|
(isinstance(asset, (Asset, ContinuousFuture))))
|
||||||
|
|
||||||
def _get_fetcher_value(self, asset, field, dt):
|
def _get_fetcher_value(self, asset, field, dt):
|
||||||
day = normalize_date(dt)
|
day = normalize_date(dt)
|
||||||
@@ -405,6 +420,8 @@ class DataPortal(object):
|
|||||||
return 0
|
return 0
|
||||||
elif field != "last_traded":
|
elif field != "last_traded":
|
||||||
return np.NaN
|
return np.NaN
|
||||||
|
elif field == "contract":
|
||||||
|
return None
|
||||||
|
|
||||||
if data_frequency == "daily":
|
if data_frequency == "daily":
|
||||||
return self._get_daily_data(asset, field, session_label)
|
return self._get_daily_data(asset, field, session_label)
|
||||||
@@ -414,6 +431,8 @@ class DataPortal(object):
|
|||||||
elif field == "price":
|
elif field == "price":
|
||||||
return self._get_minute_spot_value(asset, "close", dt,
|
return self._get_minute_spot_value(asset, "close", dt,
|
||||||
ffill=True)
|
ffill=True)
|
||||||
|
elif field == "contract":
|
||||||
|
return self._get_current_contract(asset, dt)
|
||||||
else:
|
else:
|
||||||
return self._get_minute_spot_value(asset, field, dt)
|
return self._get_minute_spot_value(asset, field, dt)
|
||||||
|
|
||||||
@@ -1219,3 +1238,10 @@ class DataPortal(object):
|
|||||||
ret = np.nan
|
ret = np.nan
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def _get_current_contract(self, continuous_future, dt):
|
||||||
|
rf = self._roll_finders[continuous_future.roll_style]
|
||||||
|
return self.asset_finder.retrieve_asset(
|
||||||
|
rf.get_contract_center(continuous_future.root_symbol,
|
||||||
|
dt,
|
||||||
|
continuous_future.offset))
|
||||||
|
|||||||
Reference in New Issue
Block a user