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, 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: # 401 - Auth error. This is fatal with API keys. if response.status_code == 401: self.logger.error( "Login information or API Key incorrect, please check and restart." ) # 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). ") # 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 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))) time.sleep(3) return self._curl_bitmex(api, query, postdict, timeout, verb) elif response.status_code == 400: error = response.json()['error'] message = error['message'].lower() # Duplicate clOrdID: that's fine, probably a deploy, go get the order and return it if 'duplicate clordid' in message: order = self._curl_bitmex( '/order', query={ 'filter': json.dumps({'clOrdID': postdict['clOrdID']}) }, verb='GET')[0] if (order['orderQty'] != postdict['orderQty'] 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(postdict), json.dumps(order))) # All good return order elif 'insufficient available balance' in message: raise Exception('Account out of funds. The message: %s' % error['message']) # If we haven't returned or re-raised yet, we get here. self.logger.error("Error: %s: %s" % (e, response.text)) self.logger.error("Endpoint was: %s %s: %s" % (verb, api, json.dumps(postdict))) raise e except requests.exceptions.Timeout as e: # Timeout, re-run this request self.logger.warning("Timed out, retrying...") return self._curl_bitmex(api, query, postdict, timeout, verb) except requests.exceptions.ConnectionError as e: self.logger.warning( "Unable to contact the BitMEX API (ConnectionError). Please check the URL. Retrying. " + "Request: %s \n %s" % (url, json.dumps(postdict))) time.sleep(1) return self._curl_bitmex(api, query, postdict, timeout, verb) return response.json()
def _curl_bitmex(self, api, query=None, postdict=None, timeout=3, verb=None, rethrow_errors=False): """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) def maybe_exit(e): if rethrow_errors: raise e else: exit(1) # Make the request try: 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: # 401 - Auth error. This is fatal with API keys. if response.status_code == 401: 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) # Always exit, even if rethrow_errors, because this is fatal exit(1) 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))) maybe_exit(e) # 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) # Duplicate clOrdID: that's fine, probably a deploy, go get the order and return it elif (response.status_code == 400 and response.json()['error'] and response.json()['error']['message'] == 'Duplicate clOrdID'): order = self._curl_bitmex( '/order', query={ 'filter': json.dumps({'clOrdID': postdict['clOrdID']}) }, verb='GET')[0] if (order['orderQty'] != postdict['quantity'] 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(postdict), json.dumps(order))) # All good return order # Unknown Error else: self.logger.error("Unhandled Error: %s: %s" % (e, response.text)) self.logger.error("Endpoint was: %s %s: %s" % (verb, api, json.dumps(postdict))) maybe_exit(e) except requests.exceptions.Timeout as e: # Timeout, re-run this request self.logger.warning("Timed out, retrying...") return self._curl_bitmex(api, query, postdict, timeout, verb) except requests.exceptions.ConnectionError as e: self.logger.warning( "Unable to contact the BitMEX API (ConnectionError). Please check the URL. Retrying. " + "Request: %s \n %s" % (url, json.dumps(postdict))) sleep(1) return self._curl_bitmex(api, query, postdict, timeout, verb) return response.json()