diff --git a/catalyst/exchange/__init__.py b/catalyst/exchange/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/catalyst/exchange/assets.py b/catalyst/exchange/assets.py new file mode 100644 index 00000000..a2007a45 --- /dev/null +++ b/catalyst/exchange/assets.py @@ -0,0 +1,10 @@ +import pandas as pd +import pytz + +assets = list( + dict( + symbol='eth-usd', + exchange='bitfinex', + first_traded=pd.datetime(2010, 1, 1, 0, 0, 0, 0, pytz.utc) + ) +) diff --git a/catalyst/exchange/bitfinex.py b/catalyst/exchange/bitfinex.py new file mode 100644 index 00000000..9d4c2294 --- /dev/null +++ b/catalyst/exchange/bitfinex.py @@ -0,0 +1,266 @@ +import six +import base64 +import hashlib +import hmac +import json +import time +import requests +import pandas as pd +# from websocket import create_connection +from catalyst.exchange.exchange import Exchange, RTVolumeBar, Position +from logbook import Logger +from catalyst.finance.order import Order, ORDER_STATUS +from catalyst.finance.execution import (MarketOrder, + LimitOrder, + StopOrder, + StopLimitOrder) + +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"} }' + +log = Logger('Bitfinex') +warning_logger = Logger('AlgoWarning') + + +class Bitfinex(Exchange): + def __init__(self): + self.url = BITFINEX_URL + self.key = BITFINEX_KEY + self.secret = BITFINEX_SECRET + self.id = 'b' + self.name = 'bitfinex' + self.orders = {} + self.assets = {} + self.load_assets(ASSETS) + + def request(self, operation, data, version='v1'): + payload_object = { + 'request': '/{}/{}'.format(version, operation), + 'nonce': '{0:f}'.format(time.time() * 100000), # convert to string + 'options': {} + } + + if data is None: + payload_dict = payload_object + else: + payload_dict = payload_object.copy() + payload_dict.update(data) + + payload_json = json.dumps(payload_dict) + if six.PY3: + payload = base64.b64encode(bytes(payload_json, 'utf-8')) + else: + payload = base64.b64encode(payload_json) + + m = hmac.new(self.secret, payload, hashlib.sha384) + m = m.hexdigest() + + # headers + headers = { + 'X-BFX-APIKEY': self.key, + 'X-BFX-PAYLOAD': payload, + 'X-BFX-SIGNATURE': m + } + + if data is None: + request = requests.get( + self.url + '/{version}/{operation}'.format( + version=version, + operation=operation + ), data={}, + headers=headers) + else: + request = requests.post( + self.url + '/{version}/{operation}'.format( + version=version, + operation=operation + ), + headers=headers) + + return request + + def subscribe_to_market_data(self, symbol): + pass + + def positions(self): + pass + + def portfolio(self): + pass + + def account(self): + pass + + @property + def time_skew(self): + # TODO: research the time skew conditions + return 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 + + def get_order(self, order_id): + pass + + def get_spot_value(self, assets, field, dt, data_frequency): + raise NotImplementedError() + + def balance(self, currencies): + 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 + + # 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. + """ + + is_buy = (amount > 0) + + if isinstance(style, MarketOrder): + order_type = 'market' + elif isinstance(style, LimitOrder): + order_type = 'limit' + price = limit_price + elif isinstance(style, StopOrder): + order_type = 'stop' + price = stop_price + elif isinstance(style, StopLimitOrder): + raise NotImplementedError('Stop/limit orders not available') + + exchange_symbol = self.get_symbol(asset) + req = dict( + symbol=exchange_symbol, + amount=str(float(amount)), + price=str(float(price)), + side='buy' if is_buy else 'sell', + type='exchange ' + order_type, # TODO: support margin trades + exchange='bitfinex', + is_hidden=False, + is_postonly=False, + use_all_available=0, + ocoorder=False, + buy_price_oco=0, + sell_price_oco=0 + ) + + response = self.request('order/new', req) + exchange_order = response.json() + if 'message' in exchange_order: + raise ValueError( + 'unable to create Bitfinex order %s' % exchange_order[ + 'message'] + ) + + order = Order( + dt=pd.Timestamp.utcnow(), + asset=asset, + amount=amount, + stop=style.get_stop_price(is_buy), + limit=style.get_limit_price(is_buy), + ) + + order_id = order.broker_order_id = exchange_order['id'] + self.orders[order_id] = order + + return order_id + + def cancel_order(self, order_id): + response = self.request('order/cancel', {'order_id': order_id}) + status = response.json() + return status + + def order_status(self, order_id): + 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') + + if order_status['is_cancelled']: + warning_logger.warn( + 'removing cancelled order from the open orders list %s', + order_status) + result['status'] = 'canceled' + + 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']) + + else: + result['status'] = 'open' + + return result + + def get_v2_symbols(self, assets): + """ + Workaround to support Bitfinex v2 + TODO: Might require a separate asset dictionary + + :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) + log.debug('fetching tickers {}'.format(symbols)) + + request = requests.get( + self.url + '/v2/tickers?symbols={}'.format(','.join(symbols)) + ) + tickers = request.json() + if 'message' in tickers: + raise ValueError( + 'Unable to retrieve tickers: %s' % tickers['message'] + ) + + formatted_tickers = [] + for index, ticker in enumerate(tickers): + if not len(ticker) == 11: + raise ValueError('Invalid ticker: %s' % ticker) + + tick = dict( + asset=assets[index], + timestamp=date, + bid=ticker[1], + ask=ticker[3], + last_price=ticker[7], + low=ticker[10], + high=ticker[9], + volume=ticker[8], + ) + formatted_tickers.append(tick) + + log.debug('got tickers {}'.format(formatted_tickers)) + return formatted_tickers diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py new file mode 100644 index 00000000..9838dc7e --- /dev/null +++ b/catalyst/exchange/exchange.py @@ -0,0 +1,131 @@ +import abc +from collections import namedtuple +from abc import ABCMeta, abstractmethod, abstractproperty +import json +import pandas as pd +from catalyst.assets._assets import Asset + +RTVolumeBar = namedtuple('RTVolumeBar', ['last_trade_price', + 'last_trade_size', + 'last_trade_time', + 'total_volume', + 'vwap', + 'single_trade_flag']) + +Position = namedtuple('Position', ['contract', 'position', 'market_price', + 'market_value', 'average_cost', + 'unrealized_pnl', 'realized_pnl', + 'account_name']) + + +class Exchange: + __metaclass__ = ABCMeta + + def __init__(self): + self.name = None + self.trading_pairs = None + self.assets = {} + + def get_trading_pairs(self, pairs): + return [pair for pair in pairs if pair in self.trading_pairs] + + def get_symbol(self, asset): + symbol = None + + for key in self.assets: + if not symbol and self.assets[key].symbol == asset.symbol: + symbol = key + + if not symbol: + raise ValueError('Currency %s not supported by exchange %s' % + (asset['symbol'], self.name)) + + return symbol + + def get_symbols(self, assets): + symbols = [] + for asset in assets: + symbols.append(self.get_symbol(asset)) + return symbols + + @staticmethod + def asset_parser(asset): + for key in asset: + if key == 'start_date': + asset[key] = pd.to_datetime(asset[key], utc=True) + return asset + + def load_assets(self, assets_json): + assets = json.loads( + assets_json, + object_hook=Exchange.asset_parser + ) + + for exchange_symbol in assets: + asset_obj = Asset( + sid=0, + exchange=self.name, + **assets[exchange_symbol] + ) + self.assets[exchange_symbol] = asset_obj + + @abstractmethod + def subscribe_to_market_data(self, symbol): + pass + + @abstractproperty + def positions(self): + pass + + @abstractproperty + def portfolio(self): + pass + + @abstractproperty + def account(self): + pass + + @abstractproperty + def time_skew(self): + pass + + @abstractmethod + def order(self, asset, amount, limit_price, stop_price, style): + pass + + @abstractmethod + def get_open_orders(self, asset): + pass + + @abstractmethod + def get_order(self, order_id): + pass + + @abstractmethod + def cancel_order(self, order_param): + pass + + @abstractmethod + def get_spot_value(self, assets, field, dt, data_frequency): + pass + + @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/catalyst/exchange/symbols/bitfinex.json b/catalyst/exchange/symbols/bitfinex.json new file mode 100644 index 00000000..2419516c --- /dev/null +++ b/catalyst/exchange/symbols/bitfinex.json @@ -0,0 +1 @@ +{ "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"} } \ No newline at end of file diff --git a/tests/exchange/__init__.py b/tests/exchange/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/exchange/base.py b/tests/exchange/base.py new file mode 100644 index 00000000..777b1b80 --- /dev/null +++ b/tests/exchange/base.py @@ -0,0 +1,27 @@ +import unittest +import abc +from abc import ABCMeta + + +class BaseExchangeTestCase(): + __metaclass__ = ABCMeta + + @abc.abstractmethod + def test_order(self): + pass + + @abc.abstractmethod + def test_cancel_order(self): + pass + + @abc.abstractmethod + def test_order_status(self): + pass + + @abc.abstractmethod + def test_balance(self): + pass + + @abc.abstractmethod + def test_ticker(self): + pass diff --git a/tests/exchange/test_bitfinex.py b/tests/exchange/test_bitfinex.py new file mode 100644 index 00000000..45d665dd --- /dev/null +++ b/tests/exchange/test_bitfinex.py @@ -0,0 +1,56 @@ +from catalyst.exchange.bitfinex import Bitfinex +from .base import BaseExchangeTestCase +from logbook import Logger +import pandas as pd +from catalyst.finance.execution import (MarketOrder, + LimitOrder, + StopOrder, + StopLimitOrder) +from catalyst.assets._assets import Asset + +log = Logger('BitfinexTestCase') + + +class BitfinexTestCase(BaseExchangeTestCase): + def test_ticker(self): + log.info('fetching ticker 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)) + + 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, + style=LimitOrder(limit_price=200), + limit_price=200, + amount=1, + stop_price=None + ) + log.info('order created {}'.format(order_id)) + + def test_cancel_order(self): + log.info('canceling order from bitfinex') + bitfinex = Bitfinex() + response = bitfinex.cancel_order(order_id=2776936269) + log.info('canceled order: {}'.format(response)) + + 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_balance(self): + log.info('querying positions from bitfinex') + bitfinex = Bitfinex() + balance = bitfinex.balance(currencies=['usd', 'etc', 'pez']) + log.info('the balance: {}'.format(balance))