diff --git a/README.md b/README.md index 5661db7..df6338d 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ ## Objective -The library is for researchers to analysis cryptocurrency in a fast and -flexible way. Currently there are different source of API to get the -cryptocurrency market information. The sources are from websites which provides -a general comparative information among the currencies and from exchanges. The -target is to normalize the API functions from data source, and let the users -query the data without pain. +* Support API calls of top cryptocurrency exchanges +* Support trading and analysis tools for cryptocurrency market + +The project aims to answer the following two questions: + +1. How to get historical data, especially a long period, in a single query? + +2. How to send exchange API query in an elegent fashion? ## Prerequisite @@ -28,53 +30,82 @@ or development version pip install git+https://github.com/libcryptomarket/libcryptomarket.git ``` -## Usage +## Exchanges supported -All the query result are converted into pandas Series or DataFrame. +| Exchange | Public API | Private API | +|---|---|---| +| [Bitfinex](https://docs.bitfinex.com/v2/docs/ws-general) | v | x | +| [BitMEX](https://www.bitmex.com/api/explorer/) | v | x | +| [CoinMarketCap](https://coinmarketcap.com/api/) | v | x | +| [GDAX](https://docs.gdax.com/#api) | v | x | +| [Poloniex](https://poloniex.com/support/api/) | v | v | -### Instrument +## Basic Usage -To get a list of available currencies, run +### Generic API + +You can just make a simple call to query the same function from different +exchanges. For example, if you want to get the historical data from GDAX and +Bitfinex in a 5 minute timeframe, just call ``` -from libcryptomarket.instrument import get_instruments +In [1]: from libcryptomarket.api import GdaxApi, BitfinexApi -instruments = get_instruments() +In [2]: from libcryptomarket.core import historical_ticker + +In [3]: from datetime import datetime + +In [4]: ticker1 = historical_ticker(source=GdaxApi(), symbol="BTC-USD", period=300, start_time=datetime(2017, 12, 30, 12, 0, 0), end_time=datetime(2017, 12, 31, 12, 0, 0)) + +In [5]: ticker2 = historical_ticker(source=BitfinexApi(), symbol="tBTCUSD", period="5m", start_time=datetime(2017, 12, 30, 12, 0, 0), end_time=datetime(2017, 12, 31, 12, 0, 0)) ``` -### Historical +The historical data query automatically rolls over if the exchange response +chunk a long period data. It can guarantee that you do not need to write your +own logic to get a long period of data. -Currently, multiple data sources of historical data are supported. -For example, for the source of [Cryptocompare](https://www.cryptocompare.com/api/#), -run +### Exchange API + +You can easily initialize an exchange API client and call any method stated +in the exchange API official documentation. The return value is always +the native response from library `requests`. + + +For example, to get GDAX order book information, ``` -from datetime import datetime -from libcryptomarket.historical import get_historical_prices +In [1]: from libcryptomarket.api import GdaxApi -prices = get_historical_prices(source='cryptocompare', - symbol='LTC/BTC', - exchange='Poloniex', - period="hour", - from_time=datetime(2017, 5, 1), - to_time=datetime(2017, 8, 1)) +In [2]: exchange = GdaxApi() + +In [3]: exchange.products_book(product_id="BTC-USD").json() +Out[3]: +{'asks': [['14903.01', '22.60310282', 18]], + 'bids': [['14903', '3.4933731', 2]], + 'sequence': 4757644393} ``` -Then you can get historical price in ascending order seamlessly, even though -the limit has exceeded the source limit. The application helps continue -querying until the data reaches the requirements. - -For the source of [Poloniex](https://poloniex.com/support/api/), run +Also, to buy order in Poloniex, ``` -prices = get_historical_prices(source='Poloniex', - symbol='LTC/BTC', - period="30m", - from_time=datetime(2016, 1, 1), - to_time=datetime(2017, 8, 1)) +In [1]: from libcryptomarket.api import PoloniexApi + +In [2]: exchange = PoloniexApi(public_key="", private_key="") + +In [3]: exchange.buy(currencyPair="BTC_LTC", rate=0.001, amount=0.1, postOnly=1) ``` +The exchange API methods are always delimited by underscore. + +| Exchange | Method | libcryptomarket | URL | +|---|---|---|---| +| [Bitfinex](https://docs.bitfinex.com/v2/docs/ws-general) | [candles](https://docs.bitfinex.com/v2/reference#rest-public-candles) | `BitfinexApi().candles(timeframe="1m", symbol="tBTCUSD", session="hist")` | https://api.bitfinex.com/v2/candles/trade:1m:tBTCUSD/hist | +| [BitMEX](https://www.bitmex.com/api/explorer/) | [trades/bucketed](https://www.bitmex.com/api/explorer/#!/Trade/Trade_getBucketed) | `BitmexApi().trade_bucketed(binSize="1m", symbol="XBTUSD")` | https://www.bitmex.com/api/v1/trade/bucketed?binSize=1m&symbol=XBTUSD | +| [CoinMarketCap](https://coinmarketcap.com/api/) | [ticket//](https://coinmarketcap.com/api/) | `CoinMarketCapApi().ticker(id="bitcoin", convert="EUR")` | https://api.coinmarketcap.com/v1/ticker/bitcoin/?convert=EUR | +| [GDAX](https://docs.gdax.com/#api) | [/products//candles](https://docs.gdax.com/#get-historic-rates) | `GdaxApi().products_candles(product_id="BTC-USD")` | https://api.gdax.com/products/BTC-USD/candles | +| [Poloniex](https://poloniex.com/support/api/) | [returnOrderBook](https://poloniex.com/support/api/#returnOrderBook) | `PoloniexApi().return_order_book(currencyPair="BTC_NXT", depth=10)` | https://poloniex.com/public?command=returnOrderBook¤cyPair=BTC_NXT&depth=10 | + ## Contribution The project is targeting as a core but generic toolkit to query cryptocurrency diff --git a/libcryptomarket/api/__init__.py b/libcryptomarket/api/__init__.py index e69de29..f6d1768 100644 --- a/libcryptomarket/api/__init__.py +++ b/libcryptomarket/api/__init__.py @@ -0,0 +1,7 @@ +# pylint: disable-msg=W0401 +# flake8: noqa +from libcryptomarket.api.bitfinex_api import BitfinexApi +from libcryptomarket.api.bitmex_api import BitmexApi +from libcryptomarket.api.coinmarketcap_api import CoinMarketCapApi +from libcryptomarket.api.gdax_api import GdaxApi +from libcryptomarket.api.poloniex_api import PoloniexApi diff --git a/libcryptomarket/api/bitfinex_api.py b/libcryptomarket/api/bitfinex_api.py index c4ad124..723429d 100644 --- a/libcryptomarket/api/bitfinex_api.py +++ b/libcryptomarket/api/bitfinex_api.py @@ -44,8 +44,7 @@ class BitfinexApi(ExchangeApi): def get_private_calls(cls): """Get public API calls. """ - return { - } + return {} @classmethod def translate_call_name(cls, name): @@ -112,7 +111,7 @@ class BitfinexApi(ExchangeApi): :param name: Method name. :param http_method: HTTP method (POST, GET, DELETE). """ - raise NotImplementedError() + raise NotImplementedError("Not support private api at this moment.") # @classmethod # def _generate_auth(cls, public_key, private_key): diff --git a/libcryptomarket/api/bitmex_api.py b/libcryptomarket/api/bitmex_api.py index 72ae672..bb82c51 100644 --- a/libcryptomarket/api/bitmex_api.py +++ b/libcryptomarket/api/bitmex_api.py @@ -6,7 +6,7 @@ class BitmexApi(ExchangeApi): """BitMEX API connector. """ - 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. @@ -82,4 +82,4 @@ class BitmexApi(ExchangeApi): # 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.") + raise NotImplementedError("Not support private api at this moment.") diff --git a/libcryptomarket/api/coinmarketcap_api.py b/libcryptomarket/api/coinmarketcap_api.py index 290de21..2144bf1 100644 --- a/libcryptomarket/api/coinmarketcap_api.py +++ b/libcryptomarket/api/coinmarketcap_api.py @@ -1,64 +1,76 @@ #!/bin/python -import requests -from datetime import datetime +from libcryptomarket.api.exchange_api import ExchangeApi -TICKET_URL = "https://api.coinmarketcap.com/v1/ticker/" - - -class CoinMarketCapApiTicker: - """Result class of query /ticker +class CoinMarketCapApi(ExchangeApi): + """Coinmarketcap API. """ - def __init__(self, **kwargs): + def __init__(self, logger=None): """Constructor. - Constructed from the request result like the following - { - "id": "bitcoin", - "name": "Bitcoin", - "symbol": "BTC", - "rank": "1", - "price_usd": "573.137", - "price_btc": "1.0" - "24h_volume_usd": "72855700.0", - "market_cap_usd": "9080883500.0", - "available_supply": "15844176.0", - "total_supply": "15844176.0", - "percent_change_1h": "0.04", - "percent_change_24h": "-0.3", - "percent_change_7d": "-0.57", - "last_updated": "1472762067" - } + :param public_key: Public key. + :param private_key: Private key. + :param logger: Logger. """ - self.r_id = kwargs["id"] - self.r_name = kwargs["name"] - self.r_symbol = kwargs["symbol"] - self.r_rank = int(kwargs["rank"] or '0') - self.r_price_usd = float(kwargs["price_usd"] or '0') - self.r_price_btc = float(kwargs["price_btc"] or '0') - self.r_24h_volume_usd = float(kwargs["24h_volume_usd"] or '0') - self.r_market_cap_usd = float(kwargs["market_cap_usd"] or '0') - self.r_available_supply = float(kwargs["available_supply"] or '0') - self.r_total_supply = float(kwargs["total_supply"] or '0') - self.r_percent_change_1h = float(kwargs["percent_change_1h"] or '0') - self.r_percent_change_24h = float(kwargs["percent_change_24h"] or '0') - self.r_percent_change_7d = float(kwargs["percent_change_7d"] or '0') - self.r_last_updated = datetime.fromtimestamp( - int(kwargs["last_updated"])) + ExchangeApi.__init__(self, public_key=None, private_key=None, + logger=logger) + @classmethod + def get_url(cls): + """Get API url. + """ + return "https://api.coinmarketcap.com/v1" -def get_ticker(coin=None): - """Return the ticker of all coins or the particular coin. + @classmethod + def get_public_calls(cls): + """Get public API calls. + """ + return { + "ticker": "GET", + "global": "GET" + } - It returns a list of `CoinMarketCapApiTicker` objects. + @classmethod + def get_private_calls(cls): + """Get private API calls. + """ + return {} - :param coin: Coin id. Default None which means all coins are - queued. - """ - url = TICKET_URL + @classmethod + def translate_call_name(cls, name): + """Translate API call name. - if coin is not None: - url += coin + The class method name is always underscored (aligned with Python + standard.) This method is to translate underscored name to exchange + API call name. - return requests.get(url).json() + :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). + """ + name_list = [name] + + if name == "ticker" and "id" in kwargs.keys(): + name_list.append(kwargs["id"]) + del kwargs["id"] + + name_list.append("") + + return self._send_request( + command='/'.join(name_list), 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 RuntimeError("No private method provided") diff --git a/libcryptomarket/api/cryptocompare_api.py b/libcryptomarket/api/cryptocompare_api.py deleted file mode 100644 index bcbf93a..0000000 --- a/libcryptomarket/api/cryptocompare_api.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/python -import requests -from datetime import datetime -import logging - -API_URL = "https://min-api.cryptocompare.com/data/" -MAX_QUERY_LIMIT = 2000 - -logger = logging.getLogger(__name__) - - -class CryptocompareCoinlist: - """Cryptocompare coinlist. - """ - - def __init__(self, **kwargs): - """Constructor. - """ - self.r_algorithm = kwargs['Algorithm'].replace('N/A', '') or '' - self.r_coinname = kwargs['CoinName'] or '' - self.r_fullname = kwargs['FullName'] or '' - self.r_fullypremined = int(kwargs['FullyPremined'].replace('N/A', '') - or 0) - self.r_id = kwargs['Id'] or '' - self.r_imageurl = kwargs.get('ImageUrl', '') or '' - self.r_name = kwargs['Name'] or '' - self.r_preminedvalue = float(kwargs['PreMinedValue'].replace('N/A', '') - or '0') - self.r_prooftype = kwargs['ProofType'].replace('N/A', '') or '' - self.r_sortorder = int(kwargs['SortOrder'].replace('N/A', '') or '0') - self.r_sponsored = kwargs['Sponsored'] or False - self.r_symbol = kwargs['Symbol'] or '' - # TotalCoinSupply and TotalCoinsFreeFloat are not supported due to - # very dirty data. - self.r_url = kwargs['Url'] or '' - - -class CryptocompareHisto: - """Cryptocompare histo. - """ - - def __init__(self, **kwargs): - """Constructor. - """ - self.r_close = float(kwargs['close']) - self.r_high = float(kwargs['high']) - self.r_low = float(kwargs['low']) - self.r_open = float(kwargs['open']) - self.r_time = datetime.fromtimestamp(int(kwargs['time'])) - self.r_volumefrom = float(kwargs['volumefrom']) - self.r_volumeto = float(kwargs['volumeto']) - - -def get_coinlist(): - """Return general info for all coins available. - """ - url = API_URL + "all/coinlist" - - r = requests.get(url) - r.raise_for_status() - return r.json() - - -def get_histo(period, fsym, tsym, e, limit=None, toTs=None): - """Return historical prices. - - :param period: Period, one of the values of "minute", "hour" and "day". - :param fsym: From symbol. - :param tsym: To symbol. - :param e: Exchange name. - :param limit: Limit of return data. Default is None. - :param toTs: To timestamp. Default is None. - """ - valid_list = ["minute", "hour", "day"] - if period not in valid_list: - raise ValueError("Period must be in {0}".format(', '.join(valid_list))) - - url = API_URL + "histo" + period - params = { - "fsym": fsym, - "tsym": tsym, - "e": e - } - - if limit is not None: - params["limit"] = limit - - if toTs is not None: - params["toTs"] = toTs - - r = requests.get(url, params=params) - - # Raise html error status - r.raise_for_status() - - # The api raises a 200 for a warning, but passes a message - rjson = r.json() - if rjson.get("Message", None): - logger.warning( - 'api returned message %r, for url %r', - rjson["Message"], - r.url) - - return rjson diff --git a/libcryptomarket/api/exchange_api.py b/libcryptomarket/api/exchange_api.py index 5001c5c..840fc81 100644 --- a/libcryptomarket/api/exchange_api.py +++ b/libcryptomarket/api/exchange_api.py @@ -45,13 +45,13 @@ class ExchangeApi: def get_public_calls(cls): """Get public API calls. """ - raise NotImplementedError("Public API calls getter not implemented") + return {} @classmethod def get_private_calls(cls): """Get private API calls. """ - raise NotImplementedError("Private API calls getter not implemented") + return {} @classmethod def translate_call_name(cls, name): diff --git a/libcryptomarket/api/gdax_api.py b/libcryptomarket/api/gdax_api.py index fb97c65..cb75032 100644 --- a/libcryptomarket/api/gdax_api.py +++ b/libcryptomarket/api/gdax_api.py @@ -44,8 +44,7 @@ class GdaxApi(ExchangeApi): def get_private_calls(cls): """Get public API calls. """ - return { - } + return {} @classmethod def translate_call_name(cls, name): @@ -83,7 +82,7 @@ class GdaxApi(ExchangeApi): :param name: Method name. :param http_method: HTTP method (POST, GET, DELETE). """ - raise NotImplementedError() + raise NotImplementedError("request private is not implemented.") # @classmethod # def _generate_auth(cls, public_key, private_key): diff --git a/libcryptomarket/api/poloniex_api.py b/libcryptomarket/api/poloniex_api.py index 552f93a..928f8bf 100644 --- a/libcryptomarket/api/poloniex_api.py +++ b/libcryptomarket/api/poloniex_api.py @@ -1,69 +1,9 @@ #!/bin/python -import requests import hmac import hashlib import urllib -# from functools import partial from libcryptomarket.api.exchange_api import ExchangeApi -API_URL = "https://poloniex.com/public?command=" -VALID_PERIODS = [300, 900, 1800, 7200, 14400, 86400] - - -def get_return_chart_data(currency_pair, period, start, end=None): - """Return returnChartData. - - :param currency_pair: Currency pair. For example, BTC_XMR. - :param period: Period. Valid values are 300, 900, 1800, 7200, 14400 and - 86400. - :param start: Start time in unix timestamp. - :param end: End time in unix timestamp. Optional. - """ - - if period not in VALID_PERIODS: - raise ValueError( - "Period is not in the valid periods (%s)" % VALID_PERIODS) - - url = (API_URL + "returnChartData" + - "¤cyPair={0}".format(currency_pair) + - "&period={0}".format(period) + - "&start={0}".format(start)) - - if end is not None: - url += "&end={0}".format(end) - - r = requests.get(url) - r.raise_for_status() - rjson = r.json() - if isinstance(rjson, dict) and 'error' in rjson.keys(): - raise ValueError("Query error from Poloniex API ({0})".format(rjson)) - - return r.json() - - -def get_return_order_book(currency_pair, depth=10): - """Return returnOrderBook. - - :param currency_pair: Currency pair. Specify "all" if requesting for all - symbols. - :param depth: Number of depth. Default is 10. - """ - if currency_pair is None: - raise ValueError("Currency pair cannot be None.") - - params = {} - params["currencyPair"] = currency_pair - params["depth"] = depth - - url = API_URL + "returnOrderBook" - - r = requests.get(url, params=params) - r.raise_for_status() - rjson = r.json() - if isinstance(rjson, dict) and 'error' in rjson.keys(): - raise ValueError("Query error from Poloniex API ({0})".format(rjson)) - - return r.json() class PoloniexApi(ExchangeApi): diff --git a/libcryptomarket/api/rest_api_connector.py b/libcryptomarket/api/rest_api_connector.py deleted file mode 100644 index 39323e8..0000000 --- a/libcryptomarket/api/rest_api_connector.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/python -import requests -import urllib -import json -from time import time - - -class RestApiConnector(object): - """REST API connector. - """ - - def __init__(self, url, logger=None): - """Constructor. - - :param url: URL address. - :param logger: Logger. Default is None. - """ - self._logger = logger - self._url = url - - @classmethod - def _generate_nonce(cls): - """Generate an increasing unique number. - """ - return int(round(time() * 1000)) - - def _generate_headers(self, 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.") - - def _generate_auth(self, public_key, private_key): - """Generate authentication. - - :param public_key: Public key. - :param private_key: Private key. - """ - raise NotImplementedError("Not yet implemented.") - - def _format_data(self, data): - """Format the data to exchange desirable format. - - :param data: Data. - """ - raise NotImplementedError("Not yet implemented.") - - def _send_request(self, command, http_method, public_key, private_key, - params=None, data=None): - """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. - :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.") - - url = urllib.parse.urljoin(self._url, command) - data = self._format_data(data) - headers = self._generate_headers(command, http_method, params, - data, public_key, private_key) - auth = self._generate_auth(public_key, private_key) - - 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/core/__init__.py b/libcryptomarket/core/__init__.py new file mode 100644 index 0000000..8f94529 --- /dev/null +++ b/libcryptomarket/core/__init__.py @@ -0,0 +1,5 @@ +# pylint: disable-msg=W0401 +# flake8: noqa +from libcryptomarket.core.historical import historical_ticker +from libcryptomarket.core.instrument import instruments +from libcryptomarket.core.order_book import order_book diff --git a/libcryptomarket/core.py b/libcryptomarket/core/historical.py similarity index 100% rename from libcryptomarket/core.py rename to libcryptomarket/core/historical.py diff --git a/libcryptomarket/core/instrument.py b/libcryptomarket/core/instrument.py new file mode 100644 index 0000000..d28a1f2 --- /dev/null +++ b/libcryptomarket/core/instrument.py @@ -0,0 +1,18 @@ +import pandas as pd + + +def instruments(source): + """Return instruments. + + :param source: Source, an Exchange API object. + """ + # Source object name + source_name = source.__class__.__name__.lower().replace("api", "") + + if source_name == "coinmarketcap": + response = source.ticker() + response.raise_for_status() + return pd.DataFrame(response.json()) + else: + raise ValueError("Source (%s [%s]) does not support instruments" + % (source, source_name)) diff --git a/libcryptomarket/core/order_book.py b/libcryptomarket/core/order_book.py new file mode 100644 index 0000000..0b985bd --- /dev/null +++ b/libcryptomarket/core/order_book.py @@ -0,0 +1,40 @@ +import pandas as pd + + +def order_book(source, symbol, depth=5): + """Return the order book. + + :param source: Source, an Exchange API object. + :param symbol: Symbol. + :param depth: Depth of the order book. + """ + source_name = source.__class__.__name__.lower().replace("api", "") + + if source_name == "poloniex": + return _order_book_poloniex(source, symbol, depth) + else: + raise ValueError("Source (%s [%s]) does not support order book" + % (source, source_name)) + + +def _order_book_poloniex(source, symbol, depth=5): + """Return the order book from Poloniex + + :param source: Source, an Exchange API object. + :param symbol: Symbol. + :param depth: Depth of the order book. + """ + if symbol == "all": + raise ValueError("Currently not support all symbol order book query.") + + response = source.return_order_book(currencyPair=symbol, depth=depth) + response.raise_for_status() + data = response.json() + data = [pd.DataFrame( + data[side], + columns=pd.MultiIndex.from_product( + [[side], ['price', 'quantity']]), + index=range(1, depth + 1)) for side in ['bids', 'asks']] + data = pd.concat(data, axis=1).astype('float64') + + return data diff --git a/libcryptomarket/historical.py b/libcryptomarket/historical.py deleted file mode 100644 index a87b48d..0000000 --- a/libcryptomarket/historical.py +++ /dev/null @@ -1,206 +0,0 @@ -from functools import partial -from datetime import datetime, timedelta - -import pandas as pd - - -def get_historical_prices(source='cryptocompare', symbol=None, exchange=None, - period=None, limit=0, from_time=None, to_time=None): - """Get historical prices. - - :param source: Source of data. - :param symbol: Symbol. Default is None. - :param exchange: Exchange. Default is None. - :param period: Data frequency. Default is None, which follows the source - default value. - :param limit: Limit of records. Default is 0, which follows the source - default value. - :param from_time: From time. Default is None, which follows the source - default value. - :param to_time: To time. Default is None, which follows the source default - value. - """ - source = source.lower() - - if source == 'cryptocompare': - if period is None: - raise ValueError("Input parameter period cannot be None.") - - if exchange is None: - raise ValueError("Input parameter exchange cannot be None.") - - if ((limit > 0) + - ((from_time is not None) or (to_time is not None)) > 1): - raise ValueError("Only accept input parameter limit, or from_time" - " and to_time pair.") - - if (from_time is None) ^ (to_time is None): - raise ValueError("Cannot accept either from_time or to_time is " - "None") - - return _get_historical_prices_cryptocompare(symbol, exchange, period, - limit, from_time, to_time) - elif source == 'poloniex': - if exchange is not None: - raise ValueError("Poloniex does not need exchange parameter.") - - # Validate and transform symbol - symbol = symbol.replace("/", "_").upper() - - # Validate and transform periods - valid_periods = { - "5m": 300, - "15m": 900, - "30m": 1800, - "2h": 7200, - "4h": 14400, - "1d": 86400 - } - - period = period.lower() - if period not in valid_periods.keys(): - raise ValueError("Periods is not valid. " + - ("Valid values (%s)" % str(valid_periods))) - else: - period = valid_periods[period] - - # Validate and transform to_time and from_time - from_time = from_time.timestamp() - to_time = to_time.timestamp() if to_time is not None else 9999999999 - if to_time <= from_time: - raise ValueError("From time should not be greater than or " - "equal to to time.") - - 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)) - - -def _get_historical_prices_cryptocompare(symbol, exchange, period, - limit, from_time, to_time): - from libcryptomarket.api.cryptocompare_api import ( - get_histo, CryptocompareHisto - ) - - from_sym = symbol.split('/')[0] - to_sym = symbol.split('/')[1] - - func = partial(get_histo, period=period, fsym=from_sym, tsym=to_sym, - e=exchange) - - data = [] - if limit > 0: - # Get the data by limit of records - to_time = 0 - - while limit > 0: - if to_time == 0: - response = func(limit=limit)['Data'] - else: - response = func(limit=limit, toTs=to_time)['Data'] - - if len(response) == 0: - # Terminate if no further response - break - else: - data += response - limit -= len(response) - to_time = response[0]['time'] - 1 - elif from_time is not None and to_time is not None: - # Get the data by time range - from libcryptomarket.api.cryptocompare_api import MAX_QUERY_LIMIT - from_time_ts = int(from_time.timestamp()) - to_time_ts = int(to_time.timestamp()) - - while from_time_ts < to_time_ts: - response = func(limit=MAX_QUERY_LIMIT, - toTs=to_time_ts)['Data'] - - if len(response) == 0: - # Terminate if no further response - break - else: - data += response - to_time_ts = response[0]['time'] - 1 - - else: - data += func()['Data'] - - if len(data) == 0: - return data - - data = pd.DataFrame([CryptocompareHisto(**e).__dict__ for e in data]) - # Filter only valid time range - if from_time is not None and to_time is not None: - data = data[(data['r_time'] >= from_time) & - (data['r_time'] <= to_time)] - - data = data.set_index(['r_time']) - data.index.name = 'datetime' - - return data - - -def _get_historical_prices_poloniex(symbol, period, from_time, to_time): - from libcryptomarket.api.poloniex_api import get_return_chart_data - data = get_return_chart_data(symbol, period, from_time, to_time) - data = pd.DataFrame(data) - data['date'] = data['date'].map( - lambda x: pd.Timestamp(datetime.fromtimestamp(x))) - data.columns = "r_" + data.columns.str.lower() - data = data.set_index(['r_date']) - data.index.name = 'datetime' - return data diff --git a/libcryptomarket/instrument.py b/libcryptomarket/instrument.py deleted file mode 100644 index d955f58..0000000 --- a/libcryptomarket/instrument.py +++ /dev/null @@ -1,26 +0,0 @@ -import pandas as pd - - -def get_instruments(source='coinmarketcap', **kwargs): - """Return all the instruments. - """ - if source == 'coinmarketcap': - # Coinmarketcap - from libcryptomarket.api.coinmarketcap_api import ( - get_ticker, CoinMarketCapApiTicker) - - result = get_ticker(**kwargs) - result = [CoinMarketCapApiTicker(**e) for e in result] - elif source == 'cryptocompare': - # Cryptocompare - from libcryptomarket.api.cryptocompare_api import ( - get_coinlist, CryptocompareCoinlist) - - result = get_coinlist(**kwargs) - result = [CryptocompareCoinlist(**value) - for key, value in result['Data'].items()] - else: - raise ValueError("No source is called {0}".format(source)) - - result = pd.DataFrame([r.__dict__ for r in result]) - return result diff --git a/libcryptomarket/price.py b/libcryptomarket/price.py deleted file mode 100644 index e2ed4aa..0000000 --- a/libcryptomarket/price.py +++ /dev/null @@ -1,43 +0,0 @@ -import pandas as pd - - -def get_order_book(source, symbol, depth=5): - """Return the order book. - - :param source: Data source name. - :param symbol: Symbol. - :param depth: Depth of the order book. - """ - source = source.lower() - - if source == 'poloniex': - from libcryptomarket.api.poloniex_api import get_return_order_book - - # Get the raw data - symbol = symbol.replace("/", "_") - data = get_return_order_book(currency_pair=symbol, depth=depth) - - # Align the format as symbol="all" - if symbol == "all": - # Convert it into a multiindex dataframe with symbol at the first - # level of the columns - data = [[pd.DataFrame( - data[symbol][side], - columns=pd.MultiIndex.from_product( - [[symbol], [side], ['price', 'quantity']]), - index=range(1, depth + 1)) for side in ['bids', 'asks']] - for symbol, prices in data.items()] - data = pd.concat(sum(data, []), axis=1) - else: - # Convert it into a dataframe where bid and ask at the columns - data = [pd.DataFrame( - data[side], - columns=pd.MultiIndex.from_product( - [[side], ['price', 'quantity']]), - index=range(1, depth + 1)) for side in ['bids', 'asks']] - data = pd.concat(data, axis=1) - - return data.astype('float64') - else: - raise ValueError("Source ({0}) is not yet implemented.".format( - source)) diff --git a/setup.py b/setup.py index 478df63..81d7a32 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,8 @@ setup( author="Gavin Chan", author_email='gavincyi@gmail.com', url='https://github.com/gavincyi/libcryptomarket', - packages=['libcryptomarket', 'libcryptomarket.api'], + packages=['libcryptomarket', 'libcryptomarket.api', + 'libcryptomarket.core'], include_package_data=True, install_requires=requirements, license="GNU General Public License v3", diff --git a/tests/test_coinmarketcap_api.py b/tests/test_coinmarketcap_api.py deleted file mode 100644 index 6b25e09..0000000 --- a/tests/test_coinmarketcap_api.py +++ /dev/null @@ -1,134 +0,0 @@ -import requests -import datetime - -import pandas as pd -from pandas.util.testing import assert_frame_equal - -from libcryptomarket.instrument import get_instruments - - -def test_get_instruments_coinmarketcap(monkeypatch): - def mockreturn(url): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - url_split = url.split('/') - if url_split[-1] == "": - # Query all symbols - return [ - { - "id": "bitcoin", - "name": "Bitcoin", - "symbol": "BTC", - "rank": "1", - "price_usd": "573.137", - "price_btc": "1.0", - "24h_volume_usd": "72855700.0", - "market_cap_usd": "9080883500.0", - "available_supply": "15844176.0", - "total_supply": "15844176.0", - "percent_change_1h": "0.04", - "percent_change_24h": "-0.3", - "percent_change_7d": "-0.57", - "last_updated": "1472762067" - }, - { - "id": "ethereum", - "name": "Ethereum", - "symbol": "ETH", - "rank": "2", - "price_usd": "12.1844", - "price_btc": "0.021262", - "24h_volume_usd": "24085900.0", - "market_cap_usd": "1018098455.0", - "available_supply": "83557537.0", - "total_supply": "83557537.0", - "percent_change_1h": "-0.58", - "percent_change_24h": "6.34", - "percent_change_7d": "8.59", - "last_updated": "1472762062" - }] - else: - # Query the particular symbol. Now only test with bitcoin - return [ - { - "id": "bitcoin", - "name": "Bitcoin", - "symbol": "BTC", - "rank": "1", - "price_usd": "573.137", - "price_btc": "1.0", - "24h_volume_usd": "72855700.0", - "market_cap_usd": "9080883500.0", - "available_supply": "15844176.0", - "total_supply": "15844176.0", - "percent_change_1h": "0.04", - "percent_change_24h": "-0.3", - "percent_change_7d": "-0.57", - "last_updated": "1472762067" - }] - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - # Test getting all coins - result = get_instruments(source='coinmarketcap') - expected_result = pd.DataFrame([ - { - "r_id": "bitcoin", - "r_name": "Bitcoin", - "r_symbol": "BTC", - "r_rank": 1, - "r_price_usd": 573.137, - "r_price_btc": 1.0, - "r_24h_volume_usd": 72855700.0, - "r_market_cap_usd": 9080883500.0, - "r_available_supply": 15844176.0, - "r_total_supply": 15844176.0, - "r_percent_change_1h": 0.04, - "r_percent_change_24h": -0.3, - "r_percent_change_7d": -0.57, - "r_last_updated": datetime.datetime(2016, 9, 1, 20, 34, 27) - }, - { - "r_id": "ethereum", - "r_name": "Ethereum", - "r_symbol": "ETH", - "r_rank": 2, - "r_price_usd": 12.1844, - "r_price_btc": 0.021262, - "r_24h_volume_usd": 24085900.0, - "r_market_cap_usd": 1018098455.0, - "r_available_supply": 83557537.0, - "r_total_supply": 83557537.0, - "r_percent_change_1h": -0.58, - "r_percent_change_24h": 6.34, - "r_percent_change_7d": 8.59, - "r_last_updated": datetime.datetime(2016, 9, 1, 20, 34, 22) - }]) - assert_frame_equal(result.set_index(['r_id']).sort_index(), - expected_result.set_index(['r_id']).sort_index()) - - # Test getting only bitcoin - result = get_instruments(source='coinmarketcap', coin='bitcoin') - expected_result = pd.DataFrame([ - { - "r_id": "bitcoin", - "r_name": "Bitcoin", - "r_symbol": "BTC", - "r_rank": 1, - "r_price_usd": 573.137, - "r_price_btc": 1.0, - "r_24h_volume_usd": 72855700.0, - "r_market_cap_usd": 9080883500.0, - "r_available_supply": 15844176.0, - "r_total_supply": 15844176.0, - "r_percent_change_1h": 0.04, - "r_percent_change_24h": -0.3, - "r_percent_change_7d": -0.57, - "r_last_updated": datetime.datetime(2016, 9, 1, 20, 34, 27) - }]) - assert_frame_equal(result.set_index(['r_id']).sort_index(), - expected_result.set_index(['r_id']).sort_index()) diff --git a/tests/test_cryptocompare_api.py b/tests/test_cryptocompare_api.py deleted file mode 100644 index 00018c4..0000000 --- a/tests/test_cryptocompare_api.py +++ /dev/null @@ -1,172 +0,0 @@ -import requests -from datetime import datetime - -import pandas as pd -from pandas.util.testing import assert_frame_equal - -from libcryptomarket.instrument import get_instruments -from libcryptomarket.historical import get_historical_prices - - -def test_get_instruments_cryptocompare(monkeypatch): - def mockreturn(url, *args, **kwargs): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - # Query all symbols - return { - 'BaseImageUrl': 'https://www.cryptocompare.com', - 'BaseLinkUrl': 'https://www.cryptocompare.com', - 'Data': { - 'STX': { - 'Algorithm': 'N/A', - 'CoinName': 'Stox', - 'FullName': 'Stox (STX)', - 'FullyPremined': '0', - 'Id': '204716', - 'ImageUrl': '/media/1383946/stx.png', - 'Name': 'STX', - 'PreMinedValue': 'N/A', - 'ProofType': 'N/A', - 'SortOrder': '1431', - 'Sponsored': False, - 'Symbol': 'STX', - 'TotalCoinSupply': '29600000', - 'TotalCoinsFreeFloat': 'N/A', - 'Url': '/coins/stx/overview'}, - 'BCN': { - 'Algorithm': 'CryptoNight', - 'CoinName': 'ByteCoin', - 'FullName': 'ByteCoin (BCN)', - 'FullyPremined': '0', - 'Id': '5280', - 'ImageUrl': '/media/12318404/bcn.png', - 'Name': 'BCN', - 'PreMinedValue': 'N/A', - 'ProofType': 'PoW', - 'SortOrder': '249', - 'Sponsored': False, - 'Symbol': 'BCN', - 'TotalCoinSupply': '184467440735', - 'TotalCoinsFreeFloat': 'N/A', - 'Url': '/coins/bcn/overview'} - } - } - - @classmethod - def raise_for_status(cls): - pass - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - # Test getting all coins - result = get_instruments(source='cryptocompare') - - assert len(result) == 2 - expected_result = pd.DataFrame([ - { - 'r_algorithm': '', - 'r_coinname': 'Stox', - 'r_fullname': 'Stox (STX)', - 'r_fullypremined': 0, - 'r_id': '204716', - 'r_imageurl': '/media/1383946/stx.png', - 'r_name': 'STX', - 'r_preminedvalue': 0.0, - 'r_prooftype': '', - 'r_sortorder': 1431, - 'r_sponsored': False, - 'r_symbol': 'STX', - 'r_url': '/coins/stx/overview'}, - { - 'r_algorithm': 'CryptoNight', - 'r_coinname': 'ByteCoin', - 'r_fullname': 'ByteCoin (BCN)', - 'r_fullypremined': 0, - 'r_id': '5280', - 'r_imageurl': '/media/12318404/bcn.png', - 'r_name': 'BCN', - 'r_preminedvalue': 0.0, - 'r_prooftype': 'PoW', - 'r_sortorder': 249, - 'r_sponsored': False, - 'r_symbol': 'BCN', - 'r_url': '/coins/bcn/overview'}]) - assert_frame_equal(result.set_index(['r_id']).sort_index(), - expected_result.set_index(['r_id']).sort_index()) - - -def test_get_historical_prices_cryptocompare(monkeypatch): - def mockreturn(url, *args, **kwargs): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - # Query all symbols - return { - 'Aggregated': False, - 'ConversionType': { - 'conversionSymbol': '', - 'type': 'force_direct'}, - 'Data': [ - { - 'close': 0.007707, - 'high': 0.007716, - 'low': 0.007701, - 'open': 0.00771, - 'time': 1510045800, - 'volumefrom': 289.12, - 'volumeto': 2.23 - }, - { - 'close': 0.0077, - 'high': 0.007716, - 'low': 0.0077, - 'open': 0.007707, - 'time': 1510045860, - 'volumefrom': 33.53, - 'volumeto': 0.2586 - - }] - } - - @classmethod - def raise_for_status(cls): - pass - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - # Test to get historical prices - result = get_historical_prices(source='cryptocompare', - period='minute', - exchange='Poloniex', - symbol='LTC/BTC') - - expected_result = pd.DataFrame([ - { - 'r_close': 0.007707, - 'r_high': 0.007716, - 'r_low': 0.007701, - 'r_open': 0.00771, - 'r_time': datetime(2017, 11, 7, 9, 10), - 'r_volumefrom': 289.12, - 'r_volumeto': 2.23 - }, - { - 'r_close': 0.0077, - 'r_high': 0.007716, - 'r_low': 0.0077, - 'r_open': 0.007707, - 'r_time': datetime(2017, 11, 7, 9, 11), - 'r_volumefrom': 33.53, - 'r_volumeto': 0.2586 - - }]).set_index(['r_time']) - expected_result.index.name = 'datetime' - - assert_frame_equal(result, expected_result) diff --git a/tests/test_poloniex_api.py b/tests/test_poloniex_api.py deleted file mode 100644 index 62e5f8f..0000000 --- a/tests/test_poloniex_api.py +++ /dev/null @@ -1,196 +0,0 @@ -import requests -from datetime import datetime - -import pandas as pd -from pandas.util.testing import assert_frame_equal -import pytest - -from libcryptomarket.historical import get_historical_prices -from libcryptomarket.price import get_order_book - - -def test_get_historical_prices_poloniex(monkeypatch): - def mockreturn(url, *args, **kwargs): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - # Query all symbols - return [ - { - "date": 1405699200, - "high": 0.0045388, - "low": 0.00403001, - "open": 0.00404545, - "close": 0.00435873, - "volume": 44.34555992, - "quoteVolume": 10311.88079097, - "weightedAverage": 0.00430043 - }, - { - "date": 1405713600, - "high": 0.00435, - "low": 0.00412, - "open": 0.00428012, - "close": 0.00412, - "volume": 19.12271662, - "quoteVolume": 4531.85801066, - "weightedAverage": 0.00421961 - }] - - @classmethod - def raise_for_status(cls): - pass - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - # Test to get historical prices - result = get_historical_prices(source='Poloniex', - period='4h', - symbol='BTC/XMR', - from_time=datetime(2014, 7, 18, 16, 0, 0)) - - expected_result = pd.DataFrame([ - { - "r_date": datetime(2014, 7, 18, 16, 0, 0), - "r_high": 0.0045388, - "r_low": 0.00403001, - "r_open": 0.00404545, - "r_close": 0.00435873, - "r_volume": 44.34555992, - "r_quotevolume": 10311.88079097, - "r_weightedaverage": 0.00430043 - }, - { - "r_date": datetime(2014, 7, 18, 20, 0, 0), - "r_high": 0.00435, - "r_low": 0.00412, - "r_open": 0.00428012, - "r_close": 0.00412, - "r_volume": 19.12271662, - "r_quotevolume": 4531.85801066, - "r_weightedaverage": 0.00421961 - } - ]).set_index(['r_date']) - expected_result.index.name = 'datetime' - - assert_frame_equal(result, expected_result) - - -def test_get_historical_prices_poloniex_invalid_period(monkeypatch): - with pytest.raises(ValueError): - get_historical_prices( - source='Poloniex', - period='29m', - symbol='BTC/XMR', - from_time=datetime(2014, 7, 18, 16, 0, 0)) - - -def test_get_historical_prices_poloniex_invalid_instrument(monkeypatch): - def mockreturn(url, *args, **kwargs): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - # Query all symbols - return {"error": "Invalid currency pair."} - - @classmethod - def raise_for_status(cls): - pass - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - # Test to get historical prices - with pytest.raises(ValueError): - get_historical_prices( - source='Poloniex', - period='4h', - symbol='BTC/XXX', - from_time=datetime(2014, 7, 18, 16, 0, 0)) - - -def test_get_order_book_poloniex_all_symbols(monkeypatch): - def mockreturn(url, *args, **kwargs): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - # Query all symbols - import json - return json.loads( - """ -{"BTC_AMP": {"asks": [["0.00002755", 16.03393139], - ["0.00002756", 20.24556409]], - "bids": [["0.00002739", 20.23199043], ["0.00002723", 20.30358806]], - "isFrozen": "0", - "seq": 44422443}, - "BTC_ARDR": {"asks": [["0.00003331", 62.49465571], ["0.00003332", 6567.629]], - "bids": [["0.00003285", 25], ["0.00003277", 44.83974499]], - "isFrozen": "0", - "seq": 27008837}} -""" - ) - - @classmethod - def raise_for_status(cls): - pass - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - expected_df = pd.DataFrame([ - [0.00002739, 20.231990, 0.00002755, 16.033931, - 0.00003285, 25.000000, 0.00003331, 62.494656], - [0.00002723, 20.303588, 0.00002756, 20.245564, - 0.00003277, 44.839745, 0.00003332, 6567.629000]], - columns=pd.MultiIndex.from_product( - [['BTC_AMP', 'BTC_ARDR'], - ['bids', 'asks'], - ['price', 'quantity']]), - index=[1, 2]) - - df = get_order_book(source="Poloniex", symbol="all", depth=2) - assert_frame_equal(expected_df.sort_index(axis=1), - df.sort_index(axis=1)) - - -def test_get_order_book_poloniex_one_symbol(monkeypatch): - def mockreturn(url, *args, **kwargs): - # The result is from request.get(...).json() - class MockReturnClass: - @classmethod - def json(cls): - # Query all symbols - import json - return json.loads( - """ -{"asks": [["0.00002755", 16.03393139], - ["0.00002756", 20.24556409]], - "bids": [["0.00002739", 20.23199043], ["0.00002723", 20.30358806]]} -""" - ) - - @classmethod - def raise_for_status(cls): - pass - - return MockReturnClass() - - monkeypatch.setattr(requests, 'get', mockreturn) - - expected_df = pd.DataFrame([ - [0.00002739, 20.231990, 0.00002755, 16.033931], - [0.00002723, 20.303588, 0.00002756, 20.245564]], - columns=pd.MultiIndex.from_product([['bids', 'asks'], - ['price', 'quantity']]), - index=[1, 2]) - - df = get_order_book(source="Poloniex", symbol="BTC_AMP", depth=2) - assert_frame_equal(expected_df, - df)