diff --git a/catalyst/assets/_assets.pyx b/catalyst/assets/_assets.pyx index a06e4239..b6ce2b39 100644 --- a/catalyst/assets/_assets.pyx +++ b/catalyst/assets/_assets.pyx @@ -20,15 +20,17 @@ Cythonized Asset 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, +Py_EQ, +Py_NE, +Py_GE, +Py_LE, +Py_GT, +Py_LT, ) from cpython cimport bool +import pandas as pd +from datetime import timedelta import numpy as np from numpy cimport int64_t import warnings @@ -36,15 +38,12 @@ cimport numpy as np from catalyst.utils.calendars import get_calendar - # IMPORTANT NOTE: You must change this template if you change # Asset.__reduce__, or else we'll attempt to unpickle an old version of this # class CACHE_FILE_TEMPLATE = '/tmp/.%s-%s.v7.cache' - cdef class Asset: - cdef readonly int sid # Cached hash of self.sid cdef int sid_hash @@ -73,8 +72,8 @@ cdef class Asset: }) def __init__(self, - int sid, # sid is required - object exchange, # exchange is required + int sid, # sid is required + object exchange, # exchange is required object symbol="", object asset_name="", object start_date=None, @@ -230,9 +229,7 @@ cdef class Asset: calendar = get_calendar(self.exchange) return calendar.is_open_on_minute(dt_minute) - cdef class Equity(Asset): - def __repr__(self): attrs = ('symbol', 'asset_name', 'exchange', 'start_date', 'end_date', 'first_traded', 'auto_close_date', @@ -250,8 +247,8 @@ cdef class Equity(Asset): """ def __get__(self): warnings.warn("The security_start_date property will soon be " - "retired. Please use the start_date property instead.", - DeprecationWarning) + "retired. Please use the start_date property instead.", + DeprecationWarning) return self.start_date property security_end_date: @@ -261,8 +258,8 @@ cdef class Equity(Asset): """ def __get__(self): warnings.warn("The security_end_date property will soon be " - "retired. Please use the end_date property instead.", - DeprecationWarning) + "retired. Please use the end_date property instead.", + DeprecationWarning) return self.end_date property security_name: @@ -272,13 +269,11 @@ cdef class Equity(Asset): """ def __get__(self): warnings.warn("The security_name property will soon be " - "retired. Please use the asset_name property instead.", - DeprecationWarning) + "retired. Please use the asset_name property instead.", + DeprecationWarning) return self.asset_name - cdef class Future(Asset): - cdef readonly object root_symbol cdef readonly object notice_date cdef readonly object expiration_date @@ -303,8 +298,8 @@ cdef class Future(Asset): }) def __init__(self, - int sid, # sid is required - object exchange, # exchange is required + int sid, # sid is required + object exchange, # exchange is required object symbol="", object root_symbol="", object asset_name="", @@ -388,6 +383,113 @@ cdef class Future(Asset): super_dict['multiplier'] = self.multiplier return super_dict +cdef class TradingPair(Asset): + cdef readonly float leverage + + _kwargnames = frozenset({ + 'sid', + 'symbol', + 'asset_name', + 'start_date', + 'end_date', + 'first_traded', + 'auto_close_date', + 'exchange', + 'exchange_full', + 'leverage', + }) + def __init__(self, + object symbol, + object exchange, + object start_date=None, + object asset_name=None, + int sid=0, + float leverage=1.0, + object end_date=None, + object first_traded=None, + object auto_close_date=None, + object exchange_full=None): + """ + Replicates the Asset constructor with some built-in conventions + and a new 'leverage' attribute. + + Symbol + ------ + Catalyst defines its own set of "universal" symbols to reference + trading pairs across exchanges. This is required because exchanges + are not adhering to a universal symbolism. For example, Bitfinex + uses the BTC symbol for Bitcon while Kraken uses XBT. In addition, + pairs are sometimes presented differently. For example, Bitfinex + puts the market currency before the base currency without a + separator, Bittrex puts the base currency first and uses a dash + seperator. + + Here is the Catalyst convention: [Market Currency]_[Base Currency] + For example: btc_usd, eth_btc, neo_eth, ltc_eur. + + The symbol for each currency (e.g. btc, eth, ltc) is generally + aligned with the Bittrex exchange. + + Sid + --- + The sid of each asset is calculated based on a numeric hash of the + universal symbol. This simple approach avoids maintaining a mapping + of sids. + + Leverage + -------- + In contrast with equities, crypto exchanges generally assign + leverage values to specific trading pairs. Pairs with the + highest volume and market cap generally benefit from high leverage. + New currencies from ICO generally cannot be leveraged. + + The leverage value is either None or and integer. + + Leverage allows you to open a larger position with a smaller amount + of funds. For example, if you open a $5,000 position in BTC/USD + with 5:1 leverage, only one-fifth of this amount, or $1000, will be + tied to the position from your balance. Your remaining balance will + be available for opening more positions. If you open this same + position with 2:1 leverage, $2,500 of your balance will be tied to + the position. If you open with 1:1 leverage, $5,000 of your balance + will be tied to the position. + + :param symbol: + :param exchange: + :param start_date: + :param asset_name: + :param sid: + :param leverage: + :param end_date: + :param first_traded: + :param auto_close_date: + :param exchange_full: + """ + + asset_name = ' / '.join(symbol.split('_')).upper() \ + if asset_name is None else asset_name + + start_date = pd.Timestamp.utcnow() \ + if start_date is None else start_date + + end_date = start_date + timedelta(days=1) \ + if end_date is None else end_date + + sid = abs(hash(symbol)) % (10 ** 4) \ + if sid == 0 else sid + + super().__init__( + sid, + exchange, + symbol=symbol, + asset_name=asset_name, + start_date=start_date, + end_date=end_date, + first_traded=first_traded, + auto_close_date=auto_close_date, + exchange_full=exchange_full, + ) + self.leverage = leverage def make_asset_array(int size, Asset asset): cdef np.ndarray out = np.empty([size], dtype=object) diff --git a/catalyst/exchange/asset_finder_exchange.py b/catalyst/exchange/asset_finder_exchange.py index dc3d00b1..2239f8a8 100644 --- a/catalyst/exchange/asset_finder_exchange.py +++ b/catalyst/exchange/asset_finder_exchange.py @@ -81,7 +81,7 @@ class AssetFinderExchange(object): there are multiple candidates for the given ``symbol`` on the ``as_of_date``. """ - log.info('looking up symbol: {}'.format(symbol)) + log.debug('looking up symbol: {}'.format(symbol)) if symbol in self._asset_cache: return self._asset_cache[symbol] diff --git a/catalyst/exchange/bitfinex/bitfinex.py b/catalyst/exchange/bitfinex/bitfinex.py index f1339fd0..b46fe11f 100644 --- a/catalyst/exchange/bitfinex/bitfinex.py +++ b/catalyst/exchange/bitfinex/bitfinex.py @@ -10,7 +10,7 @@ import pandas as pd import pytz import requests import six -from catalyst.assets._assets import Asset +from catalyst.assets._assets import TradingPair from logbook import Logger # from websocket import create_connection @@ -300,7 +300,7 @@ class Bitfinex(Exchange): ) # Making sure that assets are iterable - asset_list = [assets] if isinstance(assets, Asset) else assets + asset_list = [assets] if isinstance(assets, TradingPair) else assets ohlc_map = dict() for asset in asset_list: symbol = self._get_v2_symbol(asset) @@ -357,7 +357,7 @@ class Bitfinex(Exchange): ohlc_map[asset] = ohlc return ohlc_map[assets] \ - if isinstance(assets, Asset) else ohlc_map + if isinstance(assets, TradingPair) else ohlc_map def order(self, asset, amount, limit_price, stop_price, style): """Place an order. diff --git a/catalyst/exchange/bittrex/bittrex.py b/catalyst/exchange/bittrex/bittrex.py index f443bb9e..5f8a1b17 100644 --- a/catalyst/exchange/bittrex/bittrex.py +++ b/catalyst/exchange/bittrex/bittrex.py @@ -7,7 +7,7 @@ import pandas as pd from catalyst.exchange.exchange import Exchange from catalyst.exchange.bittrex.bittrex_api import Bittrex_api -from catalyst.assets._assets import Asset +from catalyst.assets._assets import TradingPair log = Logger('Bittrex') @@ -123,7 +123,7 @@ class Bittrex(Exchange): ) # Making sure that assets are iterable - asset_list = [assets] if isinstance(assets, Asset) else assets + asset_list = [assets] if isinstance(assets, TradingPair) else assets ohlc_map = dict() for asset in asset_list: url = '{url}/pub/market/GetTicks?marketName={symbol}' \ @@ -169,7 +169,7 @@ class Bittrex(Exchange): ohlc_map[asset] = ohlc_bars return ohlc_map[assets] \ - if isinstance(assets, Asset) else ohlc_map + if isinstance(assets, TradingPair) else ohlc_map def tickers(self): log.info('retrieving tickers') diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 5e9b631b..c6a7f308 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -7,7 +7,7 @@ from datetime import timedelta import numpy as np import pandas as pd -from catalyst.assets._assets import Asset +from catalyst.assets._assets import TradingPair from logbook import Logger from catalyst.data.data_portal import BASE_FIELDS @@ -138,19 +138,13 @@ class Exchange: symbol_map = self.fetch_symbol_map() for exchange_symbol in symbol_map: asset = symbol_map[exchange_symbol] - symbol = asset['symbol'] - asset_name = ' / '.join(symbol.split('_')).upper() - - asset_obj = Asset( - symbol=symbol, - asset_name=asset_name, - sid=abs(hash(symbol)) % (10 ** 4), + trading_pair = TradingPair( + symbol=asset['symbol'], exchange=self.name, - start_date=pd.to_datetime(asset['start_date'], utc=True), - end_date=pd.Timestamp.utcnow() + timedelta(minutes=300000), + start_date=pd.to_datetime(asset['start_date']), ) - self.assets[exchange_symbol] = asset_obj + self.assets[exchange_symbol] = trading_pair def check_open_orders(self): """ diff --git a/setup.py b/setup.py index a3f23296..a3243545 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ class LazyBuildExtCommandClass(dict): Lazy command class that defers operations requiring Cython and numpy until they've actually been downloaded and installed by setup_requires. """ + def __contains__(self, key): return ( key == 'build_ext' @@ -62,6 +63,7 @@ class LazyBuildExtCommandClass(dict): Custom build_ext command that lazily adds numpy's include_dir to extensions. """ + def build_extensions(self): """ Lazily append numpy's include directory to Extension includes. @@ -75,6 +77,7 @@ class LazyBuildExtCommandClass(dict): ext.include_dirs.append(numpy_incl) super(build_ext, self).build_extensions() + return build_ext @@ -100,7 +103,8 @@ ext_modules = [ window_specialization('label'), Extension('catalyst.lib.rank', ['catalyst/lib/rank.pyx']), Extension('catalyst.data._equities', ['catalyst/data/_equities.pyx']), - Extension('catalyst.data._adjustments', ['catalyst/data/_adjustments.pyx']), + Extension('catalyst.data._adjustments', + ['catalyst/data/_adjustments.pyx']), Extension('catalyst._protocol', ['catalyst/_protocol.pyx']), Extension('catalyst.gens.sim_engine', ['catalyst/gens/sim_engine.pyx']), Extension( @@ -117,7 +121,6 @@ ext_modules = [ ), ] - STR_TO_CMP = { '<': lt, '<=': le, @@ -264,6 +267,7 @@ def setup_requirements(requirements_path, module_names, strict_bounds, ) return module_lines + conda_build = os.path.basename(sys.argv[0]) in ('conda-build', # unix 'conda-build-script.py') # win @@ -295,7 +299,7 @@ setup( ext_modules=ext_modules, include_package_data=True, package_data={root.replace(os.sep, '.'): - ['*.pyi', '*.pyx', '*.pxi', '*.pxd'] + ['*.pyi', '*.pyx', '*.pxi', '*.pxd'] for root, dirnames, filenames in os.walk('catalyst') if '__pycache__' not in root}, license='Apache 2.0', @@ -314,7 +318,6 @@ setup( 'Topic :: System :: Distributed Computing', ], install_requires=install_requires(conda_format=conda_build), - extras_require=extras_requires(conda_format=conda_build, - install_requires=['six']), + extras_require=extras_requires(conda_format=conda_build), **conditional_arguments )