From 383370f9d85aa67a6a7e3f8671f330057d8ff86d Mon Sep 17 00:00:00 2001 From: Frederic Fortier Date: Mon, 14 Aug 2017 09:01:41 -0400 Subject: [PATCH] Code documentation and cleanup --- catalyst/exchange/bitfinex.py | 251 ++++++++++++++++++++++++-------- catalyst/exchange/exchange.py | 103 ++++++++++--- tests/exchange/base.py | 43 ++++-- tests/exchange/test_bitfinex.py | 70 ++++++--- 4 files changed, 350 insertions(+), 117 deletions(-) diff --git a/catalyst/exchange/bitfinex.py b/catalyst/exchange/bitfinex.py index 9d4c2294..b3109cad 100644 --- a/catalyst/exchange/bitfinex.py +++ b/catalyst/exchange/bitfinex.py @@ -6,6 +6,7 @@ import json import time import requests import pandas as pd +from catalyst.protocol import Portfolio, Account # from websocket import create_connection from catalyst.exchange.exchange import Exchange, RTVolumeBar, Position from logbook import Logger @@ -18,7 +19,7 @@ from catalyst.finance.execution import (MarketOrder, BITFINEX_URL = 'https://api.bitfinex.com' BITFINEX_KEY = 'hjZ7DZzwbBZsIZPWeSSQtrWCPNwyhxw96r3LnY7jtOH' BITFINEX_SECRET = b'LilCoxcqUnHKBcGtrttwCIv4qONTdjuFMSdz8Rxh6OM' -ASSETS = '{ "btcusd": {"symbol":"btc_usd", "start_date": "2010-01-01"}, "ltcusd": {"symbol":"ltc-usd", "start_date": "2010-01-01"}, "ltcbtc": {"symbol":"ltc_btc", "start_date": "2010-01-01"}, "ethusd": {"symbol":"eth_usd", "start_date": "2010-01-01"}, "ethbtc": {"symbol":"eth_btc", "start_date": "2010-01-01"}, "etcbtc": {"symbol":"etc_btc", "start_date": "2010-01-01"}, "etcusd": {"symbol":"etc_usd", "start_date": "2010-01-01"}, "rrtusd": {"symbol":"rrt_usd", "start_date": "2010-01-01"}, "rrtbtc": {"symbol":"rrt_btc", "start_date": "2010-01-01"}, "zecusd": {"symbol":"zec_usd", "start_date": "2010-01-01"}, "zecbtc": {"symbol":"zec_btc", "start_date": "2010-01-01"}, "xmrusd": {"symbol":"xmr_usd", "start_date": "2010-01-01"}, "xmrbtc": {"symbol":"xmr_btc", "start_date": "2010-01-01"}, "dshusd": {"symbol":"dsh_usd", "start_date": "2010-01-01"}, "dshbtc": {"symbol":"dsh_btc", "start_date": "2010-01-01"}, "bccbtc": {"symbol":"bcc_btc", "start_date": "2010-01-01"}, "bcubtc": {"symbol":"bcu_btc", "start_date": "2010-01-01"}, "bccusd": {"symbol":"bcc_usd", "start_date": "2010-01-01"}, "bcuusd": {"symbol":"bcu_usd", "start_date": "2010-01-01"}, "xrpusd": {"symbol":"xrp_usd", "start_date": "2010-01-01"}, "xrpbtc": {"symbol":"xrp_btc", "start_date": "2010-01-01"}, "iotusd": {"symbol":"iot_usd", "start_date": "2010-01-01"}, "iotbtc": {"symbol":"iot_btc", "start_date": "2010-01-01"}, "ioteth": {"symbol":"iot_eth", "start_date": "2010-01-01"}, "eosusd": {"symbol":"eos_usd", "start_date": "2010-01-01"}, "eosbtc": {"symbol":"eos_btc", "start_date": "2010-01-01"}, "eoseth": {"symbol":"eos_eth", "start_date": "2010-01-01"} }' +ASSETS = '{ "btcusd": {"symbol":"btc_usd", "start_date": "2010-01-01"}, "ltcusd": {"symbol":"ltc_usd", "start_date": "2010-01-01"}, "ltcbtc": {"symbol":"ltc_btc", "start_date": "2010-01-01"}, "ethusd": {"symbol":"eth_usd", "start_date": "2010-01-01"}, "ethbtc": {"symbol":"eth_btc", "start_date": "2010-01-01"}, "etcbtc": {"symbol":"etc_btc", "start_date": "2010-01-01"}, "etcusd": {"symbol":"etc_usd", "start_date": "2010-01-01"}, "rrtusd": {"symbol":"rrt_usd", "start_date": "2010-01-01"}, "rrtbtc": {"symbol":"rrt_btc", "start_date": "2010-01-01"}, "zecusd": {"symbol":"zec_usd", "start_date": "2010-01-01"}, "zecbtc": {"symbol":"zec_btc", "start_date": "2010-01-01"}, "xmrusd": {"symbol":"xmr_usd", "start_date": "2010-01-01"}, "xmrbtc": {"symbol":"xmr_btc", "start_date": "2010-01-01"}, "dshusd": {"symbol":"dsh_usd", "start_date": "2010-01-01"}, "dshbtc": {"symbol":"dsh_btc", "start_date": "2010-01-01"}, "bccbtc": {"symbol":"bcc_btc", "start_date": "2010-01-01"}, "bcubtc": {"symbol":"bcu_btc", "start_date": "2010-01-01"}, "bccusd": {"symbol":"bcc_usd", "start_date": "2010-01-01"}, "bcuusd": {"symbol":"bcu_usd", "start_date": "2010-01-01"}, "xrpusd": {"symbol":"xrp_usd", "start_date": "2010-01-01"}, "xrpbtc": {"symbol":"xrp_btc", "start_date": "2010-01-01"}, "iotusd": {"symbol":"iot_usd", "start_date": "2010-01-01"}, "iotbtc": {"symbol":"iot_btc", "start_date": "2010-01-01"}, "ioteth": {"symbol":"iot_eth", "start_date": "2010-01-01"}, "eosusd": {"symbol":"eos_usd", "start_date": "2010-01-01"}, "eosbtc": {"symbol":"eos_btc", "start_date": "2010-01-01"}, "eoseth": {"symbol":"eos_eth", "start_date": "2010-01-01"} }' log = Logger('Bitfinex') warning_logger = Logger('AlgoWarning') @@ -35,7 +36,7 @@ class Bitfinex(Exchange): self.assets = {} self.load_assets(ASSETS) - def request(self, operation, data, version='v1'): + def _request(self, operation, data, version='v1'): payload_object = { 'request': '/{}/{}'.format(version, operation), 'nonce': '{0:f}'.format(time.time() * 100000), # convert to string @@ -66,14 +67,16 @@ class Bitfinex(Exchange): if data is None: request = requests.get( - self.url + '/{version}/{operation}'.format( + '{url}/{version}/{operation}'.format( + url=self.url, version=version, operation=operation ), data={}, headers=headers) else: request = requests.post( - self.url + '/{version}/{operation}'.format( + '{url}/{version}/{operation}'.format( + url=self.url, version=version, operation=operation ), @@ -81,56 +84,136 @@ class Bitfinex(Exchange): return request - def subscribe_to_market_data(self, symbol): - pass + def _get_v2_symbols(self, assets): + """ + Workaround to support Bitfinex v2 + TODO: Might require a separate asset dictionary - def positions(self): - pass + :param assets: + :return: + """ - def portfolio(self): - pass - - def account(self): - pass + v2_symbols = [] + for asset in assets: + pair = asset.symbol.split('_') + symbol = 't' + pair[0].upper() + pair[1].upper() + v2_symbols.append(symbol) + return v2_symbols @property - def time_skew(self): - # TODO: research the time skew conditions - return None + def portfolio(self): + """ + TODO: I'm not sure how that's used yet + :return: + """ + portfolio = Portfolio() + portfolio.capital_used = None + portfolio.starting_cash = None - def get_open_orders(self, asset): - # TODO: map to asset - response = self.request('orders', None) - orders = response.json() - # TODO: what is the right format? - return orders + portfolio.portfolio_value = None + portfolio.pnl = None + portfolio.cash = None - def get_order(self, order_id): - pass + portfolio.returns = None + portfolio.start_date = None + portfolio.positions = self.positions + portfolio.positions_value = None + portfolio.positions_exposure = None - def get_spot_value(self, assets, field, dt, data_frequency): - raise NotImplementedError() + return portfolio - def balance(self, currencies): - response = self.request('balances', None) + @property + def account(self): + account = Account() + + account.settled_cash = None + account.accrued_interest = None + account.buying_power = None + account.equity_with_loan = None + account.total_positions_value = None + account.total_positions_exposure = None + account.regt_equity = None + account.regt_margin = None + account.initial_margin_requirement = None + account.maintenance_margin_requirement = None + account.available_funds = None + account.excess_liquidity = None + account.cushion = None + account.day_trades_remaining = None + account.leverage = None + account.net_leverage = None + account.net_liquidation = None + + return account + + @property + def positions(self): + response = self._request('balances', None) positions = response.json() if 'message' in positions: raise ValueError( 'unable to fetch balance %s' % positions['message'] ) - balance = dict() - for position in positions: - if position['currency'] in currencies: - balance[position['currency']] = float(position['available']) - return balance + return positions + + @property + def time_skew(self): + # TODO: research the time skew conditions + return None + + def subscribe_to_market_data(self, symbol): + pass + + def get_spot_value(self, assets, field, dt, data_frequency): + raise NotImplementedError() # TODO: why repeating prices if already in style? def order(self, asset, amount, limit_price, stop_price, style): - """ - The type of the order: LIMIT, MARKET, STOP, TRAILING STOP, - EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP, - EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK. + """Place an order. + + Parameters + ---------- + asset : Asset + The asset that this order is for. + amount : int + The amount of shares to order. If ``amount`` is positive, this is + the number of shares to buy or cover. If ``amount`` is negative, + this is the number of shares to sell or short. + limit_price : float, optional + The limit price for the order. + stop_price : float, optional + The stop price for the order. + style : ExecutionStyle, optional + The execution style for the order. + + Returns + ------- + order_id : str or None + The unique identifier for this order, or None if no order was + placed. + + Notes + ----- + The ``limit_price`` and ``stop_price`` arguments provide shorthands for + passing common execution styles. Passing ``limit_price=N`` is + equivalent to ``style=LimitOrder(N)``. Similarly, passing + ``stop_price=M`` is equivalent to ``style=StopOrder(M)``, and passing + ``limit_price=N`` and ``stop_price=M`` is equivalent to + ``style=StopLimitOrder(N, M)``. It is an error to pass both a ``style`` + and ``limit_price`` or ``stop_price``. + + Bitfinex Order Types + -------------------- + LIMIT, MARKET, STOP, TRAILING STOP, + EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP, + EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK. + + See Also + -------- + :class:`catalyst.finance.execution.ExecutionStyle` + :func:`catalyst.api.order_value` + :func:`catalyst.api.order_percent` """ is_buy = (amount > 0) @@ -153,7 +236,7 @@ class Bitfinex(Exchange): price=str(float(price)), side='buy' if is_buy else 'sell', type='exchange ' + order_type, # TODO: support margin trades - exchange='bitfinex', + exchange=self.name, is_hidden=False, is_postonly=False, use_all_available=0, @@ -162,7 +245,7 @@ class Bitfinex(Exchange): sell_price_oco=0 ) - response = self.request('order/new', req) + response = self._request('order/new', req) exchange_order = response.json() if 'message' in exchange_order: raise ValueError( @@ -183,20 +266,51 @@ class Bitfinex(Exchange): return order_id - def cancel_order(self, order_id): - response = self.request('order/cancel', {'order_id': order_id}) - status = response.json() - return status + def get_open_orders(self, asset): + """Retrieve all of the current open orders. - def order_status(self, order_id): - response = self.request('order/status', {'order_id': int(order_id)}) + Parameters + ---------- + asset : Asset + If passed and not None, return only the open orders for the given + asset instead of all open orders. + + Returns + ------- + open_orders : dict[list[Order]] or list[Order] + If no asset is passed this will return a dict mapping Assets + to a list containing all the open orders for the asset. + If an asset is passed then this will return a list of the open + orders for this asset. + """ + # TODO: map to asset + response = self._request('orders', None) + orders = response.json() + # TODO: what is the right format? + return orders + + def get_order(self, order_id): + """Lookup an order based on the order id returned from one of the + order functions. + + Parameters + ---------- + order_id : str + The unique identifier for the order. + + Returns + ------- + order : Order + The order object. + """ + response = self._request('order/status', {'order_id': int(order_id)}) order_status = response.json() if 'message' in order_status: raise ValueError( 'Unable to retrieve order status: %s' % order_status['message'] ) - result = dict(exchange='b') + result = dict(exchange=self.name) if order_status['is_cancelled']: warning_logger.warn( @@ -207,39 +321,48 @@ class Bitfinex(Exchange): elif not order_status['is_live']: log.info('found executed order %s', order_status) result['status'] = 'closed' - result['executed_price'] = float( - order_status['avg_execution_price']) - result['executed_amount'] = float(order_status['executed_amount']) + result['executed_price'] = \ + float(order_status['avg_execution_price']) + result['executed_amount'] = \ + float(order_status['executed_amount']) else: result['status'] = 'open' + # TODO: what's the right format? return result - def get_v2_symbols(self, assets): - """ - Workaround to support Bitfinex v2 - TODO: Might require a separate asset dictionary + def cancel_order(self, order_id): + """Cancel an open order. + Parameters + ---------- + order_param : str or Order + The order_id or order object to cancel. + """ + response = self._request('order/cancel', {'order_id': order_id}) + status = response.json() + return status + + def tickers(self, date, assets): + """ + Fetch ticket data for assets + https://docs.bitfinex.com/v2/reference#rest-public-tickers + :param date: :param assets: :return: """ - - v2_symbols = [] - for asset in assets: - pair = asset.symbol.split('_') - symbol = 't' + pair[0].upper() + pair[1].upper() - v2_symbols.append(symbol) - return v2_symbols - - def tickers(self, date, assets): - symbols = self.get_v2_symbols(assets) + symbols = self._get_v2_symbols(assets) log.debug('fetching tickers {}'.format(symbols)) request = requests.get( - self.url + '/v2/tickers?symbols={}'.format(','.join(symbols)) + '{url}/v2/tickers?symbols={symbols}'.format( + url=self.url, + symbols=','.join(symbols), + ) ) tickers = request.json() + if 'message' in tickers: raise ValueError( 'Unable to retrieve tickers: %s' % tickers['message'] diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 9838dc7e..9b64c18c 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -42,6 +42,15 @@ class Exchange: return symbol + def get_asset(self, symbol): + asset = None + + for key in self.assets: + if not asset and self.assets[key].symbol == symbol: + asset = self.assets[key] + + return asset + def get_symbols(self, assets): symbols = [] for asset in assets: @@ -63,7 +72,7 @@ class Exchange: for exchange_symbol in assets: asset_obj = Asset( - sid=0, + sid=abs(hash(assets[exchange_symbol]['symbol'])) % (10 ** 4), exchange=self.name, **assets[exchange_symbol] ) @@ -91,18 +100,93 @@ class Exchange: @abstractmethod def order(self, asset, amount, limit_price, stop_price, style): + """Place an order. + + Parameters + ---------- + asset : Asset + The asset that this order is for. + amount : int + The amount of shares to order. If ``amount`` is positive, this is + the number of shares to buy or cover. If ``amount`` is negative, + this is the number of shares to sell or short. + limit_price : float, optional + The limit price for the order. + stop_price : float, optional + The stop price for the order. + style : ExecutionStyle, optional + The execution style for the order. + + Returns + ------- + order_id : str or None + The unique identifier for this order, or None if no order was + placed. + + Notes + ----- + The ``limit_price`` and ``stop_price`` arguments provide shorthands for + passing common execution styles. Passing ``limit_price=N`` is + equivalent to ``style=LimitOrder(N)``. Similarly, passing + ``stop_price=M`` is equivalent to ``style=StopOrder(M)``, and passing + ``limit_price=N`` and ``stop_price=M`` is equivalent to + ``style=StopLimitOrder(N, M)``. It is an error to pass both a ``style`` + and ``limit_price`` or ``stop_price``. + + See Also + -------- + :class:`catalyst.finance.execution.ExecutionStyle` + :func:`catalyst.api.order_value` + :func:`catalyst.api.order_percent` + """ pass @abstractmethod def get_open_orders(self, asset): + """Retrieve all of the current open orders. + + Parameters + ---------- + asset : Asset + If passed and not None, return only the open orders for the given + asset instead of all open orders. + + Returns + ------- + open_orders : dict[list[Order]] or list[Order] + If no asset is passed this will return a dict mapping Assets + to a list containing all the open orders for the asset. + If an asset is passed then this will return a list of the open + orders for this asset. + """ pass @abstractmethod def get_order(self, order_id): + """Lookup an order based on the order id returned from one of the + order functions. + + Parameters + ---------- + order_id : str + The unique identifier for the order. + + Returns + ------- + order : Order + The order object. + """ pass @abstractmethod def cancel_order(self, order_param): + """Cancel an open order. + + Parameters + ---------- + order_param : str or Order + The order_id or order object to cancel. + """ pass @abstractmethod @@ -112,20 +196,3 @@ class Exchange: @abc.abstractmethod def tickers(self, date, pairs): return - - # @abc.abstractmethod - # def new_order(self, symbol, side, order_type, price, amount, leverage): - # return - # - # @abc.abstractmethod - # def cancel_order(self, order_id): - # return - # - # @abc.abstractmethod - # def order_status(self, order_id): - # return - # - # @abc.abstractmethod - # def balance(self, currencies): - # return - # diff --git a/tests/exchange/base.py b/tests/exchange/base.py index 777b1b80..3b1d67d8 100644 --- a/tests/exchange/base.py +++ b/tests/exchange/base.py @@ -1,27 +1,46 @@ import unittest -import abc -from abc import ABCMeta +from abc import ABCMeta, abstractmethod class BaseExchangeTestCase(): __metaclass__ = ABCMeta - @abc.abstractmethod + @abstractmethod + def test_positions(self): + pass + + @abstractmethod + def test_portfolio(self): + pass + + @abstractmethod + def test_account(self): + pass + + @abstractmethod + def test_time_skew(self): + pass + + @abstractmethod + def test_get_open_orders(self): + pass + + @abstractmethod def test_order(self): pass - @abc.abstractmethod + @abstractmethod + def test_get_order(self): + pass + + @abstractmethod def test_cancel_order(self): pass - @abc.abstractmethod - def test_order_status(self): + @abstractmethod + def test_spot_value(self): pass - @abc.abstractmethod - def test_balance(self): - pass - - @abc.abstractmethod - def test_ticker(self): + @abstractmethod + def test_tickers(self): pass diff --git a/tests/exchange/test_bitfinex.py b/tests/exchange/test_bitfinex.py index 45d665dd..cf54f180 100644 --- a/tests/exchange/test_bitfinex.py +++ b/tests/exchange/test_bitfinex.py @@ -6,51 +6,75 @@ from catalyst.finance.execution import (MarketOrder, LimitOrder, StopOrder, StopLimitOrder) -from catalyst.assets._assets import Asset +import catalyst.protocol as protocol log = Logger('BitfinexTestCase') class BitfinexTestCase(BaseExchangeTestCase): - def test_ticker(self): - log.info('fetching ticker from bitfinex') + def test_positions(self): + log.info('querying positions from bitfinex') bitfinex = Bitfinex() - current_date = pd.Timestamp.utcnow() - assets = [ - Asset(sid=0, exchange=bitfinex.name, symbol='eth_usd'), - Asset(sid=1, exchange=bitfinex.name, symbol='etc_usd'), - Asset(sid=2, exchange=bitfinex.name, symbol='eos_usd') - ] - tickers = bitfinex.tickers(date=current_date, assets=assets) - log.info('got tickers {}'.format(tickers)) + balance = bitfinex.positions() + log.info('the balance: {}'.format(balance)) + pass + + def test_portfolio(self): + log.info('fetching portfolio data') + pass + + def test_account(self): + log.info('fetching account data') + pass + + def test_time_skew(self): + log.info('time skew not implemented') + pass + + def test_get_open_orders(self): + log.info('fetching open orders') + pass def test_order(self): log.info('ordering from bitfinex') bitfinex = Bitfinex() - asset = Asset(sid=0, exchange=bitfinex.name, symbol='eth_usd') order_id = bitfinex.order( - asset=asset, + asset=bitfinex.get_asset('eth_usd'), style=LimitOrder(limit_price=200), limit_price=200, amount=1, stop_price=None ) log.info('order created {}'.format(order_id)) + pass + + def test_get_order(self): + log.info('querying orders from bitfinex') + bitfinex = Bitfinex() + response = bitfinex.order_status(order_id=3330866978) + log.info('the orders: {}'.format(response)) + pass def test_cancel_order(self): log.info('canceling order from bitfinex') bitfinex = Bitfinex() - response = bitfinex.cancel_order(order_id=2776936269) + response = bitfinex.cancel_order(order_id=3330847408) log.info('canceled order: {}'.format(response)) + pass - def test_order_status(self): - log.info('querying orders from bitfinex') - bitfinex = Bitfinex() - response = bitfinex.order_status(order_id=2776972180) - log.info('the orders: {}'.format(response)) + def test_spot_value(self): + log.info('spot valud not implemented') + pass - def test_balance(self): - log.info('querying positions from bitfinex') + def test_tickers(self): + log.info('fetching ticker from bitfinex') bitfinex = Bitfinex() - balance = bitfinex.balance(currencies=['usd', 'etc', 'pez']) - log.info('the balance: {}'.format(balance)) + current_date = pd.Timestamp.utcnow() + assets = [ + bitfinex.get_asset('eth_usd'), + bitfinex.get_asset('etc_usd'), + bitfinex.get_asset('eos_usd'), + ] + tickers = bitfinex.tickers(date=current_date, assets=assets) + log.info('got tickers {}'.format(tickers)) + pass