diff --git a/catalyst/exchange/bitfinex/bitfinex.py b/catalyst/exchange/bitfinex/bitfinex.py index e684e397..cbecfa9c 100644 --- a/catalyst/exchange/bitfinex/bitfinex.py +++ b/catalyst/exchange/bitfinex/bitfinex.py @@ -166,13 +166,8 @@ class Bitfinex(Exchange): return order, executed_price - def update_portfolio(self): - """ - Update the portfolio cash and position balances based on the - latest ticker prices. - - :return: - """ + def get_balances(self): + log.debug('retrieving wallets balances') try: response = self._request('balances', None) balances = response.json() @@ -184,36 +179,12 @@ class Bitfinex(Exchange): error='unable to fetch balance {}'.format(balances['message']) ) - base_position = None - for position in balances: - if not base_position and position['type'] == 'exchange' \ - and position['currency'] == self.base_currency: - base_position = position + std_balances = dict() + for balance in balances: + currency = balance['currency'].lower() + std_balances[currency] = float(balance['available']) - if position is None: - raise ValueError( - error='Base currency %s not found in portfolio' % self.base_currency - ) - - portfolio = self._portfolio - portfolio.cash = float(base_position['available']) - if portfolio.starting_cash is None: - portfolio.starting_cash = portfolio.cash - - if portfolio.positions: - assets = portfolio.positions.keys() - tickers = self.tickers(assets) - portfolio.positions_value = 0.0 - for ticker in tickers: - # TODO: convert if the position is not in the base currency - position = portfolio.positions[ticker['asset']] - position.last_sale_price = ticker['last_price'] - position.last_sale_date = ticker['timestamp'] - - portfolio.positions_value += \ - position.amount * position.last_sale_price - portfolio.portfolio_value = \ - portfolio.positions_value + portfolio.cash + return std_balances @property def account(self): @@ -397,17 +368,17 @@ class Bitfinex(Exchange): date = pd.Timestamp.utcnow() try: response = self._request('order/new', req) - exchange_order = response.json() + order_status = response.json() except Exception as e: raise ExchangeRequestError(error=e) - if 'message' in exchange_order: + if 'message' in order_status: raise ExchangeRequestError( error='unable to create Bitfinex order {}'.format( - exchange_order['message']) + order_status['message']) ) - order_id = exchange_order['id'] + order_id = str(order_status['id']) order = Order( dt=date, asset=asset, @@ -538,15 +509,14 @@ class Bitfinex(Exchange): tickers = response.json() - formatted_tickers = [] + ticks = dict() for index, ticker in enumerate(tickers): if not len(ticker) == 11: raise ExchangeRequestError( error='Invalid ticker in response: {}'.format(ticker) ) - tick = dict( - asset=assets[index], + ticks[assets[index]] = dict( timestamp=pd.Timestamp.utcnow(), bid=ticker[1], ask=ticker[3], @@ -555,7 +525,6 @@ class Bitfinex(Exchange): high=ticker[9], volume=ticker[8], ) - formatted_tickers.append(tick) - log.debug('got tickers {}'.format(formatted_tickers)) - return formatted_tickers + log.debug('got tickers {}'.format(ticks)) + return ticks diff --git a/catalyst/exchange/bittrex/bittrex.py b/catalyst/exchange/bittrex/bittrex.py index c91b9b31..b1df213e 100644 --- a/catalyst/exchange/bittrex/bittrex.py +++ b/catalyst/exchange/bittrex/bittrex.py @@ -80,8 +80,17 @@ class Bittrex(Exchange): return symbol_map - def update_portfolio(self): - pass + def get_balances(self): + try: + balances = self.api.getbalances() + except Exception as e: + raise ExchangeRequestError(error=e) + + std_balances = dict() + for balance in balances: + currency = balance['Currency'].lower() + std_balances[currency] = balance['Available'] + return std_balances def create_order(self, asset, amount, is_buy, style): log.info('creating order') @@ -249,9 +258,34 @@ class Bittrex(Exchange): return ohlc_map[assets] \ if isinstance(assets, TradingPair) else ohlc_map - def tickers(self): + def tickers(self, assets): + """ + As of v1.1, Bittrex only allows one ticker at the time. + So we have to make multiple calls to fetch multiple assets. + + :param assets: + :return: + """ log.info('retrieving tickers') - pass + + ticks = dict() + for asset in assets: + symbol = self.get_symbol(asset) + try: + ticker = self.api.getticker(symbol) + except Exception as e: + raise ExchangeRequestError(error=e) + + # TODO: catch invalid ticker + ticks[asset] = dict( + timestamp=pd.Timestamp.utcnow(), + bid=ticker['Bid'], + ask=ticker['Ask'], + last_price=ticker['Last'] + ) + + log.debug('got tickers {}'.format(ticks)) + return ticks def get_account(self): log.info('retrieving account data') diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 790f6458..ecc59f9e 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -14,7 +14,7 @@ from catalyst.errors import ( SymbolNotFound, ) from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ - InvalidOrderStyle + InvalidOrderStyle, BaseCurrencyNotFoundError from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ ExchangeLimitOrder, ExchangeStopOrder from catalyst.exchange.exchange_portfolio import ExchangePortfolio @@ -35,15 +35,12 @@ class Exchange: self._portfolio = None self.minute_writer = None self.minute_reader = None + self.base_currency = None @abstractproperty def positions(self): pass - @abstractproperty - def update_portfolio(self): - pass - @property def portfolio(self): """ @@ -113,7 +110,7 @@ class Exchange: asset = self.assets[key] if not asset: - raise SymbolNotFound('Asset not found: %s' % symbol) + raise SymbolNotFound(symbol=symbol) return asset @@ -379,6 +376,55 @@ class Exchange: df = pd.concat(series) return df + def update_portfolio(self): + """ + Update the portfolio cash and position balances based on the + latest ticker prices. + + :return: + """ + balances = self.get_balances() + + base_position_available = balances[self.base_currency] \ + if self.base_currency in balances else None + + if base_position_available is None: + raise BaseCurrencyNotFoundError( + base_currency=self.base_currency, + exchange=self.name + ) + + portfolio = self._portfolio + portfolio.cash = base_position_available + + if portfolio.starting_cash is None: + portfolio.starting_cash = portfolio.cash + + if portfolio.positions: + assets = portfolio.positions.keys() + tickers = self.tickers(assets) + + portfolio.positions_value = 0.0 + for asset in tickers: + # 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'] + + portfolio.positions_value += \ + position.amount * position.last_sale_price + portfolio.portfolio_value = \ + portfolio.positions_value + portfolio.cash + + @abstractmethod + def get_balances(self): + """ + Retrieve wallet balances for the exchange + :return balances: A dict of currency => available balance + """ + pass + @abstractmethod def create_order(self, asset, amount, is_buy, style): pass diff --git a/catalyst/exchange/exchange_errors.py b/catalyst/exchange/exchange_errors.py index b23fcdec..0cfa35b3 100644 --- a/catalyst/exchange/exchange_errors.py +++ b/catalyst/exchange/exchange_errors.py @@ -99,6 +99,13 @@ class SidHashError(ZiplineError): ).strip() +class BaseCurrencyNotFoundError(ZiplineError): + msg = ( + 'Algorithm base currency {base_currency} not found in exchange ' + '{exchange}.' + ).strip() + + class MismatchingBaseCurrencies(ZiplineError): msg = ( 'Unable to trade with base currency {base_currency} when the ' diff --git a/tests/exchange/base.py b/tests/exchange/base.py index c81abf66..73c43017 100644 --- a/tests/exchange/base.py +++ b/tests/exchange/base.py @@ -30,5 +30,9 @@ class BaseExchangeTestCase(): pass @abstractmethod - def get_account(self): + def test_get_balances(self): + pass + + @abstractmethod + def test_get_account(self): pass diff --git a/tests/exchange/test_bitfinex.py b/tests/exchange/test_bitfinex.py index 4fd7b8ca..0315ef6b 100644 --- a/tests/exchange/test_bitfinex.py +++ b/tests/exchange/test_bitfinex.py @@ -37,6 +37,7 @@ class BitfinexTestCase(BaseExchangeTestCase): def test_open_orders(self): log.info('retrieving open orders') + orders = self.exchange.get_open_orders() pass def test_get_order(self): @@ -53,12 +54,21 @@ class BitfinexTestCase(BaseExchangeTestCase): def test_tickers(self): log.info('retrieving tickers') + tickers = self.exchange.tickers([ + self.exchange.get_asset('eth_usd'), + self.exchange.get_asset('btc_usd') + ]) pass - def get_account(self): + def test_get_account(self): log.info('retrieving account data') pass + def test_get_balances(self): + log.info('testing exchange balances') + balances = self.exchange.get_balances() + pass + # def test_order(self): # log.info('ordering from bitfinex') # bitfinex = Bitfinex() diff --git a/tests/exchange/test_bittrex.py b/tests/exchange/test_bittrex.py index bf8fed1a..2ad863eb 100644 --- a/tests/exchange/test_bittrex.py +++ b/tests/exchange/test_bittrex.py @@ -1,11 +1,7 @@ from catalyst.exchange.bittrex.bittrex import Bittrex +from catalyst.finance.order import Order from .base import BaseExchangeTestCase from logbook import Logger -import pandas as pd -from catalyst.finance.execution import (MarketOrder, - LimitOrder, - StopOrder, - StopLimitOrder) from catalyst.exchange.exchange_utils import get_exchange_auth log = Logger('test_bittrex') @@ -31,6 +27,7 @@ class BittrexTestCase(BaseExchangeTestCase): amount=1, ) log.info('order created {}'.format(order_id)) + assert order_id is not None pass def test_open_orders(self): @@ -39,10 +36,12 @@ class BittrexTestCase(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( + u'2c584020-9caf-4af5-bde0-332c0bba17e2') + assert isinstance(order, Order) pass - def test_cancel_order(self,): + def test_cancel_order(self, ): log.info('cancel order') self.exchange.cancel_order(u'dc7bcca2-5219-4145-8848-8a593d2a72f9') pass @@ -65,8 +64,18 @@ class BittrexTestCase(BaseExchangeTestCase): def test_tickers(self): log.info('retrieving tickers') + tickers = self.exchange.tickers([ + self.exchange.get_asset('ubq_btc'), + self.exchange.get_asset('neo_btc') + ]) + assert len(tickers) == 2 pass - def get_account(self): - log.info('retrieving account data') + 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