From 790980c87fad0a5032b1aab8cb19ead71a642cb7 Mon Sep 17 00:00:00 2001 From: "Gavin.Chan" Date: Mon, 1 Jan 2018 13:58:13 +0000 Subject: [PATCH] [#11] Add Bitfinex API --- libcryptomarket/api/bitfinex_api.py | 154 +++++++++++++++++ libcryptomarket/core.py | 249 +++++++++++++++++++--------- 2 files changed, 327 insertions(+), 76 deletions(-) create mode 100644 libcryptomarket/api/bitfinex_api.py diff --git a/libcryptomarket/api/bitfinex_api.py b/libcryptomarket/api/bitfinex_api.py new file mode 100644 index 0000000..c4ad124 --- /dev/null +++ b/libcryptomarket/api/bitfinex_api.py @@ -0,0 +1,154 @@ +#!/bin/python +# import requests +# import hmac +# import hashlib +# import urllib +# from functools import partial + +from libcryptomarket.api.exchange_api import ExchangeApi + + +class BitfinexApi(ExchangeApi): + """Bitfinex API. + """ + + def __init__(self, public_key=None, private_key=None, logger=None): + """Constructor. + + :param public_key: Public key. + :param private_key: Private key. + :param logger: Logger. + """ + ExchangeApi.__init__(self, public_key, private_key, logger) + + @classmethod + def get_url(cls): + """Get API url. + """ + return "https://api.bitfinex.com/v2" + + @classmethod + def get_public_calls(cls): + """Get public API calls. + """ + return { + 'tickers': 'GET', + 'ticker': 'GET', + 'trades': 'GET', + 'book': 'GET', + 'stat1': 'GET', + 'candles': 'GET' + } + + @classmethod + def get_private_calls(cls): + """Get public API calls. + """ + return { + } + + @classmethod + def translate_call_name(cls, name): + """Translate API call name. + + The class method name is always underscored (aligned with Python + standard.) This method is to translate underscored name to exchange + API call name. + + :param name: Method name (underscored). + """ + return name + + def _request_public(self, name, http_method, **kwargs): + """Request public API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + if name == "ticker": + symbol = kwargs["symbol"] + del kwargs["symbol"] + name = '/'.join([name, symbol]) + elif name == "trades": + symbol = kwargs["symbol"] + del kwargs["symbol"] + name = '/'.join([name, symbol, "hist"]) + elif name == "book": + symbol = kwargs["symbol"] + precision = kwargs["precision"] + del kwargs["symbol"] + del kwargs["precision"] + name = "/".join([name, symbol, precision]) + elif name == "stat1": + key = kwargs["key"] + size = kwargs["size"] + symbol = kwargs["symbol"] + section = kwargs["section"] + del kwargs["key"] + del kwargs["size"] + del kwargs["symbol"] + del kwargs["section"] + name = "/".join([name, + "{}:{}:{}".format(key, size, symbol), + section]) + elif name == "candles": + timeframe = kwargs["timeframe"] + symbol = kwargs["symbol"] + section = kwargs["section"] + del kwargs["timeframe"] + del kwargs["symbol"] + del kwargs["section"] + name = "/".join([name, + "trade:{}:{}".format(timeframe, symbol), + section]) + + return self._send_request( + command=name, http_method=http_method, + public_method=True, params=kwargs) + + def _request_private(self, name, http_method, **kwargs): + """Request private API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + raise NotImplementedError() + + # @classmethod + # def _generate_auth(cls, public_key, private_key): + # """Generate authentication. + + # :param public_key: Public key. + # :param private_key: Private key. + # """ + # return None + + # @classmethod + # def _generate_headers(cls, command, http_method, params, data, + # public_key, private_key): + # """Generate headers. + + # :param command: Command. + # :param http_method: HTTP method, for example GET. + # :param params: Parameters. + # :param data: Data. + # :param public_key: Public key. + # :param private_key: Private key. + # """ + # signature = hmac.new(private_key.encode(), + # urllib.parse.urlencode(data).encode(), + # digestmod=hashlib.sha512).hexdigest() + # header = { + # 'Key': public_key, + # 'Sign': signature + # } + + # return header + + # @classmethod + # def _format_data(cls, data): + # """Format the data to exchange desirable format. + + # :param data: Data. + # """ + # return data diff --git a/libcryptomarket/core.py b/libcryptomarket/core.py index aad581f..f4f54df 100644 --- a/libcryptomarket/core.py +++ b/libcryptomarket/core.py @@ -9,6 +9,9 @@ def historical_ticker(source, symbol, period, start_time=None, end_time=None): """Return historical ticker. :param source: Source, an Exchange API object. + :param symbol: Symbol, string object. + :param period: Period or frequency, followed with exchange protocol, string + object. :param start_time: Start time, datetime object. :param end_time: Start time, datetime object. """ @@ -23,84 +26,178 @@ def historical_ticker(source, symbol, period, start_time=None, end_time=None): source_name = source.__class__.__name__.lower().replace("api", "") if source_name == "poloniex": - # Exchange validation - if start_time is None and end_time is None: - raise ValueError("Start time and end time cannot be both None.") - - request_func = partial(source.return_chart_data, currencyPair=symbol, - period=period) - - if start_time is not None: - request_func = partial(request_func, - start=start_time.timestamp()) - if end_time is not None: - request_func = partial(request_func, - end=end_time.timestamp()) - data = request_func() - data.raise_for_status() - data = pd.DataFrame(data.json()) - data['date'] = data['date'].apply( - lambda x: pd.to_datetime(x, unit='s')) - data = data.set_index(['date']) - data.index.name = 'datetime' - data.columns.name = symbol - - return data + return _historical_ticker_poloniex( + source=source, symbol=symbol, period=period, + start_time=start_time, end_time=end_time) elif source_name == "gdax": - # Exchange validation - if (start_time is None) + (end_time is None) not in [0, 2]: - # Both start and end time must be provided - raise ValueError("Start and end time must be both provided") - - request_func = partial(source.products_candles, product_id=symbol, - granularity=period) - - if start_time is None and end_time is None: - # Just get the latest 300 ticks - data = request_func() - data.raise_for_status() - data = pd.DataFrame(data.json()) - - else: - # Safety net - last_datetime = start_time.timestamp() - - data = [] - - while start_time <= end_time: - tmp_data = request_func( - start=start_time.isoformat(), - end=(start_time + - timedelta(seconds=period * 200)).isoformat()) - tmp_data.raise_for_status() - tmp_data = pd.DataFrame(tmp_data.json()) - - # Append into data list - data.append(tmp_data) - - # Check to exit - if last_datetime >= tmp_data.iloc[0, 0]: - # Same as the previous query - break - else: - last_datetime = tmp_data.iloc[0, 0] - start_time = datetime.fromtimestamp(last_datetime) - - sleep(0.333) - - if len(data) > 1: - data = pd.concat(data, axis=0) - else: - data = data[0] - - data.columns = ['datetime', 'low', 'high', 'open', 'close', 'volume'] - data['datetime'] = pd.to_datetime(data['datetime'], unit='s') - data = data.set_index('datetime').sort_index() - data = data[~data.index.duplicated(keep='first')] - - return data - + return _historical_ticker_gdax( + source=source, symbol=symbol, period=period, + start_time=start_time, end_time=end_time) + elif source_name == "bitfinex": + return _historical_ticker_bitfinex( + source=source, symbol=symbol, period=period, + start_time=start_time, end_time=end_time) else: raise ValueError("Source (%s [%s]) does not support historical ticker" % (source, source_name)) + + +def _historical_ticker_poloniex(source, symbol, period, start_time, end_time): + """Return historical ticker in Poloniex. + + :param source: Source, an Exchange API object. + :param symbol: Symbol, string object. + :param period: Period or frequency, followed with exchange protocol, string + object. + :param start_time: Start time, datetime object. + :param end_time: Start time, datetime object. + """ + # Exchange validation + if start_time is None and end_time is None: + raise ValueError("Start time and end time cannot be both None.") + + request_func = partial(source.return_chart_data, currencyPair=symbol, + period=period) + + if start_time is not None: + request_func = partial(request_func, + start=start_time.timestamp()) + if end_time is not None: + request_func = partial(request_func, + end=end_time.timestamp()) + data = request_func() + data.raise_for_status() + data = pd.DataFrame(data.json()) + data['date'] = data['date'].apply( + lambda x: pd.to_datetime(x, unit='s')) + data = data.set_index(['date']) + data.index.name = 'datetime' + data.columns.name = symbol + + return data + + +def _historical_ticker_gdax(source, symbol, period, start_time, end_time): + """Return historical ticker in GDAX. + + :param source: Source, an Exchange API object. + :param symbol: Symbol, string object. + :param period: Period or frequency, followed with exchange protocol, string + object. + :param start_time: Start time, datetime object. + :param end_time: Start time, datetime object. + """ + # Exchange validation + if (start_time is None) + (end_time is None) not in [0, 2]: + # Both start and end time must be provided + raise ValueError("Start and end time must be both provided") + + request_func = partial(source.products_candles, product_id=symbol, + granularity=period) + + if start_time is None and end_time is None: + # Just get the latest 300 ticks + data = request_func() + data.raise_for_status() + data = pd.DataFrame(data.json()) + + else: + # Safety net + last_datetime = start_time.timestamp() + + data = [] + + while start_time <= end_time: + tmp_data = request_func( + start=start_time.isoformat(), + end=(start_time + + timedelta(seconds=period * 200)).isoformat()) + tmp_data.raise_for_status() + tmp_data = pd.DataFrame(tmp_data.json()) + + # Append into data list + data.append(tmp_data) + + # Check to exit + if last_datetime >= tmp_data.iloc[0, 0]: + # Same as the previous query + break + else: + last_datetime = tmp_data.iloc[0, 0] + start_time = datetime.fromtimestamp(last_datetime) + + sleep(0.333) + + if len(data) > 1: + data = pd.concat(data, axis=0) + else: + data = data[0] + + data.columns = ['datetime', 'low', 'high', 'open', 'close', 'volume'] + data['datetime'] = pd.to_datetime(data['datetime'], unit='s') + data = data.set_index('datetime').sort_index() + data = data[~data.index.duplicated(keep='first')] + + return data + + +def _historical_ticker_bitfinex(source, symbol, period, start_time, end_time): + """Return historical ticker in Bitfinex. + + :param source: Source, an Exchange API object. + :param symbol: Symbol, string object. + :param period: Period or frequency, followed with exchange protocol, string + object. + :param start_time: Start time, datetime object. + :param end_time: Start time, datetime object. + """ + request_func = partial(source.candles, symbol=symbol, timeframe=period, + section="hist", sort=1, limit=1000) + + if start_time is None and end_time is None: + data = request_func() + data.raise_for_status() + data = pd.DataFrame(data.json()) + else: + # Safety net + last_datetime = ( + 0 if start_time is None else start_time.timestamp() * 1000) + + data = [] + + while start_time is None or end_time is None or start_time <= end_time: + f = request_func + + if start_time is not None: + f = partial(f, start=round(start_time.timestamp() * 1000)) + + if end_time is not None: + f = partial(f, end=round(end_time.timestamp() * 1000)) + + tmp_data = f() + tmp_data.raise_for_status() + tmp_data = tmp_data.json() + + if len(tmp_data) == 0: + break + elif last_datetime >= tmp_data[-1][0]: + print(last_datetime) + print(tmp_data[-1][0]) + break + else: + last_datetime = tmp_data[-1][0] + start_time = datetime.fromtimestamp( + round(last_datetime / 1000 + 1)) + data.append(pd.DataFrame(tmp_data)) + + sleep(1) + + data = pd.concat(data, axis=0) + + data.columns = ["datetime", "open", "close", "high", "low", "volume"] + data["datetime"] = pd.to_datetime(data["datetime"], unit="ms") + data = data.set_index("datetime").sort_index() + data = data[~data.index.duplicated(keep='first')] + + return data