Example #1
0
    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()
Example #2
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()
Example #3
0
    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()