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