diff --git a/libcryptomarket/__init__.py b/libcryptomarket/__init__.py index 720c7b2..84a8725 100644 --- a/libcryptomarket/__init__.py +++ b/libcryptomarket/__init__.py @@ -4,4 +4,4 @@ __author__ = """Gavin Chan""" __email__ = 'gavincyi@gmail.com' -__version__ = '0.1.4' +__version__ = '0.1.5rc1' diff --git a/libcryptomarket/api/bitmex_api.py b/libcryptomarket/api/bitmex_api.py new file mode 100644 index 0000000..22e245d --- /dev/null +++ b/libcryptomarket/api/bitmex_api.py @@ -0,0 +1,85 @@ +#!/bin/python +from libcryptomarket.api.exchange_api import ExchangeApi + + +class BitmexApi(ExchangeApi): + """BitMEX API connector. + """ + + def __init__(self, public_key, private_key, 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://www.bitmex.com/api/v1/' + + @classmethod + def get_public_calls(cls): + """Get public API calls. + """ + return { + "funding": "GET", + "instrument": "GET", + "instrument/active": "GET", + "instrument/activeAndIndices": "GET", + "instrument/activeIntervals": "GET", + "instrument/compositeIndex": "GET", + "instrument/indices": "GET", + "insurance": "GET", + "liquidation": "GET", + "orderbook/l2": "GET", + "quote": "GET", + "quote/bucketed": "GET", + "settlement": "GET", + "stats": "GET", + "stats/history": "GET", + "stats/history/usd": "GET", + "trade": "GET", + "trade/bucketed": "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.replace("_", "/") + + def _request_public(self, name, http_method, **kwargs): + """Request public API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + return self._send_request( + command=name, http_method=http_method, params=kwargs, data=None) + + def _request_private(self, name, http_method, **kwargs): + """Request private API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + # return self._send_request( + # command=name, http_method=http_method, params=kwargs, + # public_key=self._public_key, private_key=self._private_key) + raise NotImplementedError("request private is not implemented.") \ No newline at end of file diff --git a/libcryptomarket/api/exchange_api.py b/libcryptomarket/api/exchange_api.py new file mode 100644 index 0000000..7c76e6f --- /dev/null +++ b/libcryptomarket/api/exchange_api.py @@ -0,0 +1,216 @@ +#!/bin/python +import requests +import hmac +import hashlib +import urllib +import json +from functools import partial +from time import time + +class ExchangeApi: + """Exchange API connector. + """ + def __init__(self, public_key, private_key, logger=None): + """Constructor. + + :param public_key: Public key. + :param private_key: Private key. + :param logger: Logger. + """ + self._public_key = public_key + self._private_key = private_key + self._logger = logger + + @classmethod + def get_url(cls): + """Get API url. + """ + raise NotImplementedError("Url getter not implemented") + + @classmethod + def get_public_url(cls): + """Get public API url. + """ + raise NotImplementedError("Public url getter not implemented") + + @classmethod + def get_private_url(cls): + """Get private API url. + """ + raise NotImplementedError("Private url getter not implemented") + + @classmethod + def get_public_calls(cls): + """Get public API calls. + """ + raise NotImplementedError("Public API calls getter not implemented") + + @classmethod + def get_private_calls(cls): + """Get private API calls. + """ + raise NotImplementedError("Private API calls getter not implemented") + + @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 __getattr__(self, name): + """Get attribute. + """ + name = self.translate_call_name(name) + + if name in self.get_public_calls().keys(): + return partial(self._request_public, name=name, + http_method=self.get_public_calls()[name]) + elif name in self.get_private_calls().keys(): + return partial(self._request_private, name=name, + http_method=self.get_private_calls()[name]) + else: + raise AttributeError("Trading method ({0}) ".format(name) + + "is not defined.") + + def _request_public(self, name, http_method, **kwargs): + """Request public API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + raise NotImplementedError("Public API request not implemented") + + 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("Private API request not implemented") + + # ====================================================================== + # Requests + # ====================================================================== + + @classmethod + def _generate_nonce(cls): + """Generate an increasing unique number. + """ + return int(round(time() * 1000)) + + @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. + """ + raise NotImplementedError("Not yet implemented.") + + @classmethod + def _generate_auth(cls, public_key, private_key): + """Generate authentication. + + :param public_key: Public key. + :param private_key: Private key. + """ + raise NotImplementedError("Not yet implemented.") + + @classmethod + def _generate_data(cls, data): + """Generate data. + + :param data: Dict containing data information. + """ + raise NotImplementedError("Not yet implemented.") + + + def _send_request(self, command, http_method, params=None, data=None, + public_method=False): + """Send request. + + :param command: API command. + :param http_method: Http method. + :param api_key: API key. + :param params: Input parameters, which will be parsed + as "?key1=value1...". + :param data: Data. + :param public_method: Indicate if the request is a public method. + :return: JSON object. + """ + http_method = http_method.upper() + if http_method == "DELETE": + R = requests.delete + elif http_method == "GET": + R = requests.get + elif http_method == "POST": + R = requests.post + else: + raise ValueError("Http method must be either DELETE, GET or " + "POST.") + + # Get url + url = self.get_url() + + if url is None: + if public_method: + url = self.get_public_url() + else: + url = self.get_private_url() + + if url is None: + raise NotImplementedError("Url cannot be None.") + + if command is not None or command == "": + url = '/'.join([url, command]) + + # Get data + if data is not None: + data = self._format_data(data) + else: + data = "" + + # Get headers and auth + if (self._public_key is not None and self._private_key is not None and + not public_method): + headers = self._generate_headers(command, http_method, params, + data, self._public_key, + self._private_key) + auth = self._generate_auth(self._public_key, self._private_key) + else: + headers = None + auth = None + + if self._logger is not None: + self._logger.info(">>> OUT:\n%s" % json.dumps({ + "Method": http_method, + "Url": url, + "Params": params, + "Data": data, + "Headers": headers + })) + + if auth is None: + response = R(url, params=params, data=data, headers=headers) + else: + response = R(url, params=params, data=data, headers=headers, + auth=auth) + + if self._logger is not None: + self._logger.info("<<< IN:\n%s" % json.dumps({ + "Status code": response.status_code, + "Text": response.text + })) + + return response diff --git a/libcryptomarket/api/poloniex_api.py b/libcryptomarket/api/poloniex_api.py index a6ac77d..6857d8c 100644 --- a/libcryptomarket/api/poloniex_api.py +++ b/libcryptomarket/api/poloniex_api.py @@ -5,7 +5,7 @@ import hashlib import urllib from functools import partial -from libcryptomarket.api.rest_api_connector import RestApiConnector +from libcryptomarket.api.exchange_api import ExchangeApi API_URL = "https://poloniex.com/public?command=" VALID_PERIODS = [300, 900, 1800, 7200, 14400, 86400] @@ -66,80 +66,130 @@ def get_return_order_book(currency_pair, depth=10): return r.json() -class PoloniexApi(RestApiConnector): +class PoloniexApi(ExchangeApi): """Poloniex API. """ - URL = 'https://poloniex.com/tradingApi' - - AVAILABLE_TRADING_API = [ - "returnBalances", - "returnCompleteBalances", - "returnDepositAddresses", - "generateNewAddress", - "returnDepositsWithdrawals", - "returnOpenOrders", - "returnTradeHistory", - "returnOrderTrades", - "buy", - "sell", - "cancelOrder", - "moveOrder", - "withdraw", - "returnFeeInfo", - "returnAvailableAccountBalances", - "returnTradableBalances", - "transferBalance", - "returnMarginAccountSummary", - "marginBuy", - "marginSell", - "getMarginPosition", - "closeMarginPosition", - "createLoanOffer", - "cancelLoanOffer", - "returnOpenLoanOffers", - "returnActiveLoans", - "returnLendingHistory", - "toggleAutoRenew", - ] - - def __init__(self, public_key, private_key, logger=None): + 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. """ - RestApiConnector.__init__(self, url=PoloniexApi.URL, logger=logger) + ExchangeApi.__init__(self, public_key, private_key, logger) - self.__public_key = public_key - self.__private_key = private_key - - def __getattr__(self, name): - """Get attribute. + @classmethod + def get_url(cls): + """Get API url. """ - if name in PoloniexApi.AVAILABLE_TRADING_API: - return partial(self._request, command=name, http_method="POST") - else: - raise AttributeError("Trading method ({0}) ".format(name) + - "is not defined.") + return None - def _request(self, command, **kwargs): - """Send request. - - :param kwargs: Named arguments. + @classmethod + def get_public_url(cls): + """Get public API url. """ - kwargs["command"] = command - kwargs["nonce"] = self._generate_nonce() + return 'https://poloniex.com/public' + + @classmethod + def get_private_url(cls): + """Get private API url. + """ + return 'https://poloniex.com/tradingApi' + + @classmethod + def get_public_calls(cls): + """Get public API calls. + """ + return { + "returnTicker": "GET", + "return24Volume": "GET", + "returnOrderBook": "GET", + "returnTradeHistory": "GET", + "returnChartData": "GET", + "returnCurrencies": "GET", + "returnTicker": "GET", + "returnLoanOrders": "GET", + } + + @classmethod + def get_private_calls(cls): + """Get public API calls. + """ + return { + "returnBalances": "POST", + "returnCompleteBalances": "POST", + "returnDepositAddresses": "POST", + "generateNewAddress": "POST", + "returnDepositsWithdrawals": "POST", + "returnOpenOrders": "POST", + "returnTradeHistory": "POST", + "returnOrderTrades": "POST", + "buy": "POST", + "sell": "POST", + "cancelOrder": "POST", + "moveOrder": "POST", + "withdraw": "POST", + "returnFeeInfo": "POST", + "returnAvailableAccountBalances": "POST", + "returnTradableBalances": "POST", + "transferBalance": "POST", + "returnMarginAccountSummary": "POST", + "marginBuy": "POST", + "marginSell": "POST", + "getMarginPosition": "POST", + "closeMarginPosition": "POST", + "createLoanOffer": "POST", + "cancelLoanOffer": "POST", + "returnOpenLoanOffers": "POST", + "returnActiveLoans": "POST", + "returnLendingHistory": "POST", + "toggleAutoRenew": "POST" + } + + @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). + """ + splitted_names = name.split('_') + if len(splitted_names) > 1: + splitted_names = ([splitted_names[0]] + + [n.title() for n in splitted_names[1:]]) + + return ''.join(splitted_names) + + def _request_public(self, name, http_method, **kwargs): + """Request public API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + kwargs['command'] = name return self._send_request( - command="", - http_method="POST", - public_key=self.__public_key, - private_key=self.__private_key, - params=None, - data=kwargs) + command=None, http_method=http_method, params=kwargs, + public_method=True) - def _generate_auth(self, public_key, private_key): + def _request_private(self, name, http_method, **kwargs): + """Request private API call. + + :param name: Method name. + :param http_method: HTTP method (POST, GET, DELETE). + """ + kwargs['command'] = name + kwargs['nonce'] = self._generate_nonce() + + return self._send_request( + command=None, http_method=http_method, params=None, data=kwargs) + + @classmethod + def _generate_auth(cls, public_key, private_key): """Generate authentication. :param public_key: Public key. @@ -147,7 +197,8 @@ class PoloniexApi(RestApiConnector): """ return None - def _generate_headers(self, command, http_method, params, data, + @classmethod + def _generate_headers(cls, command, http_method, params, data, public_key, private_key): """Generate headers. @@ -168,12 +219,10 @@ class PoloniexApi(RestApiConnector): return header - def _format_data(self, data): + @classmethod + def _format_data(cls, data): """Format the data to exchange desirable format. :param data: Data. """ - if data is None: - return "" - else: - return data + return data diff --git a/libcryptomarket/core.py b/libcryptomarket/core.py new file mode 100644 index 0000000..3ab359f --- /dev/null +++ b/libcryptomarket/core.py @@ -0,0 +1,50 @@ +from functools import partial +from datetime import datetime, timedelta + +import pandas as pd + + +def historical_ticker(source, symbol, period, start_time=None, end_time=None): + """Return historical ticker. + + :param source: Source, an Exchange API object. + :param start_time: Start time, datetime object. + :param end_time: Start time, datetime object. + """ + # Validation + if start_time is not None and not isinstance(start_time, datetime): + raise ValueError("Start time is not a datetime object.") + + if end_time is not None and not isinstance(end_time, datetime): + raise ValueError("End time is not a datetime object.") + + # Source object name + 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 + else: + raise ValueError("Source (%s [%s]) does not support historical ticker" + % (source, source_name)) \ No newline at end of file diff --git a/libcryptomarket/historical.py b/libcryptomarket/historical.py index 0d50d50..090ae6c 100644 --- a/libcryptomarket/historical.py +++ b/libcryptomarket/historical.py @@ -1,5 +1,5 @@ from functools import partial -from datetime import datetime +from datetime import datetime, timedelta import pandas as pd @@ -73,6 +73,55 @@ def get_historical_prices(source='cryptocompare', symbol=None, exchange=None, return _get_historical_prices_poloniex(symbol, period, from_time, to_time) + elif source == 'bitmex': + if exchange is not None: + raise ValueError("Bitmex does not need exchange parameter.") + + # Validate and transform periods + valid_periods = ["1m", "5m", "1h", "1d"] + + period = period.lower() + if period not in valid_periods: + raise ValueError("Periods is not valid. " + + ("Valid values (%s)" % str(valid_periods))) + + # Validate and transform to_time and from_time + if (to_time is not None and from_time is not None and + to_time <= from_time): + raise ValueError("From time should not be greater than or " + "equal to to time.") + + ret = [] + + while from_time is None or to_time is None or from_time < to_time: + # Import and query + from libcryptomarket.api.bitmex_api import BitmexApi + exchange = BitmexApi(public_key=None, private_key=None, logger=None) + func = partial(exchange.trade_bucketed, symbol=symbol, + binSize=period) + + if from_time is not None: + from_time_s = from_time.strftime("%Y-%m-%dT%H:%M:%S") + func = partial(func, startTime=from_time_s) + + if to_time is not None: + to_time_s = to_time.strftime("%Y-%m-%dT%H:%M:%S") + func = partial(func, endTime=to_time_s) + + data = func() + data.raise_for_status() + data = pd.DataFrame(data.json()) + + if len(data) > 0: + ret.append(data) + from_time = data.iloc[-1, :]['timestamp'][:-5] + from_time = datetime.strptime(from_time, "%Y-%m-%dT%H:%M:%S") + from_time = from_time + timedelta(seconds=1) + else: + break + + return pd.concat(ret) + else: raise ValueError("No source is called {0}".format(source))