class BitMEX(object): """BitMEX API Connector.""" def __init__(self, base_url=None, symbol=None, apiKey=None, apiSecret=None, orderIDPrefix='mm_bitmex_', shouldWSAuth=True, postOnly=False, timeout=7): """Init connector.""" self.logger = logging.getLogger('root') self.base_url = base_url self.symbol = symbol self.postOnly = postOnly if (apiKey is None): raise Exception( "Please set an API key and Secret to get started. See " + "https://github.com/BitMEX/sample-market-maker/#getting-started for more information." ) self.apiKey = apiKey self.apiSecret = apiSecret if len(orderIDPrefix) > 13: raise ValueError( "settings.ORDERID_PREFIX must be at most 13 characters long!") self.orderIDPrefix = orderIDPrefix self.retries = 0 # initialize counter # Prepare HTTPS session self.session = requests.Session() # These headers are always sent self.session.headers.update( {'user-agent': 'liquidbot-' + constants.VERSION}) self.session.headers.update({'content-type': 'application/json'}) self.session.headers.update({'accept': 'application/json'}) # Create websocket for streaming data self.ws = BitMEXWebsocket() self.ws.connect(base_url, symbol, shouldAuth=shouldWSAuth) self.timeout = timeout def __del__(self): self.exit() def exit(self): self.ws.exit() # # Public methods # def ticker_data(self, symbol=None): """Get ticker data.""" if symbol is None: symbol = self.symbol return self.ws.get_ticker(symbol) def instrument(self, symbol): """Get an instrument's details.""" return self.ws.get_instrument(symbol) def instruments(self, filter=None): query = {} if filter is not None: query['filter'] = json.dumps(filter) return self._curl_bitmex(path='instrument', query=query, verb='GET') def market_depth(self, symbol): """Get market depth / orderbook.""" return self.ws.market_depth(symbol) def recent_trades(self): """Get recent trades. Returns ------- A list of dicts: {u'amount': 60, u'date': 1306775375, u'price': 8.7401099999999996, u'tid': u'93842'}, """ return self.ws.recent_trades() # # Authentication required methods # def authentication_required(fn): """Annotation for methods that require auth.""" def wrapped(self, *args, **kwargs): if not (self.apiKey): msg = "You must be authenticated to use this method" raise errors.AuthenticationError(msg) else: return fn(self, *args, **kwargs) return wrapped @authentication_required def funds(self): """Get your current balance.""" return self.ws.funds() @authentication_required def position(self, symbol): """Get your open position.""" return self.ws.position(symbol) @authentication_required def isolate_margin(self, symbol, leverage, rethrow_errors=False): """Set the leverage on an isolated margin position""" path = "position/leverage" postdict = {'symbol': symbol, 'leverage': leverage} return self._curl_bitmex(path=path, postdict=postdict, verb="POST", rethrow_errors=rethrow_errors) @authentication_required def delta(self): return self.position(self.symbol)['homeNotional'] @authentication_required def buy(self, quantity, price): """Place a buy order. Returns order object. ID: orderID """ return self.place_order(quantity, price) @authentication_required def sell(self, quantity, price): """Place a sell order. Returns order object. ID: orderID """ return self.place_order(-quantity, price) @authentication_required def place_order(self, quantity, price): """Place an order.""" if price < 0: raise Exception("Price must be positive.") endpoint = "order" # Generate a unique clOrdID with our prefix so we can identify it. clOrdID = self.orderIDPrefix + base64.b64encode( uuid.uuid4().bytes).decode('utf8').rstrip('=\n') postdict = { 'symbol': self.symbol, 'orderQty': quantity, 'price': price, 'clOrdID': clOrdID } return self._curl_bitmex(path=endpoint, postdict=postdict, verb="POST") @authentication_required def amend_bulk_orders(self, orders): """Amend multiple orders.""" # Note rethrow; if this fails, we want to catch it and re-tick return self._curl_bitmex(path='order/bulk', postdict={'orders': orders}, verb='PUT', rethrow_errors=True) @authentication_required def create_bulk_orders(self, orders): """Create multiple orders.""" for order in orders: order['clOrdID'] = self.orderIDPrefix + base64.b64encode( uuid.uuid4().bytes).decode('utf8').rstrip('=\n') order['symbol'] = self.symbol if self.postOnly: order['execInst'] = 'ParticipateDoNotInitiate' return self._curl_bitmex(path='order/bulk', postdict={'orders': orders}, verb='POST') @authentication_required def open_orders(self): """Get open orders.""" return self.ws.open_orders(self.orderIDPrefix) @authentication_required def http_open_orders(self): """Get open orders via HTTP. Used on close to ensure we catch them all.""" path = "order" orders = self._curl_bitmex(path=path, query={ 'filter': json.dumps({ 'ordStatus.isTerminated': False, 'symbol': self.symbol }), 'count': 500 }, verb="GET") # Only return orders that start with our clOrdID prefix. return [ o for o in orders if str(o['clOrdID']).startswith(self.orderIDPrefix) ] @authentication_required def cancel(self, orderID): """Cancel an existing order.""" path = "order" postdict = { 'orderID': orderID, } return self._curl_bitmex(path=path, postdict=postdict, verb="DELETE") @authentication_required def withdraw(self, amount, fee, address): path = "user/requestWithdrawal" postdict = { 'amount': amount, 'fee': fee, 'currency': 'XBt', 'address': address } return self._curl_bitmex(path=path, postdict=postdict, verb="POST", max_retries=0) def _curl_bitmex(self, path, query=None, postdict=None, timeout=None, verb=None, rethrow_errors=False, max_retries=None): """Send a request to BitMEX Servers.""" # Handle URL url = self.base_url + path if timeout is None: timeout = self.timeout # Default to POST if data is attached, GET otherwise if not verb: verb = 'POST' if postdict else 'GET' # By default don't retry POST or PUT. Retrying GET/DELETE is okay because they are idempotent. # In the future we could allow retrying PUT, so long as 'leavesQty' is not used (not idempotent), # or you could change the clOrdID (set {"clOrdID": "new", "origClOrdID": "old"}) so that an amend # can't erroneously be applied twice. if max_retries is None: max_retries = 0 if verb in ['POST', 'PUT'] else 3 # Auth: API Key/Secret auth = APIKeyAuthWithExpires(self.apiKey, self.apiSecret) def exit_or_throw(e): if rethrow_errors: raise e else: exit(1) def retry(): self.retries += 1 if self.retries > max_retries: raise Exception("Max retries on %s (%s) hit, raising." % (path, json.dumps(postdict or ''))) return self._curl_bitmex(path, query, postdict, timeout, verb, rethrow_errors, max_retries) # Make the request response = None try: self.logger.info("sending req to %s: %s" % (url, json.dumps(postdict or query or ''))) req = requests.Request(verb, url, json=postdict, auth=auth, params=query) prepped = self.session.prepare_request(req) response = self.session.send(prepped, timeout=timeout) # Make non-200s throw response.raise_for_status() except requests.exceptions.HTTPError as e: if response is None: raise e # 401 - Auth error. This is fatal. if response.status_code == 401: self.logger.error( "API Key or Secret incorrect, please check and restart.") self.logger.error("Error: " + response.text) if postdict: self.logger.error(postdict) # Always exit, even if rethrow_errors, because this is fatal exit(1) # 404, can be thrown if order canceled or does not exist. elif response.status_code == 404: if verb == 'DELETE': self.logger.error("Order not found: %s" % postdict['orderID']) return self.logger.error("Unable to contact the BitMEX API (404). " + "Request: %s \n %s" % (url, json.dumps(postdict))) exit_or_throw(e) # 429, ratelimit; cancel orders & wait until X-RateLimit-Reset elif response.status_code == 429: self.logger.error( "Ratelimited on current request. Sleeping, then trying again. Try fewer " + "order pairs or contact [email protected] to raise your limits. " + "Request: %s \n %s" % (url, json.dumps(postdict))) # Figure out how long we need to wait. ratelimit_reset = response.headers['X-RateLimit-Reset'] to_sleep = int(ratelimit_reset) - int(time.time()) reset_str = datetime.datetime.fromtimestamp( int(ratelimit_reset)).strftime('%X') # We're ratelimited, and we may be waiting for a long time. Cancel orders. self.logger.warning( "Canceling all known orders in the meantime.") self.cancel([o['orderID'] for o in self.open_orders()]) self.logger.error( "Your ratelimit will reset at %s. Sleeping for %d seconds." % (reset_str, to_sleep)) time.sleep(to_sleep) # Retry the request. return retry() # 503 - BitMEX temporary downtime, likely due to a deploy. Try again elif response.status_code == 503: self.logger.warning( "Unable to contact the BitMEX API (503), retrying. " + "Request: %s \n %s" % (url, json.dumps(postdict))) time.sleep(3) return retry() elif response.status_code == 400: error = response.json()['error'] message = error['message'].lower() if error else '' # Duplicate clOrdID: that's fine, probably a deploy, go get the order(s) and return it if 'duplicate clordid' in message: orders = postdict[ 'orders'] if 'orders' in postdict else postdict IDs = json.dumps( {'clOrdID': [order['clOrdID'] for order in orders]}) orderResults = self._curl_bitmex('/order', query={'filter': IDs}, verb='GET') for i, order in enumerate(orderResults): if (order['orderQty'] != abs(postdict['orderQty']) or order['side'] != ('Buy' if postdict['orderQty'] > 0 else 'Sell') or order['price'] != postdict['price'] or order['symbol'] != postdict['symbol']): raise Exception( 'Attempted to recover from duplicate clOrdID, but order returned from API ' + 'did not match POST.\nPOST data: %s\nReturned order: %s' % (json.dumps(orders[i]), json.dumps(order))) # All good return orderResults elif 'insufficient available balance' in message: self.logger.error('Account out of funds. The message: %s' % error['message']) exit_or_throw(Exception('Insufficient Funds')) # If we haven't returned or re-raised yet, we get here. self.logger.error("Unhandled Error: %s: %s" % (e, response.text)) self.logger.error("Endpoint was: %s %s: %s" % (verb, path, json.dumps(postdict))) exit_or_throw(e) except requests.exceptions.Timeout as e: # Timeout, re-run this request self.logger.warning("Timed out on request: %s (%s), retrying..." % (path, json.dumps(postdict or ''))) return retry() except requests.exceptions.ConnectionError as e: self.logger.warning( "Unable to contact the BitMEX API (%s). Please check the URL. Retrying. " + "Request: %s %s \n %s" % (e, url, json.dumps(postdict))) time.sleep(1) return retry() # Reset retry counter on success self.retries = 0 return response.json()
class BitMEX(object): """BitMEX API Connector.""" def __init__(self, base_url=None, symbol=None, login=None, password=None, otpToken=None, apiKey=None, apiSecret=None, orderIDPrefix='mm_bitmex_'): """Init connector.""" self.logger = logging.getLogger('root') self.base_url = base_url self.symbol = symbol self.token = None self.login = login self.password = password self.otpToken = otpToken self.apiKey = apiKey self.apiSecret = apiSecret if len(orderIDPrefix) > 13: raise ValueError("settings.ORDERID_PREFIX must be at most 13 characters long!") self.orderIDPrefix = orderIDPrefix # Prepare HTTPS session self.session = requests.Session() # These headers are always sent self.session.headers.update({'user-agent': 'liquidbot-' + constants.VERSION}) # Create websocket for streaming data self.ws = BitMEXWebsocket(base_url, symbol) # # Public methods # def ticker_data(self): """Get ticker data.""" return self.ws.get_ticker() def get_instrument(self): """Get an instrument's details.""" instrument = self.ws.get_instrument() if instrument["state"] != "Open": self.logger.error("The instrument %s is no longer open. State: %s" % (self.symbol, instrument["state"])) exit(1) return instrument def market_depth(self): """Get market depth / orderbook.""" return self.ws.market_depth() def recent_trades(self): """Get recent trades. Returns ------- A list of dicts: {u'amount': 60, u'date': 1306775375, u'price': 8.7401099999999996, u'tid': u'93842'}, """ return self.ws.recent_trades() # # Authentication required methods # def authenticate(self): """Set BitMEX authentication information.""" if self.apiKey: return loginResponse = self._curl_bitmex( api="user/login", postdict={'email': self.login, 'password': self.password, 'token': self.otpToken}) self.token = loginResponse['id'] self.session.headers.update({'access-token': self.token}) def authentication_required(function): """Annotation for methods that require auth.""" def wrapped(self, *args, **kwargs): if not (self.token or self.apiKey): msg = "You must be authenticated to use this method" raise errors.AuthenticationError, msg else: return function(self, *args, **kwargs) return wrapped @authentication_required def funds(self): """Get your current balance.""" return self.ws.funds() @authentication_required def buy(self, quantity, price): """Place a buy order. Returns order object. ID: orderID """ return self.place_order(quantity, price) @authentication_required def sell(self, quantity, price): """Place a sell order. Returns order object. ID: orderID """ return self.place_order(-quantity, price) @authentication_required def place_order(self, quantity, price): """Place an order.""" if price < 0: raise Exception("Price must be positive.") endpoint = "order" # Generate a unique clOrdID with our prefix so we can identify it. clOrdID = self.orderIDPrefix + uuid.uuid4().bytes.encode('base64').rstrip('=\n') postdict = { 'symbol': self.symbol, 'quantity': quantity, 'price': price, 'clOrdID': clOrdID } return self._curl_bitmex(api=endpoint, postdict=postdict, verb="POST") @authentication_required def open_orders(self): """Get open orders.""" return self.ws.open_orders(self.orderIDPrefix) @authentication_required def http_open_orders(self): """Get open orders via HTTP. Used on close to ensure we catch them all.""" api = "order" orders = self._curl_bitmex( api=api, query={'filter': json.dumps({'ordStatus.isTerminated': False, 'symbol': self.symbol})}, verb="GET" ) # Only return orders that start with our clOrdID prefix. return [o for o in orders if str(o['clOrdID']).startswith(self.orderIDPrefix)] @authentication_required def cancel(self, orderID): """Cancel an existing order.""" api = "order" postdict = { 'orderID': orderID, } return self._curl_bitmex(api=api, postdict=postdict, verb="DELETE") def _curl_bitmex(self, api, query=None, postdict=None, timeout=3, verb=None): """Send a request to BitMEX Servers.""" # Handle URL url = self.base_url + api # Default to POST if data is attached, GET otherwise if not verb: verb = 'POST' if postdict else 'GET' # Auth: Use Access Token by default, API Key/Secret if provided auth = AccessTokenAuth(self.token) if self.apiKey: auth = APIKeyAuthWithExpires(self.apiKey, self.apiSecret) # Make the request try: req = requests.Request(verb, url, data=postdict, auth=auth, params=query) prepped = self.session.prepare_request(req) response = self.session.send(prepped, timeout=timeout) # Make non-200s throw response.raise_for_status() except requests.exceptions.HTTPError, e: # 401 - Auth error. Re-auth and re-run this request. if response.status_code == 401: if self.token is None: self.logger.error("Login information or API Key incorrect, please check and restart.") self.logger.error("Error: " + response.text) if postdict: self.logger.error(postdict) exit(1) self.logger.warning("Token expired, reauthenticating...") sleep(1) self.authenticate() return self._curl_bitmex(api, query, postdict, timeout, verb) # 404, can be thrown if order canceled does not exist. elif response.status_code == 404: if verb == 'DELETE': self.logger.error("Order not found: %s" % postdict['orderID']) return self.logger.error("Unable to contact the BitMEX API (404). " + "Request: %s \n %s" % (url, json.dumps(postdict))) exit(1) # 429, ratelimit elif response.status_code == 429: self.logger.error("Ratelimited on current request. Sleeping, then trying again. Try fewer " + "order pairs or contact [email protected] to raise your limits. " + "Request: %s \n %s" % (url, json.dumps(postdict))) sleep(1) return self._curl_bitmex(api, query, postdict, timeout, verb) # 503 - BitMEX temporary downtime, likely due to a deploy. Try again elif response.status_code == 503: self.logger.warning("Unable to contact the BitMEX API (503), retrying. " + "Request: %s \n %s" % (url, json.dumps(postdict))) sleep(1) return self._curl_bitmex(api, query, postdict, timeout, verb) # Unknown Error else: self.logger.error("Unhandled Error: %s: %s %s" % (e, e.message, json.dumps(response.json(), indent=4))) self.logger.error("Endpoint was: %s %s" % (verb, api)) exit(1) except requests.exceptions.Timeout, e: # Timeout, re-run this request self.logger.warning("Timed out, retrying...") return self._curl_bitmex(api, query, postdict, timeout, verb)
class BitMEX(object): """BitMEX API Connector.""" def __init__(self, base_url=None, symbol=None, login=None, password=None, otpToken=None, apiKey=None, apiSecret=None, orderIDPrefix='mm_bitmex_'): """Init connector.""" self.logger = logging.getLogger('root') self.base_url = base_url self.symbol = symbol self.token = None self.login = login self.password = password self.otpToken = otpToken self.apiKey = apiKey self.apiSecret = apiSecret if len(orderIDPrefix) > 13: raise ValueError( "settings.ORDERID_PREFIX must be at most 13 characters long!") self.orderIDPrefix = orderIDPrefix # Prepare HTTPS session self.session = requests.Session() # These headers are always sent self.session.headers.update( {'user-agent': 'liquidbot-' + constants.VERSION}) # Create websocket for streaming data self.ws = BitMEXWebsocket(base_url, symbol) # # Public methods # def ticker_data(self): """Get ticker data.""" return self.ws.get_ticker() def get_instrument(self): """Get an instrument's details.""" instrument = self.ws.get_instrument() if instrument["state"] != "Open": self.logger.error( "The instrument %s is no longer open. State: %s" % (self.symbol, instrument["state"])) exit(1) return instrument def market_depth(self): """Get market depth / orderbook.""" return self.ws.market_depth() def recent_trades(self): """Get recent trades. Returns ------- A list of dicts: {u'amount': 60, u'date': 1306775375, u'price': 8.7401099999999996, u'tid': u'93842'}, """ return self.ws.recent_trades() # # Authentication required methods # def authenticate(self): """Set BitMEX authentication information.""" if self.apiKey: return loginResponse = self._curl_bitmex(api="user/login", postdict={ 'email': self.login, 'password': self.password, 'token': self.otpToken }) self.token = loginResponse['id'] self.session.headers.update({'access-token': self.token}) def authentication_required(function): """Annotation for methods that require auth.""" def wrapped(self, *args, **kwargs): if not (self.token or self.apiKey): msg = "You must be authenticated to use this method" raise errors.AuthenticationError, msg else: return function(self, *args, **kwargs) return wrapped @authentication_required def funds(self): """Get your current balance.""" return self.ws.funds() @authentication_required def buy(self, quantity, price): """Place a buy order. Returns order object. ID: orderID """ return self.place_order(quantity, price) @authentication_required def sell(self, quantity, price): """Place a sell order. Returns order object. ID: orderID """ return self.place_order(-quantity, price) @authentication_required def place_order(self, quantity, price): """Place an order.""" if price < 0: raise Exception("Price must be positive.") endpoint = "order" # Generate a unique clOrdID with our prefix so we can identify it. clOrdID = self.orderIDPrefix + uuid.uuid4().bytes.encode( 'base64').rstrip('=\n') postdict = { 'symbol': self.symbol, 'quantity': quantity, 'price': price, 'clOrdID': clOrdID } return self._curl_bitmex(api=endpoint, postdict=postdict, verb="POST") @authentication_required def open_orders(self): """Get open orders.""" return self.ws.open_orders(self.orderIDPrefix) @authentication_required def http_open_orders(self): """Get open orders via HTTP. Used on close to ensure we catch them all.""" api = "order" orders = self._curl_bitmex(api=api, query={ 'filter': json.dumps({ 'ordStatus.isTerminated': False, 'symbol': self.symbol }) }, verb="GET") # Only return orders that start with our clOrdID prefix. return [ o for o in orders if str(o['clOrdID']).startswith(self.orderIDPrefix) ] @authentication_required def cancel(self, orderID): """Cancel an existing order.""" api = "order" postdict = { 'orderID': orderID, } return self._curl_bitmex(api=api, postdict=postdict, verb="DELETE") def _curl_bitmex(self, api, query=None, postdict=None, timeout=3, verb=None): """Send a request to BitMEX Servers.""" # Handle URL url = self.base_url + api # Default to POST if data is attached, GET otherwise if not verb: verb = 'POST' if postdict else 'GET' # Auth: Use Access Token by default, API Key/Secret if provided auth = AccessTokenAuth(self.token) if self.apiKey: auth = APIKeyAuthWithExpires(self.apiKey, self.apiSecret) # Make the request try: req = requests.Request(verb, url, data=postdict, auth=auth, params=query) prepped = self.session.prepare_request(req) response = self.session.send(prepped, timeout=timeout) # Make non-200s throw response.raise_for_status() except requests.exceptions.HTTPError, e: # 401 - Auth error. Re-auth and re-run this request. if response.status_code == 401: if self.token is None: self.logger.error( "Login information or API Key incorrect, please check and restart." ) self.logger.error("Error: " + response.text) if postdict: self.logger.error(postdict) exit(1) self.logger.warning("Token expired, reauthenticating...") sleep(1) self.authenticate() return self._curl_bitmex(api, query, postdict, timeout, verb) # 404, can be thrown if order canceled does not exist. elif response.status_code == 404: if verb == 'DELETE': self.logger.error("Order not found: %s" % postdict['orderID']) return self.logger.error("Unable to contact the BitMEX API (404). " + "Request: %s \n %s" % (url, json.dumps(postdict))) exit(1) # 429, ratelimit elif response.status_code == 429: self.logger.error( "Ratelimited on current request. Sleeping, then trying again. Try fewer " + "order pairs or contact [email protected] to raise your limits. " + "Request: %s \n %s" % (url, json.dumps(postdict))) sleep(1) return self._curl_bitmex(api, query, postdict, timeout, verb) # 503 - BitMEX temporary downtime, likely due to a deploy. Try again elif response.status_code == 503: self.logger.warning( "Unable to contact the BitMEX API (503), retrying. " + "Request: %s \n %s" % (url, json.dumps(postdict))) sleep(1) return self._curl_bitmex(api, query, postdict, timeout, verb) # Unknown Error else: self.logger.error( "Unhandled Error: %s: %s %s" % (e, e.message, json.dumps(response.json(), indent=4))) self.logger.error("Endpoint was: %s %s" % (verb, api)) exit(1) except requests.exceptions.Timeout, e: # Timeout, re-run this request self.logger.warning("Timed out, retrying...") return self._curl_bitmex(api, query, postdict, timeout, verb)