From 606148e19c2a14a31653acc34d7099c9a50dae3b Mon Sep 17 00:00:00 2001 From: fredfortier Date: Tue, 28 Nov 2017 13:42:58 -0500 Subject: [PATCH 01/13] DOC: Integrating with ccxt --- catalyst/exchange/ccxt/__init__.py | 0 catalyst/exchange/ccxt/ccxt_exchange.py | 121 ++++++++++++++++++++++++ etc/requirements.txt | 3 + tests/exchange/test_ccxt.py | 101 ++++++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 catalyst/exchange/ccxt/__init__.py create mode 100644 catalyst/exchange/ccxt/ccxt_exchange.py create mode 100644 tests/exchange/test_ccxt.py diff --git a/catalyst/exchange/ccxt/__init__.py b/catalyst/exchange/ccxt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py new file mode 100644 index 00000000..86673a1c --- /dev/null +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -0,0 +1,121 @@ +import re +from collections import defaultdict + +import ccxt +import pandas as pd +from logbook import Logger + +from catalyst.constants import LOG_LEVEL +from catalyst.exchange.exchange import Exchange +from catalyst.exchange.exchange_bundle import ExchangeBundle +from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError + +log = Logger('CCXT', level=LOG_LEVEL) + + +class CCXT(Exchange): + def __init__(self, exchange_name, key, secret, base_currency, + portfolio=None): + log.debug('available exchanges:\n{}'.format(ccxt.exchanges)) + self.api = ccxt.poloniex({ + 'apiKey': key, + 'secret': secret, + }) + markets = self.api.load_markets() + log.debug('the markets:\n{}'.format(markets)) + + self.name = exchange_name + self.assets = {} + self.load_assets() + self.base_currency = base_currency + self._portfolio = portfolio + self.transactions = defaultdict(list) + + self.num_candles_limit = 2000 + self.max_requests_per_minute = 60 + self.request_cpt = dict() + + self.bundle = ExchangeBundle(self.name) + + def account(self): + return None + + def time_skew(self): + return None + + def get_symbol(self, asset): + parts = asset.symbol.split('_') + return '{}/{}'.format(parts[0].upper(), parts[1].upper()) + + def get_timeframe(self, freq): + freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I) + if freq_match: + candle_size = int(freq_match.group(1)) \ + if freq_match.group(1) else 1 + + unit = freq_match.group(2) + + else: + raise InvalidHistoryFrequencyError(frequency=freq) + + if unit.lower() == 'd': + timeframe = '{}d'.format(candle_size) + + elif unit.lower() == 'm' or unit == 'T': + timeframe = '{}m'.format(candle_size) + + elif unit.lower() == 'h' or unit == 'T': + timeframe = '{}h'.format(candle_size) + + return timeframe + + def get_candles(self, freq, assets, bar_count=None, start_dt=None, + end_dt=None): + symbols = self.get_symbols(assets) + timeframe = self.get_timeframe(freq) + delta = start_dt - pd.to_datetime('1970-1-1', utc=True) + ms = int(delta.total_seconds()) * 1000 + + ohlcvs = self.api.fetch_ohlcv( + symbol=symbols[0], + timeframe=timeframe, + since=ms, + limit=bar_count, + params={} + ) + + candles = [] + for ohlcv in ohlcvs: + candles.append(dict( + last_traded=pd.to_datetime(ohlcv[0], unit='ms', utc=True), + open=ohlcv[1], + high=ohlcv[2], + low=ohlcv[3], + close=ohlcv[4], + volume=ohlcv[5] + )) + return candles + + def get_balances(self): + return None + + def create_order(self, asset, amount, is_buy, style): + return None + + def get_open_orders(self, asset): + return None + + def get_order(self, order_id): + return None + + def cancel_order(self, order_param): + return None + + def tickers(self, assets): + return None + + def get_account(self): + return None + + def get_orderbook(self, asset, order_type, limit): + return None diff --git a/etc/requirements.txt b/etc/requirements.txt index aaa129ca..dee52a34 100644 --- a/etc/requirements.txt +++ b/etc/requirements.txt @@ -80,3 +80,6 @@ empyrical==0.2.1 tables==3.3.0 +#Catalyst dependencies +ccxt==1.10.251 + diff --git a/tests/exchange/test_ccxt.py b/tests/exchange/test_ccxt.py new file mode 100644 index 00000000..7fd0dcff --- /dev/null +++ b/tests/exchange/test_ccxt.py @@ -0,0 +1,101 @@ +import os +import tempfile + +import pandas as pd +from catalyst.exchange.ccxt.ccxt_exchange import CCXT +from catalyst.finance.order import Order +from base import BaseExchangeTestCase +from logbook import Logger +from catalyst.exchange.exchange_utils import get_exchange_auth +from catalyst.utils.paths import ensure_directory + +log = Logger('test_ccxt') + + +class TestCCXT(BaseExchangeTestCase): + @classmethod + def setup(self): + exchange_name = 'poloniex' + auth = get_exchange_auth(exchange_name) + self.exchange = CCXT( + exchange_name=exchange_name, + key=auth['key'], + secret=auth['secret'], + base_currency=None, + portfolio=None + ) + + def test_order(self): + log.info('creating order') + asset = self.exchange.get_asset('neo_btc') + order_id = self.exchange.order( + asset=asset, + limit_price=0.0005, + amount=1, + ) + log.info('order created {}'.format(order_id)) + assert order_id is not None + pass + + def test_open_orders(self): + log.info('retrieving open orders') + asset = self.exchange.get_asset('neo_btc') + orders = self.exchange.get_open_orders(asset) + pass + + def test_get_order(self): + log.info('retrieving order') + order = self.exchange.get_order( + u'2c584020-9caf-4af5-bde0-332c0bba17e2') + assert isinstance(order, Order) + pass + + def test_cancel_order(self, ): + log.info('cancel order') + self.exchange.cancel_order(u'dc7bcca2-5219-4145-8848-8a593d2a72f9') + pass + + def test_get_candles(self): + log.info('retrieving candles') + candles = self.exchange.get_candles( + freq='5T', + assets=[self.exchange.get_asset('eth_btc')], + bar_count=200, + start_dt=pd.to_datetime('2017-01-01', utc=True) + ) + + df = pd.DataFrame(candles) + df.set_index('last_traded', drop=True, inplace=True) + + folder = os.path.join( + tempfile.gettempdir(), 'catalyst', self.exchange.name, 'eth_btc' + ) + ensure_directory(folder) + + path = os.path.join(folder, 'output.csv') + df.to_csv(path) + pass + + def test_tickers(self): + log.info('retrieving tickers') + tickers = self.exchange.tickers([ + self.exchange.get_asset('eth_btc'), + self.exchange.get_asset('etc_btc') + ]) + assert len(tickers) == 2 + pass + + def test_get_balances(self): + log.info('testing wallet balances') + balances = self.exchange.get_balances() + pass + + def test_get_account(self): + log.info('testing account data') + pass + + def test_orderbook(self): + log.info('testing order book for bittrex') + asset = self.exchange.get_asset('eth_btc') + orderbook = self.exchange.get_orderbook(asset) + pass From daeccaed364737dece28e48edd9f502ee650da8c Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Tue, 28 Nov 2017 16:44:13 -0700 Subject: [PATCH 02/13] DOC: video - live trading --- docs/source/videos.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/source/videos.rst b/docs/source/videos.rst index d93c09c5..90b53f19 100644 --- a/docs/source/videos.rst +++ b/docs/source/videos.rst @@ -32,7 +32,9 @@ Where things don't: Backtesting a Strategy ---------------------- -This algorithm is based on a simple momentum strategy. When the cryptoasset +This is the first video of a two-part series on using Catalyst for algorithmic +trading. This video implements a simple momentum strategy based on +`mean reversion `_: when the cryptoasset goes up quickly, we’re going to buy; when it goes down quickly, we’re going to sell. Hopefully, we’ll ride the waves. @@ -40,3 +42,17 @@ sell. Hopefully, we’ll ride the waves. +| +| +Live Trading a Strategy +----------------------- + +This is the second part of the two-part series on using Catalyst for algorithmic +trading. Having backtested `our strategy `_ +in the previous video, we now take it to trade live against the Bittrex exchange. + +.. raw:: html + + +| +| \ No newline at end of file From c8eaa11f801cbd9a07adcd1da13c6517b818fa14 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Wed, 29 Nov 2017 09:37:46 -0700 Subject: [PATCH 03/13] DOC: added portfolio_optimization to documented examples --- docs/source/example-algos.rst | 152 ++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/docs/source/example-algos.rst b/docs/source/example-algos.rst index 002dc46b..550d8b58 100644 --- a/docs/source/example-algos.rst +++ b/docs/source/example-algos.rst @@ -31,6 +31,13 @@ Overview `two-part video tutorial `_ to show how to get started in backtesting and live trading with Catalyst. +- :ref:`Portfolio Optimization `: Use this code to + execute a portfolio optimization model. This strategy will select the + portfolio with the maximum Sharpe Ratio. The parameters are set to use 180 + days of historical data and rebalance every 30 days. This code was used in + writting the following article: + `Markowitz Portfolio Optimization for Cryptocurrencies `_. + .. _buy_btc_simple: @@ -746,4 +753,149 @@ implemented after the video was recorded, which executes the orders at slighlty different prices, but resulting in significant changes in performance of our strategy. +.. _portfolio_optimization: + +Portfolio Optimization +~~~~~~~~~~~~~~~~~~~~~~ + +Use this code to execute a portfolio optimization model. This strategy will +select the portfolio with the maximum Sharpe Ratio. The parameters are set to +use 180 days of historical data and rebalance every 30 days. This code was used +in writting the following article: +`Markowitz Portfolio Optimization for Cryptocurrencies `_. + +.. code-block:: python + + ''' + You can run this code using the Python interpreter: + + $ python portfolio_optimization.py + ''' + + from __future__ import division + import os + import pytz + import numpy as np + import pandas as pd + from scipy.optimize import minimize + import matplotlib.pyplot as plt + from datetime import datetime + + from catalyst.api import record, symbol, symbols, order_target_percent + from catalyst.utils.run_algo import run_algorithm + + np.set_printoptions(threshold='nan', suppress=True) + + + def initialize(context): + # Portfolio assets list + context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt', + 'xmr_usdt') + context.nassets = len(context.assets) + # Set the time window that will be used to compute expected return + # and asset correlations + context.window = 180 + # Set the number of days between each portfolio rebalancing + context.rebalance_period = 30 + context.i = 0 + + + def handle_data(context, data): + # Only rebalance at the beggining of the algorithm execution and + # every multiple of the rebalance period + if context.i == 0 or context.i%context.rebalance_period == 0: + n = context.window + prices = data.history(context.assets, fields='price', + bar_count=n+1, frequency='1d') + pr = np.asmatrix(prices) + t_prices = prices.iloc[1:n+1] + t_val = t_prices.values + tminus_prices = prices.iloc[0:n] + tminus_val = tminus_prices.values + # Compute daily returns (r) + r = np.asmatrix(t_val/tminus_val-1) + # Compute the expected returns of each asset with the average + # daily return for the selected time window + m = np.asmatrix(np.mean(r, axis=0)) + # ### + stds = np.std(r, axis=0) + # Compute excess returns matrix (xr) + xr = r - m + # Matrix algebra to get variance-covariance matrix + cov_m = np.dot(np.transpose(xr),xr)/n + # Compute asset correlation matrix (informative only) + corr_m = cov_m/np.dot(np.transpose(stds),stds) + + # Define portfolio optimization parameters + n_portfolios = 50000 + results_array = np.zeros((3+context.nassets,n_portfolios)) + for p in xrange(n_portfolios): + weights = np.random.random(context.nassets) + weights /= np.sum(weights) + w = np.asmatrix(weights) + p_r = np.sum(np.dot(w,np.transpose(m)))*365 + p_std = np.sqrt(np.dot(np.dot(w,cov_m),np.transpose(w)))*np.sqrt(365) + + #store results in results array + results_array[0,p] = p_r + results_array[1,p] = p_std + #store Sharpe Ratio (return / volatility) - risk free rate element + #excluded for simplicity + results_array[2,p] = results_array[0,p] / results_array[1,p] + i = 0 + for iw in weights: + results_array[3+i,p] = weights[i] + i += 1 + + #convert results array to Pandas DataFrame + results_frame = pd.DataFrame(np.transpose(results_array), + columns=['r','stdev','sharpe']+context.assets) + #locate position of portfolio with highest Sharpe Ratio + max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()] + #locate positon of portfolio with minimum standard deviation + min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()] + + #order optimal weights for each asset + for asset in context.assets: + if data.can_trade(asset): + order_target_percent(asset, max_sharpe_port[asset]) + + #create scatter plot coloured by Sharpe Ratio + plt.scatter(results_frame.stdev,results_frame.r,c=results_frame.sharpe,cmap='RdYlGn') + plt.xlabel('Volatility') + plt.ylabel('Returns') + plt.colorbar() + #plot red star to highlight position of portfolio with highest Sharpe Ratio + plt.scatter(max_sharpe_port[1],max_sharpe_port[0],marker='o',color='b',s=200) + #plot green star to highlight position of minimum variance portfolio + plt.show() + print(max_sharpe_port) + record(pr=pr,r=r, m=m, stds=stds ,max_sharpe_port=max_sharpe_port, corr_m=corr_m) + context.i += 1 + + + def analyze(context=None, results=None): + # Form DataFrame with selected data + data = results[['pr','r','m','stds','max_sharpe_port','corr_m','portfolio_value']] + + # Save results in CSV file + filename = os.path.splitext(os.path.basename(__file__))[0] + data.to_csv(filename + '.csv') + + + # Bitcoin data is available from 2015-3-2. Dates vary for other tokens. + start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc) + end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc) + results = run_algorithm(initialize=initialize, + handle_data=handle_data, + analyze=analyze, + start=start, + end=end, + exchange_name='poloniex', + capital_base=100000, ) + +.. image:: https://cdn-images-1.medium.com/max/1600/0*EjjiKZHlYF3sn7yQ. + :align: center + + From 4eb8a6eb0f9f59632bdfafa1aed806aa0ca03633 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Wed, 29 Nov 2017 22:31:44 -0500 Subject: [PATCH 04/13] BLD: populating assets with help from CCXT --- catalyst/assets/_assets.pyx | 36 +++++++++++--- catalyst/examples/mean_reversion_simple.py | 8 ++-- catalyst/exchange/ccxt/ccxt_exchange.py | 56 +++++++++++++++++++++- catalyst/exchange/exchange_utils.py | 19 ++++++++ tests/exchange/test_ccxt.py | 3 ++ 5 files changed, 110 insertions(+), 12 deletions(-) diff --git a/catalyst/assets/_assets.pyx b/catalyst/assets/_assets.pyx index 3537493f..e78e3af1 100644 --- a/catalyst/assets/_assets.pyx +++ b/catalyst/assets/_assets.pyx @@ -401,6 +401,10 @@ cdef class TradingPair(Asset): cdef readonly object end_daily cdef readonly object end_minute cdef readonly object exchange_symbol + cdef readonly float maker + cdef readonly float taker + cdef readonly int trading_state + cdef readonly object data_source _kwargnames = frozenset({ 'sid', @@ -418,7 +422,11 @@ cdef class TradingPair(Asset): 'end_daily', 'end_minute', 'exchange_symbol', - 'min_trade_size' + 'min_trade_size', + 'maker', + 'taker', + 'trading_state', + 'data_source' }) def __init__(self, object symbol, @@ -434,10 +442,14 @@ cdef class TradingPair(Asset): object first_traded=None, object auto_close_date=None, object exchange_full=None, - object min_trade_size=None): + float min_trade_size=0.000001, + float maker=0.0015, + float taker=0.0025, + int trading_state=0, + object data_source='catalyst'): """ Replicates the Asset constructor with some built-in conventions - and a new 'leverage' attribute. + and adds properties for leverage and fees. Symbol ------ @@ -469,8 +481,6 @@ cdef class TradingPair(Asset): 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 @@ -480,6 +490,11 @@ cdef class TradingPair(Asset): the position. If you open with 1:1 leverage, $5,000 of your balance will be tied to the position. + Fees + ---- + Exchanges generally charge a taker (taking from the order book) or + maker (adding to the order book) fee. + :param symbol: :param exchange: :param start_date: @@ -494,6 +509,9 @@ cdef class TradingPair(Asset): :param auto_close_date: :param exchange_full: :param min_trade_size: + :param maker: + :param taker: + :param data_source """ symbol = symbol.lower() @@ -527,13 +545,17 @@ cdef class TradingPair(Asset): first_traded=first_traded, auto_close_date=auto_close_date, exchange_full=exchange_full, - min_trade_size=min_trade_size + min_trade_size=min_trade_size, ) + self.maker = maker + self.taker = taker self.leverage = leverage self.end_daily = end_daily self.end_minute = end_minute self.exchange_symbol = exchange_symbol + self.trading_state = trading_state + self.data_source = data_source def __repr__(self): return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \ @@ -579,7 +601,7 @@ cdef class TradingPair(Asset): boolean: whether the asset's exchange is open at the given minute. """ #TODO: consider implementing to spot holds - return True + return self.trading_state > 0 cpdef __reduce__(self): """ diff --git a/catalyst/examples/mean_reversion_simple.py b/catalyst/examples/mean_reversion_simple.py index 6d4922e1..1927f746 100644 --- a/catalyst/examples/mean_reversion_simple.py +++ b/catalyst/examples/mean_reversion_simple.py @@ -36,8 +36,8 @@ def initialize(context): context.base_price = None context.current_day = None - context.RSI_OVERSOLD = 50 - context.RSI_OVERBOUGHT = 80 + context.RSI_OVERSOLD = 25 + context.RSI_OVERBOUGHT = 82 context.CANDLE_SIZE = '5T' context.start_time = time.time() @@ -247,14 +247,14 @@ if __name__ == '__main__': out = os.path.join(folder, '{}.p'.format(timestr)) # catalyst run -f catalyst/examples/mean_reversion_simple.py -x poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 run_algorithm( - capital_base=10000, + capital_base=0.5, data_frequency='minute', initialize=initialize, handle_data=handle_data, analyze=analyze, exchange_name='bitfinex', algo_namespace=NAMESPACE, - base_currency='usd', + base_currency='eth', start=pd.to_datetime('2017-10-01', utc=True), end=pd.to_datetime('2017-11-10', utc=True), output=out diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index 86673a1c..00dd2970 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -3,12 +3,15 @@ from collections import defaultdict import ccxt import pandas as pd +from catalyst.assets._assets import TradingPair from logbook import Logger from catalyst.constants import LOG_LEVEL from catalyst.exchange.exchange import Exchange from catalyst.exchange.exchange_bundle import ExchangeBundle -from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError +from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ + ExchangeSymbolsNotFound +from catalyst.exchange.exchange_utils import mixin_market_params log = Logger('CCXT', level=LOG_LEVEL) @@ -96,6 +99,57 @@ class CCXT(Exchange): )) return candles + def load_assets(self, is_local=False): + markets = self.api.fetch_markets() + try: + symbol_map = self.fetch_symbol_map(is_local) + except ExchangeSymbolsNotFound: + return None + + data_source = 'local' if is_local else 'catalyst' + for market in markets: + asset = symbol_map[market['id']] \ + if market['id'] in markets else None + + params = dict(exchange=self.name, data_source=data_source) + mixin_market_params(params, market) + + if asset: + params['symbol'] = asset['symbol'] + + params['start_date'] = pd.to_datetime( + asset['start_date'], utc=True + ) if 'start_date' in asset else None + + params['end_date'] = pd.to_datetime( + asset['end_date'], utc=True + ) if 'end_date' in asset else None + + params['leverage'] = asset['leverage'] \ + if 'leverage' in asset else 1.0 + + params['asset_name'] = asset['asset_name'] \ + if 'asset_name' in asset else None + + params['end_daily'] = pd.to_datetime( + asset['end_daily'], utc=True + ) if 'end_daily' in asset and asset['end_daily'] != 'N/A' \ + else None + + params['end_minute'] = pd.to_datetime( + asset['end_minute'], utc=True + ) if 'end_minute' in asset and asset['end_minute'] != 'N/A' \ + else None + + else: + params['symbol'] = market['id'] + + trading_pair = TradingPair(**params) + if is_local: + self.local_assets[market['id']] = trading_pair + else: + self.assets[market['id']] = trading_pair + def get_balances(self): return None diff --git a/catalyst/exchange/exchange_utils.py b/catalyst/exchange/exchange_utils.py index 678d9945..83dfae41 100644 --- a/catalyst/exchange/exchange_utils.py +++ b/catalyst/exchange/exchange_utils.py @@ -571,3 +571,22 @@ def resample_history_df(df, freq, field): resampled_df = df.resample(freq).agg(agg) return resampled_df + + +def mixin_market_params(params, market): + """ + Applies a CCXT market dict to parameters of TradingPair init. + + Parameters + ---------- + params: dict[Object] + market: dict[Object] + + Returns + ------- + + """ + params['min_trade_size'] = market['lot'] + params['maker'] = market['maker'] + params['taker'] = market['taker'] + params['trading_state'] = 1 if int(market['info']['isFrozen']) == 0 else 0 diff --git a/tests/exchange/test_ccxt.py b/tests/exchange/test_ccxt.py index 7fd0dcff..f920fa39 100644 --- a/tests/exchange/test_ccxt.py +++ b/tests/exchange/test_ccxt.py @@ -99,3 +99,6 @@ class TestCCXT(BaseExchangeTestCase): asset = self.exchange.get_asset('eth_btc') orderbook = self.exchange.get_orderbook(asset) pass + + def test_get_fees(self): + pass From 5660247da2b4043e56a337a559bc5a3dcb8e45eb Mon Sep 17 00:00:00 2001 From: fredfortier Date: Thu, 30 Nov 2017 17:08:13 -0500 Subject: [PATCH 05/13] BLD: tested all public APIs with CCXT --- catalyst/assets/_assets.pyx | 14 +-- catalyst/exchange/ccxt/ccxt_exchange.py | 134 +++++++++++++++++++----- catalyst/exchange/exchange.py | 15 +-- catalyst/exchange/exchange_errors.py | 4 + catalyst/exchange/exchange_utils.py | 29 ++++- tests/exchange/test_ccxt.py | 4 +- 6 files changed, 156 insertions(+), 44 deletions(-) diff --git a/catalyst/assets/_assets.pyx b/catalyst/assets/_assets.pyx index e78e3af1..feb41710 100644 --- a/catalyst/assets/_assets.pyx +++ b/catalyst/assets/_assets.pyx @@ -396,7 +396,7 @@ cdef class Future(Asset): cdef class TradingPair(Asset): cdef readonly float leverage - cdef readonly object market_currency + cdef readonly object quote_currency cdef readonly object base_currency cdef readonly object end_daily cdef readonly object end_minute @@ -417,7 +417,7 @@ cdef class TradingPair(Asset): 'exchange', 'exchange_full', 'leverage', - 'market_currency', + 'quote_currency', 'base_currency', 'end_daily', 'end_minute', @@ -442,7 +442,7 @@ cdef class TradingPair(Asset): object first_traded=None, object auto_close_date=None, object exchange_full=None, - float min_trade_size=0.000001, + float min_trade_size=0.0001, float maker=0.0015, float taker=0.0025, int trading_state=0, @@ -516,7 +516,7 @@ cdef class TradingPair(Asset): symbol = symbol.lower() try: - self.market_currency, self.base_currency = symbol.split('_') + self.base_currency,self.quote_currency = symbol.split('_') except Exception as e: raise InvalidSymbolError(symbol=symbol, error=e) @@ -530,7 +530,7 @@ cdef class TradingPair(Asset): asset_name = ' / '.join(symbol.split('_')).upper() if start_date is None: - start_date = pd.Timestamp.utcnow() + start_date = pd.to_datetime('2009-1-1', utc=True) if end_date is None: end_date = pd.Timestamp.utcnow() + timedelta(days=365) @@ -560,8 +560,8 @@ cdef class TradingPair(Asset): def __repr__(self): return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \ 'Introduced On: {start_date}, ' \ - 'Market Currency: {market_currency}, ' \ 'Base Currency: {base_currency}, ' \ + 'Quote Currency: {quote_currency}, ' \ 'Exchange Leverage: {leverage}, ' \ 'Minimum Trade Size: {min_trade_size} ' \ 'Last daily ingestion: {end_daily} ' \ @@ -570,7 +570,7 @@ cdef class TradingPair(Asset): sid=self.sid, exchange=self.exchange, start_date=self.start_date, - market_currency=self.market_currency, + quote_currency=self.quote_currency, base_currency=self.base_currency, leverage=self.leverage, min_trade_size=self.min_trade_size, diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index 00dd2970..58be745c 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -11,7 +11,8 @@ from catalyst.exchange.exchange import Exchange from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ ExchangeSymbolsNotFound -from catalyst.exchange.exchange_utils import mixin_market_params +from catalyst.exchange.exchange_utils import mixin_market_params, \ + from_ms_timestamp log = Logger('CCXT', level=LOG_LEVEL) @@ -19,17 +20,28 @@ log = Logger('CCXT', level=LOG_LEVEL) class CCXT(Exchange): def __init__(self, exchange_name, key, secret, base_currency, portfolio=None): - log.debug('available exchanges:\n{}'.format(ccxt.exchanges)) - self.api = ccxt.poloniex({ - 'apiKey': key, - 'secret': secret, - }) + log.debug( + 'finding {} in CCXT exchanges:\n{}'.format( + exchange_name, ccxt.exchanges + ) + ) + try: + exchange_attr = getattr(ccxt, exchange_name) + self.api = exchange_attr({ + 'apiKey': key, + 'secret': secret, + }) + except Exception: + raise ValueError('exchange not in CCXT') + markets = self.api.load_markets() log.debug('the markets:\n{}'.format(markets)) self.name = exchange_name - self.assets = {} + + self.assets = dict() self.load_assets() + self.base_currency = base_currency self._portfolio = portfolio self.transactions = defaultdict(list) @@ -50,6 +62,10 @@ class CCXT(Exchange): parts = asset.symbol.split('_') return '{}/{}'.format(parts[0].upper(), parts[1].upper()) + def get_catalyst_symbol(self, market): + parts = market['symbol'].split('/') + return '{}_{}'.format(parts[0].lower(), parts[1].lower()) + def get_timeframe(self, freq): freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I) if freq_match: @@ -99,22 +115,49 @@ class CCXT(Exchange): )) return candles - def load_assets(self, is_local=False): - markets = self.api.fetch_markets() + def _fetch_symbol_map(self, is_local): try: - symbol_map = self.fetch_symbol_map(is_local) + return self.fetch_symbol_map(is_local) except ExchangeSymbolsNotFound: return None - data_source = 'local' if is_local else 'catalyst' + def _fetch_asset(self, market_id, is_local=False): + symbol_map = self._fetch_symbol_map(is_local) + if symbol_map is not None: + assets_lower = {k.lower(): v for k, v in symbol_map.items()} + key = market_id.lower() + + asset = assets_lower[key] if key in assets_lower else None + if asset is not None: + return asset, is_local + + elif not is_local: + return self._fetch_asset(market_id, True) + + else: + return None, is_local + + elif not is_local: + return self._fetch_asset(market_id, True) + + else: + return None, is_local + + def load_assets(self): + markets = self.api.fetch_markets() + for market in markets: - asset = symbol_map[market['id']] \ - if market['id'] in markets else None + asset, is_local = self._fetch_asset(market['id']) + data_source = 'local' if is_local else 'catalyst' - params = dict(exchange=self.name, data_source=data_source) - mixin_market_params(params, market) + params = dict( + exchange=self.name, + data_source=data_source, + exchange_symbol=market['id'], + ) + mixin_market_params(self.name, params, market) - if asset: + if asset is not None: params['symbol'] = asset['symbol'] params['start_date'] = pd.to_datetime( @@ -142,13 +185,10 @@ class CCXT(Exchange): else None else: - params['symbol'] = market['id'] + params['symbol'] = self.get_catalyst_symbol(market) trading_pair = TradingPair(**params) - if is_local: - self.local_assets[market['id']] = trading_pair - else: - self.assets[market['id']] = trading_pair + self.assets[market['id']] = trading_pair def get_balances(self): return None @@ -166,10 +206,56 @@ class CCXT(Exchange): return None def tickers(self, assets): - return None + """ + Retrieve current tick data for the given assets + + Parameters + ---------- + assets: list[TradingPair] + + Returns + ------- + list[dict[str, float] + + """ + tickers = dict() + for asset in assets: + ccxt_symbol = self.get_symbol(asset) + ticker = self.api.fetch_ticker(ccxt_symbol) + + ticker['last_traded'] = from_ms_timestamp(ticker['timestamp']) + + # Using the volume represented in the base currency + ticker['volume'] = ticker['baseVolume'] \ + if 'baseVolume' in ticker else 0 + + tickers[asset] = ticker + + return tickers def get_account(self): return None - def get_orderbook(self, asset, order_type, limit): - return None + def get_orderbook(self, asset, order_type='all', limit=None): + ccxt_symbol = self.get_symbol(asset) + + params = dict() + if limit is not None: + params['depth'] = limit + + order_book = self.api.fetch_order_book(ccxt_symbol, params) + + order_types = ['bids', 'asks'] if order_type == 'all' else [order_type] + result = dict(last_traded=from_ms_timestamp(order_book['timestamp'])) + for index, order_type in enumerate(order_types): + if limit is not None and index > limit - 1: + break + + result[order_type] = [] + for entry in order_book[order_type]: + result[order_type].append(dict( + rate=float(entry[0]), + quantity=float(entry[1]) + )) + + return result diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index f43424aa..8a69d3fb 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -16,7 +16,7 @@ from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \ PricingDataNotLoadedError, \ - NoDataAvailableOnExchange, ExchangeSymbolsNotFound + NoDataAvailableOnExchange, ExchangeSymbolsNotFound, NoValueForField from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ ExchangeLimitOrder, ExchangeStopOrder from catalyst.exchange.exchange_portfolio import ExchangePortfolio @@ -412,12 +412,15 @@ class Exchange: if field not in BASE_FIELDS: raise KeyError('Invalid column: {}'.format(field)) - values = [] - for asset in assets: - value = self.get_single_spot_value(asset, field, data_frequency) - values.append(value) + tickers = self.tickers(assets) + if field == 'close' or field == 'price': + return [t['last'] for t in tickers] - return values + elif field == 'volume': + return [t['volume'] for t in tickers] + + else: + raise NoValueForField(field=field) def get_single_spot_value(self, asset, field, data_frequency): """ diff --git a/catalyst/exchange/exchange_errors.py b/catalyst/exchange/exchange_errors.py index cb4f4d32..5530ccb2 100644 --- a/catalyst/exchange/exchange_errors.py +++ b/catalyst/exchange/exchange_errors.py @@ -240,3 +240,7 @@ class NoDataAvailableOnExchange(ZiplineError): 'Requested data for trading pair {symbol} is not available on exchange {exchange} ' 'in `{data_frequency}` frequency at this time. ' 'Check `http://enigma.co/catalyst/status` for market coverage.').strip() + + +class NoValueForField(ZiplineError): + msg = ('Value not found for field: {field}.').strip() diff --git a/catalyst/exchange/exchange_utils.py b/catalyst/exchange/exchange_utils.py index 83dfae41..ee0ce44e 100644 --- a/catalyst/exchange/exchange_utils.py +++ b/catalyst/exchange/exchange_utils.py @@ -573,7 +573,7 @@ def resample_history_df(df, freq, field): return resampled_df -def mixin_market_params(params, market): +def mixin_market_params(exchange_name, params, market): """ Applies a CCXT market dict to parameters of TradingPair init. @@ -586,7 +586,26 @@ def mixin_market_params(params, market): ------- """ - params['min_trade_size'] = market['lot'] - params['maker'] = market['maker'] - params['taker'] = market['taker'] - params['trading_state'] = 1 if int(market['info']['isFrozen']) == 0 else 0 + # TODO: make this more externalized / configurable + if 'lot' in market: + params['min_trade_size'] = market['lot'] + + if exchange_name == 'bitfinex': + params['maker'] = 0.001 + params['taker'] = 0.002 + + else: + if 'maker' in market: + params['maker'] = market['maker'] + + if 'taker' in market: + params['taker'] = market['taker'] + + info = market['info'] if 'info' in market else None + if info: + if 'minimum_order_size' in info: + params['min_trade_size'] = float(info['minimum_order_size']) + + +def from_ms_timestamp(ms): + return pd.to_datetime(ms, unit='ms', utc=True) diff --git a/tests/exchange/test_ccxt.py b/tests/exchange/test_ccxt.py index f920fa39..f00ea049 100644 --- a/tests/exchange/test_ccxt.py +++ b/tests/exchange/test_ccxt.py @@ -15,7 +15,7 @@ log = Logger('test_ccxt') class TestCCXT(BaseExchangeTestCase): @classmethod def setup(self): - exchange_name = 'poloniex' + exchange_name = 'binance' auth = get_exchange_auth(exchange_name) self.exchange = CCXT( exchange_name=exchange_name, @@ -97,7 +97,7 @@ class TestCCXT(BaseExchangeTestCase): def test_orderbook(self): log.info('testing order book for bittrex') asset = self.exchange.get_asset('eth_btc') - orderbook = self.exchange.get_orderbook(asset) + orderbook = self.exchange.get_orderbook(asset, 'all', limit=10) pass def test_get_fees(self): From b587804e3e4936ad7d747097c13fde38e263be96 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Thu, 30 Nov 2017 17:11:15 -0700 Subject: [PATCH 06/13] DOC: fix video links to algos --- docs/source/videos.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/videos.rst b/docs/source/videos.rst index 90b53f19..1db8ff28 100644 --- a/docs/source/videos.rst +++ b/docs/source/videos.rst @@ -34,7 +34,7 @@ Backtesting a Strategy This is the first video of a two-part series on using Catalyst for algorithmic trading. This video implements a simple momentum strategy based on -`mean reversion `_: when the cryptoasset +`mean reversion `_: when the cryptoasset goes up quickly, we’re going to buy; when it goes down quickly, we’re going to sell. Hopefully, we’ll ride the waves. @@ -48,7 +48,7 @@ Live Trading a Strategy ----------------------- This is the second part of the two-part series on using Catalyst for algorithmic -trading. Having backtested `our strategy `_ +trading. Having backtested `our strategy `_ in the previous video, we now take it to trade live against the Bittrex exchange. .. raw:: html From 7bbb6e0b42dd512a628f35de4ba57d83c87a22d1 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Thu, 30 Nov 2017 20:18:16 -0500 Subject: [PATCH 07/13] BLD: tested creating orders and viewing open orders with CCXT --- catalyst/examples/arbitrage_with_interface.py | 24 +-- catalyst/exchange/ccxt/ccxt_exchange.py | 167 +++++++++++++++++- catalyst/exchange/exchange.py | 98 +++------- catalyst/exchange/exchange_execution.py | 2 +- catalyst/utils/run_algo.py | 2 +- tests/exchange/test_ccxt.py | 8 +- 6 files changed, 198 insertions(+), 103 deletions(-) diff --git a/catalyst/examples/arbitrage_with_interface.py b/catalyst/examples/arbitrage_with_interface.py index f3625e7a..c0c0d343 100644 --- a/catalyst/examples/arbitrage_with_interface.py +++ b/catalyst/examples/arbitrage_with_interface.py @@ -83,15 +83,15 @@ def place_orders(context, amount, buying_price, selling_price, action): else: raise ValueError('invalid order action') - base_currency = enter_exchange.base_currency - base_currency_amount = enter_exchange.portfolio.cash + quote_currency = enter_exchange.quote_currency + quote_currency_amount = enter_exchange.portfolio.cash exit_balances = exit_exchange.get_balances() exit_currency = context.trading_pairs[ - context.selling_exchange].market_currency + context.selling_exchange].quote_currency if exit_currency in exit_balances: - market_currency_amount = exit_balances[exit_currency] + quote_currency_amount = exit_balances[exit_currency] else: log.warn( 'the selling exchange {exchange_name} does not hold ' @@ -102,25 +102,25 @@ def place_orders(context, amount, buying_price, selling_price, action): ) return - if base_currency_amount < (amount * entry_price): - adj_amount = base_currency_amount / entry_price + if quote_currency_amount < (amount * entry_price): + adj_amount = quote_currency_amount / entry_price log.warn( - 'not enough {base_currency} ({base_currency_amount}) to buy ' + 'not enough {quote_currency} ({quote_currency_amount}) to buy ' '{amount}, adjusting the amount to {adj_amount}'.format( - base_currency=base_currency, - base_currency_amount=base_currency_amount, + quote_currency=quote_currency, + quote_currency_amount=quote_currency_amount, amount=amount, adj_amount=adj_amount ) ) amount = adj_amount - elif market_currency_amount < amount: + elif quote_currency_amount < amount: log.warn( 'not enough {currency} ({currency_amount}) to sell ' '{amount}, aborting'.format( currency=exit_currency, - currency_amount=market_currency_amount, + currency_amount=quote_currency_amount, amount=amount ) ) @@ -270,6 +270,6 @@ run_algorithm( exchange_name='poloniex,bitfinex', live=True, algo_namespace=algo_namespace, - base_currency='btc', + quote_currency='btc', live_graph=False ) diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index 58be745c..b0b9663b 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -3,19 +3,32 @@ from collections import defaultdict import ccxt import pandas as pd +from ccxt import ExchangeNotAvailable +from six import string_types + +from catalyst.finance.order import Order, ORDER_STATUS + +from catalyst.algorithm import MarketOrder from catalyst.assets._assets import TradingPair from logbook import Logger from catalyst.constants import LOG_LEVEL -from catalyst.exchange.exchange import Exchange +from catalyst.exchange.exchange import Exchange, ExchangeLimitOrder from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ - ExchangeSymbolsNotFound + ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle from catalyst.exchange.exchange_utils import mixin_market_params, \ from_ms_timestamp log = Logger('CCXT', level=LOG_LEVEL) +SUPPORTED_EXCHANGES = dict( + binance=ccxt.binance, + bitfinex=ccxt.bitfinex, + bittrex=ccxt.bittrex, + poloniex=ccxt.poloniex, +) + class CCXT(Exchange): def __init__(self, exchange_name, key, secret, base_currency, @@ -26,7 +39,13 @@ class CCXT(Exchange): ) ) try: - exchange_attr = getattr(ccxt, exchange_name) + # Making instantiation as explicit as possible for code tracking. + if exchange_name in SUPPORTED_EXCHANGES: + exchange_attr = SUPPORTED_EXCHANGES[exchange_name] + + else: + exchange_attr = getattr(ccxt, exchange_name) + self.api = exchange_attr({ 'apiKey': key, 'secret': secret, @@ -62,8 +81,12 @@ class CCXT(Exchange): parts = asset.symbol.split('_') return '{}/{}'.format(parts[0].upper(), parts[1].upper()) - def get_catalyst_symbol(self, market): - parts = market['symbol'].split('/') + def get_catalyst_symbol(self, market_or_symbol): + symbol = market_or_symbol if isinstance( + market_or_symbol, string_types + ) else market_or_symbol['symbol'] + + parts = symbol.split('/') return '{}_{}'.format(parts[0].lower(), parts[1].lower()) def get_timeframe(self, freq): @@ -191,13 +214,141 @@ class CCXT(Exchange): self.assets[market['id']] = trading_pair def get_balances(self): - return None + try: + log.debug('retrieving wallets balances') + balances = self.api.fetch_balance() + + balances_lower = dict() + for key in balances: + balances_lower[key.lower()] = balances[key] + + except Exception as e: + log.debug('error retrieving balances: {}', e) + raise ExchangeRequestError(error=e) + + return balances_lower + + def _create_order(self, order_status): + """ + Create a Catalyst order object from a Bitfinex order dictionary + :param order_status: + :return: Order + """ + if order_status['status'] == 'canceled': + status = ORDER_STATUS.CANCELLED + + elif order_status['status'] == 'closed' and order_status['filled'] > 0: + log.info('found executed order {}'.format(order_status)) + status = ORDER_STATUS.FILLED + + elif order_status['status'] == 'open': + status = ORDER_STATUS.OPEN + + else: + raise ValueError('invalid state for order') + + amount = float(order_status['amount']) + filled = float(order_status['filled']) + + if order_status['side'] == 'sell': + amount = -amount + filled = -filled + + price = float(order_status['price']) + order_type = order_status['type'] + + stop_price = None + limit_price = None + + # TODO: is this comprehensive enough? + if order_type.endswith('limit'): + limit_price = price + elif order_type.endswith('stop'): + stop_price = price + + executed_price = order_status['cost'] / order_status['amount'] + commission = order_status['fee'] + date = from_ms_timestamp(order_status['timestamp']) + + symbol = order_status['info']['symbol'] + order = Order( + dt=date, + asset=self.assets[symbol], + amount=amount, + stop=stop_price, + limit=limit_price, + filled=filled, + id=str(order_status['id']), + commission=commission + ) + order.status = status + + return order, executed_price def create_order(self, asset, amount, is_buy, style): - return None + symbol = self.get_symbol(asset) + + if isinstance(style, ExchangeLimitOrder): + price = style.get_limit_price(is_buy) + order_type = 'limit' + + elif isinstance(style, MarketOrder): + price = None + order_type = 'market' + + else: + raise InvalidOrderStyle( + exchange=self.name, + style=style.__class__.__name__ + ) + + side = 'buy' if amount > 0 else 'sell' + + try: + result = self.api.create_order( + symbol=symbol, + type=order_type, + side=side, + amount=abs(amount), + price=price + ) + except ExchangeNotAvailable as e: + log.debug('unable to create order: {}'.format(e)) + raise ExchangeRequestError(error=e) + + if 'info' not in result: + raise ValueError('cannot use order without info attribute') + + order_id = str(result['info']['clientOrderId']) + order = Order( + dt=from_ms_timestamp(result['info']['transactTime']), + asset=asset, + amount=amount, + stop=style.get_stop_price(is_buy), + limit=style.get_limit_price(is_buy), + id=order_id + ) + return order def get_open_orders(self, asset): - return None + try: + symbol = self.get_symbol(asset) + result = self.api.fetch_open_orders( + symbol=symbol, + since=None, + limit=None, + params=dict() + ) + except Exception as e: + raise ExchangeRequestError(error=e) + + orders = [] + for order_status in result: + order, executed_price = self._create_order(order_status) + if asset is None or asset == order.sid: + orders.append(order) + + return orders def get_order(self, order_id): return None diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 8a69d3fb..751955f0 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -8,15 +8,16 @@ import pandas as pd from catalyst.assets._assets import TradingPair from logbook import Logger +from catalyst.algorithm import MarketOrder from catalyst.constants import LOG_LEVEL from catalyst.data.data_portal import BASE_FIELDS from catalyst.exchange.bundle_utils import get_start_dt, \ get_delta, get_periods, get_periods_range from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ - InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \ + BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \ PricingDataNotLoadedError, \ - NoDataAvailableOnExchange, ExchangeSymbolsNotFound, NoValueForField + NoDataAvailableOnExchange, NoValueForField from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ ExchangeLimitOrder, ExchangeStopOrder from catalyst.exchange.exchange_portfolio import ExchangePortfolio @@ -34,7 +35,6 @@ class Exchange: def __init__(self): self.name = None self.assets = dict() - self.local_assets = dict() self._portfolio = None self.minute_writer = None self.minute_reader = None @@ -200,13 +200,13 @@ class Exchange: return assets def _find_asset(self, asset, symbol, data_frequency, is_local=False): - assets = self.assets if not is_local else self.local_assets - + assets = self.assets for key in assets: has_data = (data_frequency == 'minute' and assets[key].end_minute is not None) \ or (data_frequency == 'daily' and assets[key].end_daily is not None) + if not asset and assets[key].symbol.lower() == symbol.lower() \ and (not data_frequency or has_data): asset = assets[key] @@ -236,8 +236,7 @@ class Exchange: asset = self._find_asset(asset, symbol, data_frequency, True) if not asset: - all_values = list(self.assets.values()) + \ - list(self.local_assets.values()) + all_values = list(self.assets.values()) supported_symbols = sorted([ asset.symbol for asset in all_values ]) @@ -253,6 +252,7 @@ class Exchange: def fetch_symbol_map(self, is_local=False): return get_exchange_symbols(self.name, is_local) + @abstractmethod def load_assets(self, is_local=False): """ Populate the 'assets' attribute with a dictionary of Assets. @@ -270,66 +270,7 @@ class Exchange: via its api. """ - try: - symbol_map = self.fetch_symbol_map(is_local) - except ExchangeSymbolsNotFound: - return None - - for exchange_symbol in symbol_map: - asset = symbol_map[exchange_symbol] - - if 'start_date' in asset: - start_date = pd.to_datetime(asset['start_date'], utc=True) - else: - start_date = None - - if 'end_date' in asset: - end_date = pd.to_datetime(asset['end_date'], utc=True) - else: - end_date = None - - if 'leverage' in asset: - leverage = asset['leverage'] - else: - leverage = 1.0 - - if 'asset_name' in asset: - asset_name = asset['asset_name'] - else: - asset_name = None - - if 'min_trade_size' in asset: - min_trade_size = asset['min_trade_size'] - else: - min_trade_size = 0.0000001 - - if 'end_daily' in asset and asset['end_daily'] != 'N/A': - end_daily = pd.to_datetime(asset['end_daily'], utc=True) - else: - end_daily = None - - if 'end_minute' in asset and asset['end_minute'] != 'N/A': - end_minute = pd.to_datetime(asset['end_minute'], utc=True) - else: - end_minute = None - - trading_pair = TradingPair( - symbol=asset['symbol'], - exchange=self.name, - start_date=start_date, - end_date=end_date, - leverage=leverage, - asset_name=asset_name, - min_trade_size=min_trade_size, - end_daily=end_daily, - end_minute=end_minute, - exchange_symbol=exchange_symbol - ) - - if is_local: - self.local_assets[exchange_symbol] = trading_pair - else: - self.assets[exchange_symbol] = trading_pair + pass def check_open_orders(self): """ @@ -694,7 +635,7 @@ class Exchange: log.debug('synchronizing portfolio with exchange {}'.format(self.name)) balances = self.get_balances() - base_position_available = balances[self.base_currency] \ + base_position_available = balances[self.base_currency]['free'] \ if self.base_currency in balances else None if base_position_available is None: @@ -777,28 +718,30 @@ class Exchange: log.warn('skipping order amount of 0') return None - if asset.base_currency != self.base_currency.lower(): + if self.base_currency is None: + raise ValueError('no base_currency defined for this exchange') + + if asset.quote_currency != self.base_currency.lower(): raise MismatchingBaseCurrencies( - base_currency=asset.base_currency, + base_currency=asset.quote_currency, algo_currency=self.base_currency ) is_buy = (amount > 0) if limit_price is not None and stop_price is not None: - style = ExchangeStopLimitOrder(limit_price, stop_price, - exchange=self.name) + style = ExchangeStopLimitOrder( + limit_price, stop_price, exchange=self.name + ) + elif limit_price is not None: style = ExchangeLimitOrder(limit_price, exchange=self.name) elif stop_price is not None: style = ExchangeStopOrder(stop_price, exchange=self.name) - elif style is not None: - raise InvalidOrderStyle(exchange=self.name.title(), - style=style.__class__.__name__) else: - raise ValueError('Incomplete order data.') + style = MarketOrder(exchange=self.name) display_price = limit_price if limit_price is not None else stop_price log.debug( @@ -807,9 +750,10 @@ class Exchange: amount=amount, symbol=asset.symbol, type=style.__class__.__name__, - price='{}{}'.format(display_price, asset.base_currency) + price='{}{}'.format(display_price, asset.quote_currency) ) ) + order = self.create_order(asset, amount, is_buy, style) if order: self._portfolio.create_order(order) diff --git a/catalyst/exchange/exchange_execution.py b/catalyst/exchange/exchange_execution.py index 536b526a..fe029e3c 100644 --- a/catalyst/exchange/exchange_execution.py +++ b/catalyst/exchange/exchange_execution.py @@ -1,4 +1,4 @@ -from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder +from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder, MarketOrder class ExchangeLimitOrder(LimitOrder): diff --git a/catalyst/utils/run_algo.py b/catalyst/utils/run_algo.py index af62f97d..288c09bc 100644 --- a/catalyst/utils/run_algo.py +++ b/catalyst/utils/run_algo.py @@ -263,7 +263,7 @@ def _run(handle_data, ) if base_currency in balances: - base_currency_available = balances[base_currency] + base_currency_available = balances[base_currency]['free'] log.info( 'base currency available in the account: {} {}'.format( base_currency_available, base_currency diff --git a/tests/exchange/test_ccxt.py b/tests/exchange/test_ccxt.py index f00ea049..a0a027d6 100644 --- a/tests/exchange/test_ccxt.py +++ b/tests/exchange/test_ccxt.py @@ -21,16 +21,16 @@ class TestCCXT(BaseExchangeTestCase): exchange_name=exchange_name, key=auth['key'], secret=auth['secret'], - base_currency=None, + base_currency='eth', portfolio=None ) def test_order(self): log.info('creating order') - asset = self.exchange.get_asset('neo_btc') + asset = self.exchange.get_asset('neo_eth') order_id = self.exchange.order( asset=asset, - limit_price=0.0005, + limit_price=0.07, amount=1, ) log.info('order created {}'.format(order_id)) @@ -39,7 +39,7 @@ class TestCCXT(BaseExchangeTestCase): def test_open_orders(self): log.info('retrieving open orders') - asset = self.exchange.get_asset('neo_btc') + asset = self.exchange.get_asset('neo_eth') orders = self.exchange.get_open_orders(asset) pass From b762689225b6dffa506c10288bb2ca47fa0a6163 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Thu, 30 Nov 2017 22:45:52 -0500 Subject: [PATCH 08/13] BLD: all exchange operations now implemented an unit tested with CCXT --- catalyst/exchange/ccxt/ccxt_exchange.py | 99 +++++++++++++++++++------ catalyst/exchange/exchange.py | 16 ++-- catalyst/exchange/exchange_portfolio.py | 30 +------- tests/exchange/test_ccxt.py | 6 +- 4 files changed, 92 insertions(+), 59 deletions(-) diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index b0b9663b..6db774c1 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -77,8 +77,12 @@ class CCXT(Exchange): def time_skew(self): return None - def get_symbol(self, asset): - parts = asset.symbol.split('_') + def get_symbol(self, asset_or_symbol): + symbol = asset_or_symbol if isinstance( + asset_or_symbol, string_types + ) else asset_or_symbol.symbol + + parts = symbol.split('_') return '{}/{}'.format(parts[0].upper(), parts[1].upper()) def get_catalyst_symbol(self, market_or_symbol): @@ -230,15 +234,24 @@ class CCXT(Exchange): def _create_order(self, order_status): """ - Create a Catalyst order object from a Bitfinex order dictionary - :param order_status: - :return: Order + Create a Catalyst order object from a CCXT order dictionary + + Parameters + ---------- + order_status: dict[str, Object] + The order dict from the CCXT api. + + Returns + ------- + Order + The Catalyst order object + """ if order_status['status'] == 'canceled': status = ORDER_STATUS.CANCELLED elif order_status['status'] == 'closed' and order_status['filled'] > 0: - log.info('found executed order {}'.format(order_status)) + log.debug('found executed order {}'.format(order_status)) status = ORDER_STATUS.FILLED elif order_status['status'] == 'open': @@ -247,30 +260,27 @@ class CCXT(Exchange): else: raise ValueError('invalid state for order') - amount = float(order_status['amount']) - filled = float(order_status['filled']) + amount = order_status['amount'] + filled = order_status['filled'] if order_status['side'] == 'sell': amount = -amount filled = -filled - price = float(order_status['price']) + price = order_status['price'] order_type = order_status['type'] - stop_price = None - limit_price = None - - # TODO: is this comprehensive enough? - if order_type.endswith('limit'): - limit_price = price - elif order_type.endswith('stop'): - stop_price = price + limit_price = price if order_type == 'limit' else None + stop_price = None # TODO: add support executed_price = order_status['cost'] / order_status['amount'] commission = order_status['fee'] date = from_ms_timestamp(order_status['timestamp']) + # order_id = str(order_status['info']['clientOrderId']) + order_id = order_status['id'] symbol = order_status['info']['symbol'] + order = Order( dt=date, asset=self.assets[symbol], @@ -278,7 +288,7 @@ class CCXT(Exchange): stop=stop_price, limit=limit_price, filled=filled, - id=str(order_status['id']), + id=order_id, commission=commission ) order.status = status @@ -319,7 +329,8 @@ class CCXT(Exchange): if 'info' not in result: raise ValueError('cannot use order without info attribute') - order_id = str(result['info']['clientOrderId']) + # order_id = str(result['info']['clientOrderId']) + order_id = result['id'] order = Order( dt=from_ms_timestamp(result['info']['transactTime']), asset=asset, @@ -350,11 +361,53 @@ class CCXT(Exchange): return orders - def get_order(self, order_id): - return None + def _get_asset_from_order(self, order_id): + open_orders = self.portfolio.open_orders + order = next( + (order for order in open_orders if order.id == order_id), + None + ) # type: Order + return order.asset if order is not None else None - def cancel_order(self, order_param): - return None + def get_order(self, order_id, asset_or_symbol=None): + if asset_or_symbol is None and self.portfolio is not None: + asset_or_symbol = self._get_asset_from_order(order_id) + + if asset_or_symbol is None: + log.debug( + 'order not found in memory, the request might fail ' + 'on some exchanges.' + ) + try: + symbol = self.get_symbol(asset_or_symbol) \ + if asset_or_symbol is not None else None + order_status = self.api.fetch_order(id=order_id, symbol=symbol) + order, _ = self._create_order(order_status) + + except Exception as e: + raise ExchangeRequestError(error=e) + + return order + + def cancel_order(self, order_param, asset_or_symbol=None): + order_id = order_param.id \ + if isinstance(order_param, Order) else order_param + + if asset_or_symbol is None and self.portfolio is not None: + asset_or_symbol = self._get_asset_from_order(order_id) + + if asset_or_symbol is None: + log.debug( + 'order not found in memory, cancelling order might fail ' + 'on some exchanges.' + ) + try: + symbol = self.get_symbol(asset_or_symbol) \ + if asset_or_symbol is not None else None + self.api.cancel_order(id=order_id, symbol=symbol) + + except Exception as e: + raise ExchangeRequestError(error=e) def tickers(self, assets): """ diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 751955f0..fd2be234 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -289,9 +289,11 @@ class Exchange: log.debug('found open order: {}'.format(order_id)) order, executed_price = self.get_order(order_id) - log.debug('got updated order {} {}'.format( - order, executed_price)) - + log.debug( + 'got updated order {} {}'.format( + order, executed_price + ) + ) if order.status == ORDER_STATUS.FILLED: transaction = Transaction( asset=order.asset, @@ -822,7 +824,7 @@ class Exchange: pass @abstractmethod - def get_order(self, order_id): + def get_order(self, order_id, symbol_or_asset=None): """Lookup an order based on the order id returned from one of the order functions. @@ -830,6 +832,8 @@ class Exchange: ---------- order_id : str The unique identifier for the order. + symbol_or_asset: str|TradingPair + The catalyst symbol, some exchanges need this Returns ------- @@ -841,13 +845,15 @@ class Exchange: pass @abstractmethod - def cancel_order(self, order_param): + def cancel_order(self, order_param, symbol_or_asset=None): """Cancel an open order. Parameters ---------- order_param : str or Order The order_id or order object to cancel. + symbol_or_asset: str|TradingPair + The catalyst symbol, some exchanges need this """ pass diff --git a/catalyst/exchange/exchange_portfolio.py b/catalyst/exchange/exchange_portfolio.py index 2003654b..b9f45fbb 100644 --- a/catalyst/exchange/exchange_portfolio.py +++ b/catalyst/exchange/exchange_portfolio.py @@ -3,7 +3,6 @@ from logbook import Logger from catalyst.constants import LOG_LEVEL from catalyst.protocol import Portfolio, Positions, Position -from catalyst.utils.deprecate import deprecated log = Logger('ExchangePortfolio', level=LOG_LEVEL) @@ -11,7 +10,8 @@ log = Logger('ExchangePortfolio', level=LOG_LEVEL) class ExchangePortfolio(Portfolio): """ Since the goal is to support multiple exchanges, it makes sense to - include additional stats in the portfolio object. + include additional stats in the portfolio object. This fills the role + of Blotter and Portfolio in live mode. Instead of relying on the performance tracker, each exchange portfolio tracks its own holding. This offers a separation between tracking an @@ -89,32 +89,6 @@ class ExchangePortfolio(Portfolio): log.debug('updated portfolio with executed order') - @deprecated - def execute_transaction(self, transaction): - # TODO: almost duplicate of execute_order. Not sure why Poloniex needs this. - log.debug('executing transaction {}'.format(transaction.order_id)) - - order_position = self.positions[transaction.asset] \ - if transaction.asset in self.positions else None - - if order_position is None: - raise ValueError( - 'Trying to execute transaction for a position not held: %s' % transaction.order_id - ) - - self.capital_used += transaction.amount * transaction.price - - if transaction.amount > 0: - if order_position.cost_basis > 0: - order_position.cost_basis = np.average( - [order_position.cost_basis, transaction.price], - weights=[order_position.amount, transaction.amount] - ) - else: - order_position.cost_basis = transaction.price - - log.debug('updated portfolio with executed order') - def remove_order(self, order): """ Removing an open order. diff --git a/tests/exchange/test_ccxt.py b/tests/exchange/test_ccxt.py index a0a027d6..eccdd18f 100644 --- a/tests/exchange/test_ccxt.py +++ b/tests/exchange/test_ccxt.py @@ -45,14 +45,14 @@ class TestCCXT(BaseExchangeTestCase): def test_get_order(self): log.info('retrieving order') - order = self.exchange.get_order( - u'2c584020-9caf-4af5-bde0-332c0bba17e2') + order = self.exchange.get_order('2631386', 'neo_eth') + # order = self.exchange.get_order('2631386') assert isinstance(order, Order) pass def test_cancel_order(self, ): log.info('cancel order') - self.exchange.cancel_order(u'dc7bcca2-5219-4145-8848-8a593d2a72f9') + self.exchange.cancel_order('2631386', 'neo_eth') pass def test_get_candles(self): From 8fb8b80a12ffd9887a760b93704b8f9947c8a44a Mon Sep 17 00:00:00 2001 From: fredfortier Date: Thu, 30 Nov 2017 23:15:10 -0500 Subject: [PATCH 09/13] BLD: first rough test of CCXT in live trading --- catalyst/data/loader.py | 6 ++- catalyst/examples/simple_loop.py | 56 ++++++++++++------------- catalyst/exchange/ccxt/ccxt_exchange.py | 43 ++++++++++--------- catalyst/exchange/exchange.py | 4 +- catalyst/exchange/factory.py | 53 ++++++++++------------- catalyst/utils/run_algo.py | 42 ++++--------------- 6 files changed, 88 insertions(+), 116 deletions(-) diff --git a/catalyst/data/loader.py b/catalyst/data/loader.py index ff3f34a8..e58127f7 100644 --- a/catalyst/data/loader.py +++ b/catalyst/data/loader.py @@ -142,8 +142,10 @@ def load_crypto_market_data(trading_day=None, trading_days=None, if exchange is None: # This is exceptional, since placing the import at the module scope # breaks things and it's only needed here - from catalyst.exchange.poloniex.poloniex import Poloniex - exchange = Poloniex('', '', '') + from catalyst.exchange.factory import get_exchange + exchange = get_exchange( + exchange_name='poloniex', base_currency='usdt' + ) benchmark_asset = exchange.get_asset(bm_symbol) diff --git a/catalyst/examples/simple_loop.py b/catalyst/examples/simple_loop.py index bfd2d4f0..75f55092 100644 --- a/catalyst/examples/simple_loop.py +++ b/catalyst/examples/simple_loop.py @@ -9,7 +9,7 @@ from catalyst.exchange.stats_utils import get_pretty_stats, \ def initialize(context): print('initializing') - context.asset = symbol('neo_usd') + context.asset = symbol('neo_eth') context.base_price = None @@ -19,17 +19,14 @@ def handle_data(context, data): price = data.current(context.asset, 'close') print('got price {price}'.format(price=price)) - try: - prices = data.history( - context.asset, - fields='price', - bar_count=14, - frequency='15T' - ) - rsi = talib.RSI(prices.values, timeperiod=14)[-1] - print('got rsi: {}'.format(rsi)) - except Exception as e: - print(e) + prices = data.history( + context.asset, + fields='price', + bar_count=20, + frequency='15T' + ) + rsi = talib.RSI(prices.values, timeperiod=14)[-1] + print('got rsi: {}'.format(rsi)) # If base_price is not set, we use the current value. This is the # price at the first bar which we reference to calculate price_change. @@ -110,24 +107,25 @@ def analyze(context, perf): pass -run_algorithm( - capital_base=250, - start=pd.to_datetime('2017-11-1 0:00', utc=True), - end=pd.to_datetime('2017-11-10 23:59', utc=True), - data_frequency='daily', - initialize=initialize, - handle_data=handle_data, - analyze=analyze, - exchange_name='bitfinex', - algo_namespace='simple_loop', - base_currency='usd' -) # run_algorithm( +# capital_base=250, +# start=pd.to_datetime('2017-11-1 0:00', utc=True), +# end=pd.to_datetime('2017-11-10 23:59', utc=True), +# data_frequency='daily', # initialize=initialize, # handle_data=handle_data, -# analyze=None, -# exchange_name='poloniex', -# live=True, +# analyze=analyze, +# exchange_name='bitfinex', # algo_namespace='simple_loop', -# base_currency='eth', -# live_graph=False +# base_currency='usd' +# ) +run_algorithm( + initialize=initialize, + handle_data=handle_data, + analyze=None, + exchange_name='binance', + live=True, + algo_namespace='simple_loop', + base_currency='eth', + live_graph=False, +) diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index 6db774c1..d04c346f 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -16,7 +16,8 @@ from catalyst.constants import LOG_LEVEL from catalyst.exchange.exchange import Exchange, ExchangeLimitOrder from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ - ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle + ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \ + ExchangeNotFoundError from catalyst.exchange.exchange_utils import mixin_market_params, \ from_ms_timestamp @@ -50,8 +51,9 @@ class CCXT(Exchange): 'apiKey': key, 'secret': secret, }) + except Exception: - raise ValueError('exchange not in CCXT') + raise ExchangeNotFoundError(exchange_name=exchange_name) markets = self.api.load_markets() log.debug('the markets:\n{}'.format(markets)) @@ -122,24 +124,27 @@ class CCXT(Exchange): delta = start_dt - pd.to_datetime('1970-1-1', utc=True) ms = int(delta.total_seconds()) * 1000 - ohlcvs = self.api.fetch_ohlcv( - symbol=symbols[0], - timeframe=timeframe, - since=ms, - limit=bar_count, - params={} - ) + candles = dict() + for asset in assets: + ohlcvs = self.api.fetch_ohlcv( + symbol=symbols[0], + timeframe=timeframe, + since=ms, + limit=bar_count, + params={} + ) + + candles[asset] = [] + for ohlcv in ohlcvs: + candles[asset].append(dict( + last_traded=pd.to_datetime(ohlcv[0], unit='ms', utc=True), + open=ohlcv[1], + high=ohlcv[2], + low=ohlcv[3], + close=ohlcv[4], + volume=ohlcv[5] + )) - candles = [] - for ohlcv in ohlcvs: - candles.append(dict( - last_traded=pd.to_datetime(ohlcv[0], unit='ms', utc=True), - open=ohlcv[1], - high=ohlcv[2], - low=ohlcv[3], - close=ohlcv[4], - volume=ohlcv[5] - )) return candles def _fetch_symbol_map(self, is_local): diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index fd2be234..317175d0 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -357,10 +357,10 @@ class Exchange: tickers = self.tickers(assets) if field == 'close' or field == 'price': - return [t['last'] for t in tickers] + return [tickers[asset]['last'] for asset in tickers] elif field == 'volume': - return [t['volume'] for t in tickers] + return [tickers[asset]['volume'] for asset in tickers] else: raise NoValueForField(field=field) diff --git a/catalyst/exchange/factory.py b/catalyst/exchange/factory.py index 72c66bd8..8099b208 100644 --- a/catalyst/exchange/factory.py +++ b/catalyst/exchange/factory.py @@ -1,38 +1,31 @@ -from catalyst.exchange.bitfinex.bitfinex import Bitfinex -from catalyst.exchange.bittrex.bittrex import Bittrex -from catalyst.exchange.exchange_errors import ExchangeNotFoundError -from catalyst.exchange.exchange_utils import get_exchange_auth -from catalyst.exchange.poloniex.poloniex import Poloniex +import os + +from catalyst.exchange.ccxt.ccxt_exchange import CCXT +from catalyst.exchange.exchange_errors import ExchangeAuthEmpty +from catalyst.exchange.exchange_utils import get_exchange_auth, \ + get_exchange_folder -def get_exchange(exchange_name, base_currency=None): +def get_exchange(exchange_name, base_currency=None, portfolio=None, + must_authenticate=False): exchange_auth = get_exchange_auth(exchange_name) - if exchange_name == 'bitfinex': - return Bitfinex( - key=exchange_auth['key'], - secret=exchange_auth['secret'], - base_currency=base_currency, - portfolio=None + + has_auth = (exchange_auth['key'] != '' and exchange_auth['secret'] != '') + if must_authenticate and not has_auth: + raise ExchangeAuthEmpty( + exchange=exchange_name.title(), + filename=os.path.join( + get_exchange_folder(exchange_name), 'auth.json' + ) ) - elif exchange_name == 'bittrex': - return Bittrex( - key=exchange_auth['key'], - secret=exchange_auth['secret'], - base_currency=base_currency, - portfolio=None - ) - - elif exchange_name == 'poloniex': - return Poloniex( - key=exchange_auth['key'], - secret=exchange_auth['secret'], - base_currency=base_currency, - portfolio=None - ) - - else: - raise ExchangeNotFoundError(exchange_name=exchange_name) + return CCXT( + exchange_name=exchange_name, + key=exchange_auth['key'], + secret=exchange_auth['secret'], + base_currency=base_currency, + portfolio=portfolio + ) def get_exchanges(exchange_names): diff --git a/catalyst/utils/run_algo.py b/catalyst/utils/run_algo.py index 288c09bc..404b21c0 100644 --- a/catalyst/utils/run_algo.py +++ b/catalyst/utils/run_algo.py @@ -13,6 +13,7 @@ from catalyst.data.bundles import load from catalyst.data.data_portal import DataPortal from catalyst.exchange.bittrex.bittrex import Bittrex from catalyst.exchange.bitfinex.bitfinex import Bitfinex +from catalyst.exchange.factory import get_exchange from catalyst.exchange.poloniex.poloniex import Poloniex try: @@ -164,42 +165,15 @@ def _run(handle_data, if portfolio is None: portfolio = ExchangePortfolio( - start_date=pd.Timestamp.utcnow() + start if start is not None else pd.Timestamp.utcnow() ) - # This corresponds to the json file containing api token info - exchange_auth = get_exchange_auth(exchange_name) - - if live and (exchange_auth['key'] == '' \ - or exchange_auth['secret'] == ''): - raise ExchangeAuthEmpty( - exchange=exchange_name.title(), - filename=os.path.join( - get_exchange_folder(exchange_name, environ), 'auth.json')) - - if exchange_name == 'bitfinex': - exchanges[exchange_name] = Bitfinex( - key=exchange_auth['key'], - secret=exchange_auth['secret'], - base_currency=base_currency, - portfolio=portfolio - ) - elif exchange_name == 'bittrex': - exchanges[exchange_name] = Bittrex( - key=exchange_auth['key'], - secret=exchange_auth['secret'], - base_currency=base_currency, - portfolio=portfolio - ) - elif exchange_name == 'poloniex': - exchanges[exchange_name] = Poloniex( - key=exchange_auth['key'], - secret=exchange_auth['secret'], - base_currency=base_currency, - portfolio=portfolio - ) - else: - raise ExchangeNotFoundError(exchange_name=exchange_name) + exchanges[exchange_name] = get_exchange( + exchange_name=exchange_name, + base_currency=base_currency, + portfolio=portfolio, + must_authenticate=live, + ) open_calendar = get_calendar('OPEN') From 4db8131397e223be84b5dd3c05423f0b64d80f5a Mon Sep 17 00:00:00 2001 From: fredfortier Date: Fri, 1 Dec 2017 00:06:11 -0500 Subject: [PATCH 10/13] BLD: paper trading adjustments --- catalyst/examples/simple_loop.py | 34 ++++++++++++------------- catalyst/exchange/ccxt/ccxt_exchange.py | 15 +++++++---- catalyst/exchange/exchange_algorithm.py | 1 + catalyst/exchange/exchange_utils.py | 13 ++++++---- catalyst/utils/run_algo.py | 21 ++++++++------- tests/exchange/test_ccxt.py | 18 ++++--------- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/catalyst/examples/simple_loop.py b/catalyst/examples/simple_loop.py index 75f55092..b0dbd929 100644 --- a/catalyst/examples/simple_loop.py +++ b/catalyst/examples/simple_loop.py @@ -107,25 +107,25 @@ def analyze(context, perf): pass -# run_algorithm( -# capital_base=250, -# start=pd.to_datetime('2017-11-1 0:00', utc=True), -# end=pd.to_datetime('2017-11-10 23:59', utc=True), -# data_frequency='daily', -# initialize=initialize, -# handle_data=handle_data, -# analyze=analyze, -# exchange_name='bitfinex', -# algo_namespace='simple_loop', -# base_currency='usd' -# ) run_algorithm( + capital_base=250, + start=pd.to_datetime('2017-11-1 0:00', utc=True), + end=pd.to_datetime('2017-11-10 23:59', utc=True), + data_frequency='daily', initialize=initialize, handle_data=handle_data, - analyze=None, - exchange_name='binance', - live=True, + analyze=analyze, + exchange_name='bitfinex', algo_namespace='simple_loop', - base_currency='eth', - live_graph=False, + base_currency='usd' ) +# run_algorithm( +# initialize=initialize, +# handle_data=handle_data, +# analyze=None, +# exchange_name='binance', +# live=True, +# algo_namespace='simple_loop', +# base_currency='eth', +# live_graph=False, +# ) diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index d04c346f..d2c735cc 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -28,6 +28,8 @@ SUPPORTED_EXCHANGES = dict( bitfinex=ccxt.bitfinex, bittrex=ccxt.bittrex, poloniex=ccxt.poloniex, + bitmex=ccxt.bitmex, + gdax=ccxt.gdax, ) @@ -88,12 +90,15 @@ class CCXT(Exchange): return '{}/{}'.format(parts[0].upper(), parts[1].upper()) def get_catalyst_symbol(self, market_or_symbol): - symbol = market_or_symbol if isinstance( - market_or_symbol, string_types - ) else market_or_symbol['symbol'] + if isinstance(market_or_symbol, string_types): + parts = market_or_symbol.split('/') + return '{}_{}'.format(parts[0].lower(), parts[1].lower()) - parts = symbol.split('/') - return '{}_{}'.format(parts[0].lower(), parts[1].lower()) + else: + return '{}_{}'.format( + market_or_symbol['base'].lower(), + market_or_symbol['quote'].lower(), + ) def get_timeframe(self, freq): freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I) diff --git a/catalyst/exchange/exchange_algorithm.py b/catalyst/exchange/exchange_algorithm.py index bd7c84a1..c2102015 100644 --- a/catalyst/exchange/exchange_algorithm.py +++ b/catalyst/exchange/exchange_algorithm.py @@ -289,6 +289,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase): def __init__(self, *args, **kwargs): self.algo_namespace = kwargs.pop('algo_namespace', None) self.live_graph = kwargs.pop('live_graph', None) + self.simulate_orders = kwargs.pop('simulate_orders', None) self._clock = None self.frame_stats = deque(maxlen=60) diff --git a/catalyst/exchange/exchange_utils.py b/catalyst/exchange/exchange_utils.py index ee0ce44e..0ca18c95 100644 --- a/catalyst/exchange/exchange_utils.py +++ b/catalyst/exchange/exchange_utils.py @@ -594,12 +594,15 @@ def mixin_market_params(exchange_name, params, market): params['maker'] = 0.001 params['taker'] = 0.002 - else: - if 'maker' in market: - params['maker'] = market['maker'] + elif 'maker' in market and 'taker' in market \ + and market['maker'] is not None and market['taker'] is not None: + params['maker'] = market['maker'] + params['taker'] = market['taker'] - if 'taker' in market: - params['taker'] = market['taker'] + else: + # TODO: default commission, make configurable + params['maker'] = 0.0015 + params['taker'] = 0.0025 info = market['info'] if 'info' in market else None if info: diff --git a/catalyst/utils/run_algo.py b/catalyst/utils/run_algo.py index 404b21c0..a20490bc 100644 --- a/catalyst/utils/run_algo.py +++ b/catalyst/utils/run_algo.py @@ -11,10 +11,7 @@ import pandas as pd from catalyst.data.bundles import load from catalyst.data.data_portal import DataPortal -from catalyst.exchange.bittrex.bittrex import Bittrex -from catalyst.exchange.bitfinex.bitfinex import Bitfinex from catalyst.exchange.factory import get_exchange -from catalyst.exchange.poloniex.poloniex import Poloniex try: from pygments import highlight @@ -40,11 +37,9 @@ from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \ from catalyst.exchange.asset_finder_exchange import AssetFinderExchange from catalyst.exchange.exchange_portfolio import ExchangePortfolio from catalyst.exchange.exchange_errors import ( - ExchangeRequestError, ExchangeAuthEmpty, - ExchangeRequestErrorTooManyAttempts, - BaseCurrencyNotFoundError, ExchangeNotFoundError) -from catalyst.exchange.exchange_utils import get_exchange_auth, \ - get_algo_object, get_exchange_folder + ExchangeRequestError, ExchangeRequestErrorTooManyAttempts, + BaseCurrencyNotFoundError) +from catalyst.exchange.exchange_utils import get_algo_object from logbook import Logger from catalyst.constants import LOG_LEVEL @@ -95,7 +90,8 @@ def _run(handle_data, exchange, algo_namespace, base_currency, - live_graph): + live_graph, + simulate_orders): """Run a backtest for the given algorithm. This is shared between the cli and :func:`catalyst.run_algo`. @@ -282,7 +278,8 @@ def _run(handle_data, ExchangeTradingAlgorithmLive, exchanges=exchanges, algo_namespace=algo_namespace, - live_graph=live_graph + live_graph=live_graph, + simulate_orders=simulate_orders ) elif exchanges: # Removed the existing Poloniex fork to keep things simple @@ -444,6 +441,7 @@ def run_algorithm(initialize, base_currency=None, algo_namespace=None, live_graph=False, + simulate_orders=True, output=os.devnull): """Run a trading algorithm. @@ -565,5 +563,6 @@ def run_algorithm(initialize, exchange=exchange_name, algo_namespace=algo_namespace, base_currency=base_currency, - live_graph=live_graph + live_graph=live_graph, + simulate_orders=simulate_orders ) diff --git a/tests/exchange/test_ccxt.py b/tests/exchange/test_ccxt.py index eccdd18f..b1df6a19 100644 --- a/tests/exchange/test_ccxt.py +++ b/tests/exchange/test_ccxt.py @@ -15,7 +15,7 @@ log = Logger('test_ccxt') class TestCCXT(BaseExchangeTestCase): @classmethod def setup(self): - exchange_name = 'binance' + exchange_name = 'gdax' auth = get_exchange_auth(exchange_name) self.exchange = CCXT( exchange_name=exchange_name, @@ -64,25 +64,17 @@ class TestCCXT(BaseExchangeTestCase): start_dt=pd.to_datetime('2017-01-01', utc=True) ) - df = pd.DataFrame(candles) - df.set_index('last_traded', drop=True, inplace=True) - - folder = os.path.join( - tempfile.gettempdir(), 'catalyst', self.exchange.name, 'eth_btc' - ) - ensure_directory(folder) - - path = os.path.join(folder, 'output.csv') - df.to_csv(path) + for asset in candles: + df = pd.DataFrame(candles[asset]) + df.set_index('last_traded', drop=True, inplace=True) pass def test_tickers(self): log.info('retrieving tickers') tickers = self.exchange.tickers([ self.exchange.get_asset('eth_btc'), - self.exchange.get_asset('etc_btc') ]) - assert len(tickers) == 2 + assert len(tickers) == 1 pass def test_get_balances(self): From a3838fc00f89597f4ee8d65df733aa0ba3ce62ea Mon Sep 17 00:00:00 2001 From: fredfortier Date: Sat, 2 Dec 2017 20:20:57 -0500 Subject: [PATCH 11/13] BLD: tested ccxt with manual data ingestion --- catalyst/constants.py | 3 +- catalyst/exchange/ccxt/ccxt_exchange.py | 154 +++++++++++++++--------- catalyst/exchange/exchange.py | 54 +++++---- catalyst/exchange/exchange_bundle.py | 68 ++++++----- catalyst/exchange/exchange_utils.py | 19 ++- 5 files changed, 183 insertions(+), 115 deletions(-) diff --git a/catalyst/constants.py b/catalyst/constants.py index 35e1f727..6372b11f 100644 --- a/catalyst/constants.py +++ b/catalyst/constants.py @@ -7,7 +7,8 @@ import logbook For example, if you want to see the DEBUG messages, run: $ export CATALYST_LOG_LEVEL=10 ''' -LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO)) +# LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO)) +LOG_LEVEL = logbook.DEBUG SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \ '{exchange}/symbols.json' diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index d2c735cc..4d5b3939 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -57,12 +57,14 @@ class CCXT(Exchange): except Exception: raise ExchangeNotFoundError(exchange_name=exchange_name) - markets = self.api.load_markets() - log.debug('the markets:\n{}'.format(markets)) + self._symbol_maps = [None, None] + + markets_symbols = self.api.load_markets() + log.debug('the markets:\n{}'.format(markets_symbols)) self.name = exchange_name - self.assets = dict() + self.markets = self.api.fetch_markets() self.load_assets() self.base_currency = base_currency @@ -81,6 +83,14 @@ class CCXT(Exchange): def time_skew(self): return None + def get_market(self, symbol): + s = self.get_symbol(symbol) + market = next( + (market for market in self.markets if market['symbol'] == s), + None, + ) + return market + def get_symbol(self, asset_or_symbol): symbol = asset_or_symbol if isinstance( asset_or_symbol, string_types @@ -158,74 +168,102 @@ class CCXT(Exchange): except ExchangeSymbolsNotFound: return None - def _fetch_asset(self, market_id, is_local=False): + def fetch_asset_defs(self, market): + asset_defs = [] + + for is_local in (False, True): + asset_def = self.fetch_asset_def(market, is_local) + asset_defs.append((asset_def, is_local)) + + return asset_defs + + def fetch_asset_def(self, market, is_local=False): + exchange_symbol = market['id'] + symbol_map = self._fetch_symbol_map(is_local) if symbol_map is not None: assets_lower = {k.lower(): v for k, v in symbol_map.items()} - key = market_id.lower() + key = exchange_symbol.lower() asset = assets_lower[key] if key in assets_lower else None if asset is not None: - return asset, is_local - - elif not is_local: - return self._fetch_asset(market_id, True) + return asset else: - return None, is_local - - elif not is_local: - return self._fetch_asset(market_id, True) + return None else: - return None, is_local + return None + + def create_trading_pair(self, market, asset_def, is_local): + """ + Creating a TradingPair from market and asset data. + + Parameters + ---------- + market: dict[str, Object] + asset_def: dict[str, Object] + is_local: bool + + Returns + ------- + + """ + data_source = 'local' if is_local else 'catalyst' + params = dict( + exchange=self.name, + data_source=data_source, + exchange_symbol=market['id'], + ) + mixin_market_params(self.name, params, market) + + if asset_def is not None: + params['symbol'] = asset_def['symbol'] + + params['start_date'] = asset_def['start_date'] \ + if 'start_date' in asset_def else None + + params['end_date'] = asset_def['end_date'] \ + if 'end_date' in asset_def else None + + params['leverage'] = asset_def['leverage'] \ + if 'leverage' in asset_def else 1.0 + + params['asset_name'] = asset_def['asset_name'] \ + if 'asset_name' in asset_def else None + + params['end_daily'] = asset_def['end_daily'] \ + if 'end_daily' in asset_def \ + and asset_def['end_daily'] != 'N/A' else None + + params['end_minute'] = asset_def['end_minute'] \ + if 'end_minute' in asset_def \ + and asset_def['end_minute'] != 'N/A' else None + + else: + params['symbol'] = self.get_catalyst_symbol(market) + params['leverage'] = 1.0 + + return TradingPair(**params) def load_assets(self): - markets = self.api.fetch_markets() + self.assets = [] - for market in markets: - asset, is_local = self._fetch_asset(market['id']) - data_source = 'local' if is_local else 'catalyst' + for market in self.markets: + log.debug('fetching asset for market: {}'.format(market['id'])) + asset_defs = self.fetch_asset_defs(market) - params = dict( - exchange=self.name, - data_source=data_source, - exchange_symbol=market['id'], - ) - mixin_market_params(self.name, params, market) - - if asset is not None: - params['symbol'] = asset['symbol'] - - params['start_date'] = pd.to_datetime( - asset['start_date'], utc=True - ) if 'start_date' in asset else None - - params['end_date'] = pd.to_datetime( - asset['end_date'], utc=True - ) if 'end_date' in asset else None - - params['leverage'] = asset['leverage'] \ - if 'leverage' in asset else 1.0 - - params['asset_name'] = asset['asset_name'] \ - if 'asset_name' in asset else None - - params['end_daily'] = pd.to_datetime( - asset['end_daily'], utc=True - ) if 'end_daily' in asset and asset['end_daily'] != 'N/A' \ - else None - - params['end_minute'] = pd.to_datetime( - asset['end_minute'], utc=True - ) if 'end_minute' in asset and asset['end_minute'] != 'N/A' \ - else None - - else: - params['symbol'] = self.get_catalyst_symbol(market) - - trading_pair = TradingPair(**params) - self.assets[market['id']] = trading_pair + for asset_def in asset_defs: + if asset_def[0] is not None or not asset_defs[1]: + try: + asset = self.create_trading_pair( + market=market, + asset_def=asset_def[0], + is_local=asset_def[1] + ) + self.assets.append(asset) + except TypeError as e: + pass def get_balances(self): try: @@ -293,7 +331,7 @@ class CCXT(Exchange): order = Order( dt=date, - asset=self.assets[symbol], + asset=self.get_asset(symbol, is_exchange_symbol=True), amount=amount, stop=stop_price, limit=limit_price, diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 317175d0..f38bfa36 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -34,7 +34,8 @@ class Exchange: def __init__(self): self.name = None - self.assets = dict() + self.assets = [] + self._symbol_maps = [None, None] self._portfolio = None self.minute_writer = None self.minute_reader = None @@ -145,9 +146,9 @@ class Exchange: """ symbol = None - for key in self.assets: - if not symbol and self.assets[key].symbol == asset.symbol: - symbol = key + for a in self.assets: + if not symbol and a.symbol == asset.symbol: + symbol = a.symbol if not symbol: raise ValueError('Currency %s not supported by exchange %s' % @@ -187,33 +188,32 @@ class Exchange: list[TradingPair] """ - assets = [] if symbols is not None: + assets = [] for symbol in symbols: asset = self.get_asset(symbol, data_frequency) assets.append(asset) + return assets else: - for key in self.assets: - assets.append(self.assets[key]) + return self.assets - return assets - - def _find_asset(self, asset, symbol, data_frequency, is_local=False): - assets = self.assets - for key in assets: + def _find_asset(self, asset, symbol, data_frequency, is_exchange_symbol, + is_local=False): + for a in self.assets: has_data = (data_frequency == 'minute' - and assets[key].end_minute is not None) \ + and a.end_minute is not None) \ or (data_frequency == 'daily' - and assets[key].end_daily is not None) + and a.end_daily is not None) - if not asset and assets[key].symbol.lower() == symbol.lower() \ + symbol_attr = a.exchange_symbol if is_exchange_symbol else a.symbol + if not asset and symbol_attr.lower() == symbol.lower() \ and (not data_frequency or has_data): - asset = assets[key] + asset = a return asset - def get_asset(self, symbol, data_frequency=None): + def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False): """ The market for the specified symbol. @@ -229,16 +229,19 @@ class Exchange: asset = None log.debug('searching asset {} on the server'.format(symbol)) - asset = self._find_asset(asset, symbol, data_frequency, False) + asset = self._find_asset( + asset, symbol, data_frequency, is_exchange_symbol, False + ) log.debug('asset {} not found on the server, searching local ' 'assets'.format(symbol)) - asset = self._find_asset(asset, symbol, data_frequency, True) + asset = self._find_asset( + asset, symbol, data_frequency, is_exchange_symbol, True + ) if not asset: - all_values = list(self.assets.values()) supported_symbols = sorted([ - asset.symbol for asset in all_values + asset.symbol for asset in self.assets ]) raise SymbolNotFoundOnExchange( @@ -250,7 +253,14 @@ class Exchange: return asset def fetch_symbol_map(self, is_local=False): - return get_exchange_symbols(self.name, is_local) + index = 1 if is_local else 0 + if self._symbol_maps[index] is not None: + return self._symbol_maps[index] + + else: + symbol_map = get_exchange_symbols(self.name, is_local) + self._symbol_maps[index] = symbol_map + return symbol_map @abstractmethod def load_assets(self, is_local=False): diff --git a/catalyst/exchange/exchange_bundle.py b/catalyst/exchange/exchange_bundle.py index 2bfa6201..4f4e7fc6 100644 --- a/catalyst/exchange/exchange_bundle.py +++ b/catalyst/exchange/exchange_bundle.py @@ -31,7 +31,7 @@ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \ PricingDataNotLoadedError, DataCorruptionError, ExchangeSymbolsNotFound, \ PricingDataValueError from catalyst.exchange.exchange_utils import get_exchange_folder, \ - get_exchange_symbols, save_exchange_symbols + get_exchange_symbols, save_exchange_symbols, mixin_market_params from catalyst.utils.cli import maybe_show_progress from catalyst.utils.paths import ensure_directory @@ -667,12 +667,11 @@ class ExchangeBundle: """ log.info('ingesting csv file: {}'.format(path)) - try: - symbols_def = get_exchange_symbols( - self.exchange_name, is_local=True - ) - except ExchangeSymbolsNotFound: - symbols_def = dict() + + if self.exchange is None: + # Avoid circular dependencies + from catalyst.exchange.factory import get_exchange + self.exchange = get_exchange(self.exchange_name) problems = [] df = pd.read_csv( @@ -705,24 +704,40 @@ class ExchangeBundle: end_dt = df.index.get_level_values(1).max() end_dt_key = 'end_{}'.format(data_frequency) - if symbol is symbols_def: - symbol_def = symbols_def[symbol] + market = self.exchange.get_market(symbol) + if market is None: + raise ValueError('symbol not available in the exchange.') - start_dt = symbol_def['start_date'] \ - if symbol_def['start_date'] < start_dt else start_dt + params = dict( + exchange=self.exchange.name, + data_source='local', + exchange_symbol=market['id'], + ) + mixin_market_params(self.exchange_name, params, market) - end_dt = symbol_def[end_dt_key] \ - if symbol_def[end_dt_key] > end_dt else end_dt + asset_def = self.exchange.fetch_asset_def(market, True) + if asset_def is not None: + params['symbol'] = asset_def['symbol'] - end_daily = end_dt \ - if data_frequency == 'daily' else symbol_def['end_daily'] + params['start_date'] = asset_def['start_date'] \ + if asset_def['start_date'] < start_dt else start_dt - end_minute = end_dt \ - if data_frequency == 'minute' else symbol_def['end_minute'] + params['end_date'] = asset_def[end_dt_key] \ + if asset_def[end_dt_key] > end_dt else end_dt + + params['end_daily'] = end_dt \ + if data_frequency == 'daily' else asset_def['end_daily'] + + params['end_minute'] = end_dt \ + if data_frequency == 'minute' else asset_def['end_minute'] else: - end_daily = end_dt if data_frequency == 'daily' else 'N/A' - end_minute = end_dt if data_frequency == 'minute' else 'N/A' + params['symbol'] = self.exchange.get_catalyst_symbol(market) + + params['end_daily'] = end_dt \ + if data_frequency == 'daily' else 'N/A' + params['end_minute'] = end_dt \ + if data_frequency == 'minute' else 'N/A' if min_start_dt is None or start_dt < min_start_dt: min_start_dt = start_dt @@ -730,19 +745,8 @@ class ExchangeBundle: if max_end_dt is None or end_dt > max_end_dt: max_end_dt = end_dt - asset = TradingPair( - symbol=symbol, - exchange=self.exchange_name, - start_date=start_dt, - end_date=end_dt, - leverage=0, # TODO: add as an optional column - asset_name=symbol, - min_trade_size=0, # TODO: add as an optional column - end_daily=end_daily, - end_minute=end_minute, - exchange_symbol=symbol - ) - assets[symbol] = asset + asset = TradingPair(**params) + assets[market['id']] = asset save_exchange_symbols(self.exchange_name, assets, True) diff --git a/catalyst/exchange/exchange_utils.py b/catalyst/exchange/exchange_utils.py index 0ca18c95..c2250187 100644 --- a/catalyst/exchange/exchange_utils.py +++ b/catalyst/exchange/exchange_utils.py @@ -8,6 +8,7 @@ from datetime import date, datetime import pandas as pd from catalyst.assets._assets import TradingPair +from six import string_types from six.moves.urllib import request from catalyst.constants import DATE_FORMAT, SYMBOLS_URL @@ -100,6 +101,20 @@ def download_exchange_symbols(exchange_name, environ=None): return response +def symbols_parser(asset_def): + for key, value in asset_def.items(): + match = isinstance(value, string_types) \ + and re.search(r'(\d{4}-\d{2}-\d{2})', value) + + if match: + try: + asset_def[key] = pd.to_datetime(value, utc=True) + except ValueError: + pass + + return asset_def + + def get_exchange_symbols(exchange_name, is_local=False, environ=None): """ The de-serialized content of the exchange's symbols.json. @@ -125,10 +140,10 @@ def get_exchange_symbols(exchange_name, is_local=False, environ=None): if os.path.isfile(filename): with open(filename) as data_file: try: - data = json.load(data_file) + data = json.load(data_file, object_hook=symbols_parser) return data - except ValueError: + except ValueError as e: return dict() else: raise ExchangeSymbolsNotFound( From f995f451a7d5158d50774d598ae57574fe6a3be9 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Sat, 2 Dec 2017 21:27:03 -0500 Subject: [PATCH 12/13] BLD: some refactoring to simplify the integration logic and tested several algos --- catalyst/assets/_assets.pyx | 6 +- catalyst/exchange/ccxt/ccxt_exchange.py | 88 +++++++++++++++++++-- catalyst/exchange/exchange.py | 100 ++++++++++++++++-------- catalyst/exchange/exchange_bundle.py | 2 +- 4 files changed, 153 insertions(+), 43 deletions(-) diff --git a/catalyst/assets/_assets.pyx b/catalyst/assets/_assets.pyx index feb41710..af1e81a6 100644 --- a/catalyst/assets/_assets.pyx +++ b/catalyst/assets/_assets.pyx @@ -516,7 +516,7 @@ cdef class TradingPair(Asset): symbol = symbol.lower() try: - self.base_currency,self.quote_currency = symbol.split('_') + self.base_currency, self.quote_currency = symbol.split('_') except Exception as e: raise InvalidSymbolError(symbol=symbol, error=e) @@ -600,8 +600,8 @@ cdef class TradingPair(Asset): ------- boolean: whether the asset's exchange is open at the given minute. """ - #TODO: consider implementing to spot holds - return self.trading_state > 0 + #TODO: make more dymanic to catch holds + return True cpdef __reduce__(self): """ diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index 4d5b3939..ac978e58 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -84,6 +84,19 @@ class CCXT(Exchange): return None def get_market(self, symbol): + """ + The CCXT market. + + Parameters + ---------- + symbol: + The CCXT symbol. + + Returns + ------- + dict[str, Object] + + """ s = self.get_symbol(symbol) market = next( (market for market in self.markets if market['symbol'] == s), @@ -92,6 +105,17 @@ class CCXT(Exchange): return market def get_symbol(self, asset_or_symbol): + """ + The CCXT symbol. + + Parameters + ---------- + asset_or_symbol + + Returns + ------- + + """ symbol = asset_or_symbol if isinstance( asset_or_symbol, string_types ) else asset_or_symbol.symbol @@ -100,6 +124,17 @@ class CCXT(Exchange): return '{}/{}'.format(parts[0].upper(), parts[1].upper()) def get_catalyst_symbol(self, market_or_symbol): + """ + The Catalyst symbol. + + Parameters + ---------- + market_or_symbol + + Returns + ------- + + """ if isinstance(market_or_symbol, string_types): parts = market_or_symbol.split('/') return '{}_{}'.format(parts[0].lower(), parts[1].lower()) @@ -111,6 +146,19 @@ class CCXT(Exchange): ) def get_timeframe(self, freq): + """ + The CCXT timeframe from the Catalyst frequency. + + Parameters + ---------- + freq: str + The Catalyst frequency (Pandas convention) + + Returns + ------- + str + + """ freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I) if freq_match: candle_size = int(freq_match.group(1)) \ @@ -168,16 +216,47 @@ class CCXT(Exchange): except ExchangeSymbolsNotFound: return None - def fetch_asset_defs(self, market): + def get_asset_defs(self, market): + """ + The local and Catalyst definitions of the specified market. + + Parameters + ---------- + market: dict[str, Object] + The CCXT market dicts. + + Returns + ------- + dict[str, Object] + The asset definition. + + """ asset_defs = [] for is_local in (False, True): - asset_def = self.fetch_asset_def(market, is_local) + asset_def = self.get_asset_def(market, is_local) asset_defs.append((asset_def, is_local)) return asset_defs - def fetch_asset_def(self, market, is_local=False): + def get_asset_def(self, market, is_local=False): + """ + The asset definition (in symbols.json files) corresponding + to the the specified market. + + Parameters + ---------- + market: dict[str, Object] + The CCXT market dict. + is_local + Whether to search in local or Catalyst asset definitions. + + Returns + ------- + dict[str, Object] + The asset definition. + + """ exchange_symbol = market['id'] symbol_map = self._fetch_symbol_map(is_local) @@ -250,8 +329,7 @@ class CCXT(Exchange): self.assets = [] for market in self.markets: - log.debug('fetching asset for market: {}'.format(market['id'])) - asset_defs = self.fetch_asset_defs(market) + asset_defs = self.get_asset_defs(market) for asset_def in asset_defs: if asset_def[0] is not None or not asset_defs[1]: diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index f38bfa36..0301ebc0 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -175,71 +175,102 @@ class Exchange: return symbols - def get_assets(self, symbols=None, data_frequency=None): + def get_assets(self, symbols=None, data_frequency=None, + is_exchange_symbol=False, + is_local=None): """ The list of markets for the specified symbols. Parameters ---------- symbols: list[str] + data_frequency: str + is_exchange_symbol: bool + is_local: bool Returns ------- list[TradingPair] + A list of asset objects. + + Notes + ----- + See get_asset for details of each parameter. """ + if symbols is None: + # Make a distinct list of all symbols + symbols = list(set([asset.symbol for asset in self.assets])) + is_exchange_symbol = False - if symbols is not None: - assets = [] - for symbol in symbols: - asset = self.get_asset(symbol, data_frequency) - assets.append(asset) - return assets - else: - return self.assets + assets = [] + for symbol in symbols: + asset = self.get_asset( + symbol, data_frequency, is_exchange_symbol, is_local + ) + assets.append(asset) + return assets - def _find_asset(self, asset, symbol, data_frequency, is_exchange_symbol, - is_local=False): - for a in self.assets: - has_data = (data_frequency == 'minute' - and a.end_minute is not None) \ - or (data_frequency == 'daily' - and a.end_daily is not None) - - symbol_attr = a.exchange_symbol if is_exchange_symbol else a.symbol - if not asset and symbol_attr.lower() == symbol.lower() \ - and (not data_frequency or has_data): - asset = a - - return asset - - def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False): + def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False, + is_local=None): """ The market for the specified symbol. Parameters ---------- symbol: str + The Catalyst or exchange symbol. + + data_frequency: str + Check for asset corresponding to the specified data_frequency. + The same asset might exist in the Catalyst repository or + locally (following a CSV ingestion). Filtering by + data_frequency picks the right asset. + + is_exchange_symbol: bool + Whether the symbol uses the Catalyst or exchange convention. + + is_local: bool + For the local or Catalyst asset. Returns ------- TradingPair + The asset object. """ asset = None - log.debug('searching asset {} on the server'.format(symbol)) - asset = self._find_asset( - asset, symbol, data_frequency, is_exchange_symbol, False + log.debug( + 'searching assets for: {} {}'.format( + self.name, symbol + ) ) + for a in self.assets: + if asset is not None: + break - log.debug('asset {} not found on the server, searching local ' - 'assets'.format(symbol)) - asset = self._find_asset( - asset, symbol, data_frequency, is_exchange_symbol, True - ) + if is_local is not None: + data_source = 'local' if is_local else 'catalyst' + applies = (a.data_source == data_source) + + elif data_frequency is not None: + applies = ( + (data_frequency == 'minute' and a.end_minute is not None) + or (data_frequency == 'daily' and a.end_daily is not None) + ) + + else: + applies = True + + # The symbol provided may use the Catalyst or the exchange + # convention + key = a.exchange_symbol if is_exchange_symbol else a.symbol + if not asset and key.lower() == symbol.lower() and applies: + asset = a + + if asset is None: - if not asset: supported_symbols = sorted([ asset.symbol for asset in self.assets ]) @@ -250,6 +281,7 @@ class Exchange: supported_symbols=supported_symbols ) + log.debug('found asset: {}'.format(asset)) return asset def fetch_symbol_map(self, is_local=False): diff --git a/catalyst/exchange/exchange_bundle.py b/catalyst/exchange/exchange_bundle.py index 4f4e7fc6..07bc0207 100644 --- a/catalyst/exchange/exchange_bundle.py +++ b/catalyst/exchange/exchange_bundle.py @@ -715,7 +715,7 @@ class ExchangeBundle: ) mixin_market_params(self.exchange_name, params, market) - asset_def = self.exchange.fetch_asset_def(market, True) + asset_def = self.exchange.get_asset_def(market, True) if asset_def is not None: params['symbol'] = asset_def['symbol'] From 96a27d083c6c589819fa0311345def8a9548f1c8 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Sun, 3 Dec 2017 00:04:15 -0500 Subject: [PATCH 13/13] BLD: more live trading tests and fixed related issues --- catalyst/examples/mean_reversion_simple.py | 8 ++++---- catalyst/exchange/ccxt/ccxt_exchange.py | 23 +++++++++++++++------- catalyst/exchange/exchange.py | 4 ++-- catalyst/exchange/exchange_bundle.py | 6 ++---- catalyst/exchange/stats_utils.py | 6 +++--- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/catalyst/examples/mean_reversion_simple.py b/catalyst/examples/mean_reversion_simple.py index cd1ac8dc..130343c9 100644 --- a/catalyst/examples/mean_reversion_simple.py +++ b/catalyst/examples/mean_reversion_simple.py @@ -37,7 +37,7 @@ def initialize(context): context.base_price = None context.current_day = None - context.RSI_OVERSOLD = 25 + context.RSI_OVERSOLD = 55 context.RSI_OVERBOUGHT = 82 context.CANDLE_SIZE = '5T' @@ -239,7 +239,7 @@ def analyze(context=None, perf=None): if __name__ == '__main__': # The execution mode: backtest or live - MODE = 'backtest' + MODE = 'live' if MODE == 'backtest': folder = os.path.join( @@ -251,7 +251,7 @@ if __name__ == '__main__': out = os.path.join(folder, '{}.p'.format(timestr)) # catalyst run -f catalyst/examples/mean_reversion_simple.py -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 run_algorithm( - capital_base=0.5, + capital_base=0.1, data_frequency='minute', initialize=initialize, handle_data=handle_data, @@ -267,7 +267,7 @@ if __name__ == '__main__': elif MODE == 'live': run_algorithm( - capital_base=0.5, + capital_base=0.1, initialize=initialize, handle_data=handle_data, analyze=analyze, diff --git a/catalyst/exchange/ccxt/ccxt_exchange.py b/catalyst/exchange/ccxt/ccxt_exchange.py index ac978e58..36a0bbdb 100644 --- a/catalyst/exchange/ccxt/ccxt_exchange.py +++ b/catalyst/exchange/ccxt/ccxt_exchange.py @@ -321,6 +321,7 @@ class CCXT(Exchange): else: params['symbol'] = self.get_catalyst_symbol(market) + # TODO: add as an optional column params['leverage'] = 1.0 return TradingPair(**params) @@ -340,7 +341,8 @@ class CCXT(Exchange): is_local=asset_def[1] ) self.assets.append(asset) - except TypeError as e: + + except TypeError: pass def get_balances(self): @@ -405,7 +407,11 @@ class CCXT(Exchange): # order_id = str(order_status['info']['clientOrderId']) order_id = order_status['id'] - symbol = order_status['info']['symbol'] + + # TODO: this won't work, redo the packages with a different key. + symbol = order_status['info']['symbol'] \ + if 'symbol' in order_status['info'] \ + else order_status['info']['Exchange'] order = Order( dt=date, @@ -455,10 +461,9 @@ class CCXT(Exchange): if 'info' not in result: raise ValueError('cannot use order without info attribute') - # order_id = str(result['info']['clientOrderId']) order_id = result['id'] order = Order( - dt=from_ms_timestamp(result['info']['transactTime']), + dt=pd.Timestamp.utcnow(), asset=asset, amount=amount, stop=style.get_stop_price(is_buy), @@ -490,7 +495,7 @@ class CCXT(Exchange): def _get_asset_from_order(self, order_id): open_orders = self.portfolio.open_orders order = next( - (order for order in open_orders if order.id == order_id), + (open_orders[id] for id in open_orders if id == order_id), None ) # type: Order return order.asset if order is not None else None @@ -508,12 +513,12 @@ class CCXT(Exchange): symbol = self.get_symbol(asset_or_symbol) \ if asset_or_symbol is not None else None order_status = self.api.fetch_order(id=order_id, symbol=symbol) - order, _ = self._create_order(order_status) + order, executed_price = self._create_order(order_status) except Exception as e: raise ExchangeRequestError(error=e) - return order + return order, executed_price def cancel_order(self, order_param, asset_or_symbol=None): order_id = order_param.id \ @@ -555,6 +560,10 @@ class CCXT(Exchange): ticker['last_traded'] = from_ms_timestamp(ticker['timestamp']) + if 'last_price' not in ticker: + # TODO: any more exceptions? + ticker['last_price'] = ticker['last'] + # Using the volume represented in the base currency ticker['volume'] = ticker['baseVolume'] \ if 'baseVolume' in ticker else 0 diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 0301ebc0..a8d58069 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -270,7 +270,6 @@ class Exchange: asset = a if asset is None: - supported_symbols = sorted([ asset.symbol for asset in self.assets ]) @@ -704,8 +703,9 @@ class Exchange: # TODO: convert if the position is not in the base currency ticker = tickers[asset] position = portfolio.positions[asset] + position.last_sale_price = ticker['last_price'] - position.last_sale_date = ticker['timestamp'] + position.last_sale_date = ticker['last_traded'] portfolio.positions_value += \ position.amount * position.last_sale_price diff --git a/catalyst/exchange/exchange_bundle.py b/catalyst/exchange/exchange_bundle.py index 07bc0207..59e5ce90 100644 --- a/catalyst/exchange/exchange_bundle.py +++ b/catalyst/exchange/exchange_bundle.py @@ -1,5 +1,4 @@ import os -import os import shutil from datetime import datetime, timedelta from functools import partial @@ -28,10 +27,9 @@ from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \ TempBundleNotFoundError, \ NoDataAvailableOnExchange, \ - PricingDataNotLoadedError, DataCorruptionError, ExchangeSymbolsNotFound, \ - PricingDataValueError + PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError from catalyst.exchange.exchange_utils import get_exchange_folder, \ - get_exchange_symbols, save_exchange_symbols, mixin_market_params + save_exchange_symbols, mixin_market_params from catalyst.utils.cli import maybe_show_progress from catalyst.utils.paths import ensure_directory diff --git a/catalyst/exchange/stats_utils.py b/catalyst/exchange/stats_utils.py index e982de2a..845fccc8 100644 --- a/catalyst/exchange/stats_utils.py +++ b/catalyst/exchange/stats_utils.py @@ -153,11 +153,11 @@ def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10): def format_positions(positions): parts = [] for position in positions: - msg = '{amount:.2f}{market} cost basis {cost_basis:.4f}{base}'.format( + msg = '{amount:.2f}{base} cost basis {cost_basis:.4f}{quote}'.format( amount=position['amount'], - market=position['sid'].market_currency, + base=position['sid'].base_currency, cost_basis=position['cost_basis'], - base=position['sid'].base_currency + quote=position['sid'].quote_currency ) parts.append(msg) return ', '.join(parts)