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):