mirror of
https://github.com/wassname/libcryptomarket.git
synced 2026-09-09 11:25:52 +08:00
[#9] Add BitMEX API
This commit is contained in:
@@ -4,4 +4,4 @@
|
||||
|
||||
__author__ = """Gavin Chan"""
|
||||
__email__ = 'gavincyi@gmail.com'
|
||||
__version__ = '0.1.4'
|
||||
__version__ = '0.1.5rc1'
|
||||
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user