Files
catalyst/zipline/assets/assets.py
T
jfkirk 847b5bdc42 BUG: The 'file_name' metadata arg now overwrites 'symbol', if it is given
This is to fix the issue where 'GOOG_L's metadata lists its symbol as 'GOOG'. To resolve this, the 'file_name' is used as the canonical symbol, if it is given.
2015-06-12 16:09:19 -04:00

559 lines
19 KiB
Python

#
# Copyright 2015 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
from itertools import chain
from numbers import Integral
import numpy as np
import operator
from logbook import Logger
import pandas as pd
from pandas.tseries.tools import normalize_date
from six import with_metaclass, string_types
from zipline.errors import (
ConsumeAssetMetaDataError,
IdentifierNotFound,
InvalidAssetType,
MultipleSymbolsFound,
SidAssignmentError,
SidNotFound,
SymbolNotFound,
)
from zipline.utils import tradingcalendar
from zipline.assets._assets import (
Asset, Equity, Future
)
log = Logger('assets.py')
# Expected fields for an Asset's metadata
ASSET_FIELDS = [
'sid',
'asset_type',
'symbol',
'asset_name',
'start_date',
'end_date',
'first_traded',
'exchange',
'notice_date',
'expiration_date',
'contract_multiplier',
# The following fields are for compatibility with other systems
'file_name', # Used as symbol
'company_name', # Used as asset_name
'start_date_nano', # Used as start_date
'end_date_nano', # Used as end_date
]
class AssetFinder(object):
def __init__(self,
metadata=None,
trading_calendar=tradingcalendar,
allow_sid_assignment=True):
# Any particular instance of AssetFinder should be
# consistent throughout its lifetime, so we grab a reference
# to our cache now. That way, if the cache is refreshed later,
# our instance will continue to use the old one.
self.cache = {}
self.sym_cache = {}
self.identifier_cache = {}
self.fuzzy_match = {}
# This flag controls if the AssetFinder is allowed to generate its own
# sids. If False, metadata that does not contain a sid will raise an
# exception when building assets.
self.allow_sid_assignment = allow_sid_assignment
# The AssetFinder also holds a nested-dict of all metadata for
# reference when building Assets
self.metadata_cache = {}
if metadata:
self.consume_metadata(metadata)
self.trading_calendar = trading_calendar
self.populate_cache()
def _next_free_sid(self):
if len(self.cache) > 0:
return max(self.cache.keys()) + 1
return 0
def _assign_sid(self, identifier):
if hasattr(identifier, '__int__'):
return identifier.__int__()
if not self.allow_sid_assignment:
raise SidAssignmentError(identifier=identifier)
if isinstance(identifier, string_types):
return self._next_free_sid()
def retrieve_asset(self, sid, default_none=False):
if isinstance(sid, Asset):
return sid
asset = self.cache.get(sid)
if asset is not None:
return asset
elif default_none:
return None
else:
raise SidNotFound(sid=sid)
def retrieve_asset_by_identifier(self, identifier):
if isinstance(identifier, Asset):
return identifier
asset = self.identifier_cache.get(identifier)
if asset is not None:
return asset
else:
raise IdentifierNotFound(identifier=identifier)
@staticmethod
def _lookup_symbol_in_infos(infos, as_of_date):
"""
Search a list of symbols matching a given asset for the most recent
known symbol as of as_of_date.
Returns a pair of (Asset, bool), representing the best match we
found for as_of_date, and whether or not that match was actually
trading at as_of_date.
If no entry in infos started before as_of_date, return (None, False).
"""
# Sort entries by end_date before iterating. If asset start and end
# dates were always disjoint, then we could sort by either start or
# end_date and get the same sorting.
infos = sorted(infos, key=operator.attrgetter('end_date'))
# Find the newest asset that started before as_of_date.
candidates = [i for i in infos
if (i.start_date is None or i.start_date <= as_of_date)
and (i.end_date is None or as_of_date <= i.end_date)]
# If one SID exists for symbol, return that symbol
if len(candidates) == 1:
return candidates[0], True
# If no SID exists for symbol, return SID with the
# highest-but-not-over end_date
if len(candidates) == 0:
candidates = [i for i in infos
if i.end_date < as_of_date]
return (candidates[-1], False) if candidates else (None, False)
# If multiple SIDs exist for symbol, return latest start_date with
# end_date as a tie-breaker
if len(candidates) > 1:
best_candidate = sorted(
candidates,
key=lambda x: (x.start_date, x.end_date)
)[-1]
return best_candidate, True
def lookup_symbol_resolve_multiple(self, symbol, as_of_date=None):
"""
Return matching Asset of name symbol in database.
If multiple Assets are found and as_of_date is not set,
raises MultipleSymbolsFound.
If no Asset was active at as_of_date, and allow_expired is False
raises SymbolNotFound.
"""
if as_of_date is not None:
as_of_date = normalize_date(as_of_date)
if symbol not in self.sym_cache:
raise SymbolNotFound(symbol=symbol)
infos = self.sym_cache[symbol]
if as_of_date is None:
if len(infos) == 1:
return infos[0]
else:
raise MultipleSymbolsFound(symbol=symbol,
options=str(infos))
# Try to find symbol matching as_of_date
asset, _ = self._lookup_symbol_in_infos(infos, as_of_date)
if asset is None:
raise SymbolNotFound(symbol=symbol)
return asset
def lookup_symbol(self, symbol, as_of_date, fuzzy=None):
"""
If a fuzzy string is provided, then we try various symbols based on
the provided symbol. This is to facilitate mapping from a broker's
symbol to ours in cases where mapping to the broker's symbol loses
information. For example, if we have CMCS_A, but a broker has CMCSA,
when the broker provides CMCSA, it can also provide fuzzy='_',
so we can find a match by inserting an underscore.
"""
symbol = symbol.upper()
as_of_date = normalize_date(as_of_date)
if not fuzzy:
try:
return self.lookup_symbol_resolve_multiple(symbol, as_of_date)
except SymbolNotFound:
return None
else:
try:
return self.fuzzy_match[(symbol, fuzzy, as_of_date)]
except KeyError:
# if symbol is CMCSA and fuzzy is '_', then
# try CMCSA, then CMCS_A, then CMC_SA, etc.
for fuzzy_symbol in chain(
(symbol,),
(symbol[:i] + fuzzy + symbol[i:]
for i in range(len(symbol) - 1, 0, -1))):
infos = self.sym_cache.get(fuzzy_symbol)
if infos:
info, date_match = self._lookup_symbol_in_infos(
infos,
as_of_date,
)
if info is not None and date_match:
self.fuzzy_match[(symbol, fuzzy, as_of_date)] = \
info
return info
else:
self.fuzzy_match[(symbol, fuzzy, as_of_date)] = None
def populate_cache(self):
"""
Populates the asset cache with all values in the assets
collection.
"""
# Wipe caches before repopulating
self.cache = {}
self.sym_cache = {}
self.identifier_cache = {}
self.fuzzy_match = {}
counter = 0
for identifier, row in self.metadata_cache.items():
self.spawn_asset(identifier=identifier, **row)
counter += 1
def spawn_asset(self, identifier, **kwargs):
# Check if the sid is declared
try:
kwargs['sid']
pass
except KeyError:
# If the identifier is not a sid, assign one
kwargs['sid'] = self._assign_sid(identifier)
# Update the metadata object with the new sid
self.insert_metadata(identifier=identifier, sid=kwargs['sid'])
# If the file_name is in the kwargs, it will be used as the symbol
try:
kwargs['symbol'] = kwargs.pop('file_name')
except KeyError:
pass
# If the identifier coming in was a string and there is no defined
# symbol yet, set the symbol to the incoming identifier
try:
kwargs['symbol']
pass
except KeyError:
if isinstance(identifier, string_types):
kwargs['symbol'] = identifier
# If the company_name is in the kwargs, it may be the asset_name
try:
company_name = kwargs.pop('company_name')
try:
kwargs['asset_name']
except KeyError:
kwargs['asset_name'] = company_name
except KeyError:
pass
# If dates are given as nanos, pop them
try:
kwargs['start_date'] = kwargs.pop('start_date_nano')
except KeyError:
pass
try:
kwargs['end_date'] = kwargs.pop('end_date_nano')
except KeyError:
pass
try:
kwargs['notice_date'] = kwargs.pop('notice_date_nano')
except KeyError:
pass
try:
kwargs['expiration_date'] = kwargs.pop('expiration_date_nano')
except KeyError:
pass
# Process dates to Timestamps
try:
kwargs['start_date'] = pd.Timestamp(kwargs['start_date'], tz='UTC')
except KeyError:
pass
try:
kwargs['end_date'] = pd.Timestamp(kwargs['end_date'], tz='UTC')
except KeyError:
pass
try:
kwargs['notice_date'] = pd.Timestamp(kwargs['notice_date'],
tz='UTC')
except KeyError:
pass
try:
kwargs['expiration_date'] = pd.Timestamp(kwargs['expiration_date'],
tz='UTC')
except KeyError:
pass
# Build an Asset of the appropriate type, default to Equity
asset_type = kwargs.pop('asset_type', 'equity')
if asset_type.lower() == 'equity':
asset = Equity(**kwargs)
elif asset_type.lower() == 'future':
asset = Future(**kwargs)
else:
raise InvalidAssetType(asset_type=asset_type)
self.cache[asset.sid] = asset
self.identifier_cache[identifier] = asset
if asset.symbol is not '':
self.sym_cache.setdefault(asset.symbol, []).append(asset)
return asset
@property
def sids(self):
return self.cache.keys()
@property
def assets(self):
return self.cache.values()
def _lookup_generic_scalar(self,
asset_convertible,
as_of_date,
matches,
missing):
"""
Convert asset_convertible to an asset.
On success, append to matches.
On failure, append to missing.
"""
try:
if isinstance(asset_convertible, Asset):
matches.append(asset_convertible)
elif isinstance(asset_convertible, Integral):
result = self.retrieve_asset(int(asset_convertible))
if result is None:
raise SymbolNotFound(symbol=asset_convertible)
matches.append(result)
elif isinstance(asset_convertible, string_types):
# Throws SymbolNotFound on failure to match.
matches.append(
self.lookup_symbol_resolve_multiple(
asset_convertible,
as_of_date,
)
)
else:
raise NotAssetConvertible(
"Input was %s, not AssetConvertible."
% asset_convertible
)
except SymbolNotFound:
missing.append(asset_convertible)
return None
def lookup_generic(self,
asset_convertible_or_iterable,
as_of_date):
"""
Convert a AssetConvertible or iterable of AssetConvertibles into
a list of Asset objects.
This method exists primarily as a convenience for implementing
user-facing APIs that can handle multiple kinds of input. It should
not be used for internal code where we already know the expected types
of our inputs.
Returns a pair of objects, the first of which is the result of the
conversion, and the second of which is a list containing any values
that couldn't be resolved.
"""
matches = []
missing = []
# Interpret input as scalar.
if isinstance(asset_convertible_or_iterable, AssetConvertible):
self._lookup_generic_scalar(
asset_convertible=asset_convertible_or_iterable,
as_of_date=as_of_date,
matches=matches,
missing=missing,
)
try:
return matches[0], missing
except IndexError:
if hasattr(asset_convertible_or_iterable, '__int__'):
raise SidNotFound(sid=asset_convertible_or_iterable)
else:
raise SymbolNotFound(symbol=asset_convertible_or_iterable)
# Interpret input as iterable.
try:
iterator = iter(asset_convertible_or_iterable)
except TypeError:
raise NotAssetConvertible(
"Input was not a AssetConvertible "
"or iterable of AssetConvertible."
)
for obj in iterator:
self._lookup_generic_scalar(obj, as_of_date, matches, missing)
return matches, missing
def insert_metadata(self, identifier, **kwargs):
"""
Inserts the given metadata kwargs to the entry for the given
identifier. Matching fields in the existing entry will be overwritten.
:param identifier: The identifier for which to insert metadata
:param kwargs: The keyed metadata to insert
"""
entry = self.metadata_cache.get(identifier, {})
for key, value in kwargs.items():
# Do not accept invalid fields
if key not in ASSET_FIELDS:
continue
# Do not accept Nones
if value is None:
continue
# Do not accept empty strings
if value == '':
continue
# Do not accept nans from dataframes
if isinstance(value, float) and np.isnan(value):
continue
entry[key] = value
self.metadata_cache[identifier] = entry
def consume_identifiers(self, identifiers):
"""
Consumes the given identifiers in to the metadata cache of this
AssetFinder.
"""
for identifier in identifiers:
# Handle case where full Assets are passed in
# For example, in the creation of a DataFrameSource, the source's
# 'sid' args may be full Assets
if isinstance(identifier, Asset):
sid = identifier.sid
metadata = identifier.to_dict()
metadata['asset_type'] = identifier.__class__.__name__
self.insert_metadata(identifier=sid, **metadata)
else:
self.insert_metadata(identifier)
def consume_metadata(self, metadata):
"""
Consumes the provided metadata in to the metadata cache. The
existing values in the cache will be overwritten when there
is a conflict.
:param metadata: The metadata to be consumed
"""
# Handle dicts
if isinstance(metadata, dict):
self._insert_metadata_dict(metadata)
# Handle DataFrames
elif isinstance(metadata, pd.DataFrame):
self._insert_metadata_dataframe(metadata)
# Handle readables
elif hasattr(metadata, 'read'):
self._insert_metadata_readable(metadata)
else:
raise ConsumeAssetMetaDataError(obj=metadata)
def clear_metadata(self):
self.metadata_cache = {}
def _insert_metadata_dataframe(self, dataframe):
for identifier, row in dataframe.iterrows():
self.insert_metadata(identifier, **row)
def _insert_metadata_dict(self, dict):
for identifier, entry in dict.items():
self.insert_metadata(identifier, **entry)
def _insert_metadata_readable(self, readable):
for row in readable.read():
# Parse out the row of the readable object
metadata_dict = {}
for field in ASSET_FIELDS:
try:
row_value = row[field]
# Avoid passing placeholders
if row_value and (row_value is not 'None'):
metadata_dict[field] = row[field]
except KeyError:
continue
except IndexError:
continue
# Locate the identifier, fail if not found
if 'sid' in metadata_dict:
identifier = metadata_dict['sid']
elif 'symbol' in metadata_dict:
identifier = metadata_dict['symbol']
else:
raise ConsumeAssetMetaDataError(obj=row)
self.insert_metadata(identifier, **metadata_dict)
class AssetConvertible(with_metaclass(ABCMeta)):
"""
ABC for types that are convertible to integer-representations of
Assets.
Includes Asset, six.string_types, and Integral
"""
pass
AssetConvertible.register(Integral)
AssetConvertible.register(Asset)
# Use six.string_types for Python2/3 compatibility
for type in string_types:
AssetConvertible.register(type)
class NotAssetConvertible(ValueError):
pass