[#9] Add BitMEX API

This commit is contained in:
Gavin.Chan
2017-12-29 08:31:51 +00:00
parent 37ec63150c
commit 7115b45a4a
6 changed files with 517 additions and 68 deletions
+1 -1
View File
@@ -4,4 +4,4 @@
__author__ = """Gavin Chan"""
__email__ = 'gavincyi@gmail.com'
__version__ = '0.1.4'
__version__ = '0.1.5rc1'
+85
View File
@@ -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.")
+216
View File
@@ -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
+115 -66
View File
@@ -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
+50
View File
@@ -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))
+50 -1
View File
@@ -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))