Improve Binance error handling and resilience

- allow for non-fatal exceptions and retrying of API calls
- catch more common exceptions
- add human error for config.json JSON errors
- update setup files
This commit is contained in:
Ramon Bastiaans 2018-02-04 13:49:42 +01:00
parent c819d73cb0
commit 67b4af5ec4
4 changed files with 259 additions and 87 deletions

View File

@ -2,6 +2,7 @@ import logging
import datetime
import json
import http
from time import sleep
from typing import List, Dict, Optional
from binance.client import Client as _Binance
@ -65,32 +66,117 @@ class Binance(Exchange):
return '{0}_{1}'.format(symbol_stake_currency, symbol_currency)
@staticmethod
def _handle_exception(excepter) -> None:
def _handle_exception(excepter) -> Dict:
"""
Validates the given Binance response/exception
and raises a ContentDecodingError if a non-fatal issue happened.
"""
# Could to alternate exception handling for specific exceptions/errors
# See: http://python-binance.readthedocs.io/en/latest/binance.html#module-binance.exceptions
# Python exceptions:
# http://python-binance.readthedocs.io/en/latest/binance.html#module-binance.exceptions
handle = {}
if type(excepter) == http.client.RemoteDisconnected:
logger.info(
'Got HTTP error from Binance: %s' % excepter
'Retrying: got disconnected from Binance: %s' % excepter
)
return True
handle['retry'] = True
handle['retry_max'] = 3
handle['fatal'] = False
return handle
if type(excepter) == json.decoder.JSONDecodeError:
logger.info(
'Got JSON error from Binance: %s' % excepter
'Retrying: got JSON error from Binance: %s' % excepter
)
return True
handle['retry'] = True
handle['retry_max'] = 3
handle['fatal'] = False
return handle
# API errors:
# https://github.com/binance-exchange/binance-official-api-docs/blob/master/errors.md
if type(excepter) == BinanceAPIException:
logger.info(
'Got API error from Binance: %s' % excepter
)
if excepter.code == -1000:
logger.info(
'Retrying: General unknown API error from Binance: %s' % excepter
)
handle['retry'] = True
handle['retry_max'] = 3
handle['fatal'] = False
return handle
return True
if excepter.code == -1003:
logger.error(
'Binance API Rate limiter hit: %s' % excepter
)
# Panic: this is bad: we don't want to get banned
# TODO: automatic request throttling respecting API rate limits?
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -1021:
logger.error(
"Binance reports invalid timestamp, " +
"check your machine (NTP) time synchronisation: {}".format(
excepter)
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -1015:
logger.error(
'Binance says we have too many orders: %s' % excepter
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -2014:
logger.error(
"Binance reports bad api key format, " +
"you're probably trying to use the API with an empty key/secret: {}".format(
excepter)
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -2015:
logger.error(
"Binance reports invalid api key, source IP or permission, " +
"check your API key settings in config.json and on binance.com: {}".format(
excepter)
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -2011:
logger.error(
"Binance rejected order cancellation: %s" % excepter
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
# All other exceptions we don't know about
logger.info(
'Got error: %s' % excepter
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
raise type(excepter)(excepter.args)
@ -104,18 +190,28 @@ class Binance(Exchange):
symbol = self._pair_to_symbol(pair)
try:
data = _API.order_limit_buy(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=Decimal(rate),
amount=Decimal(amount)))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.order_limit_buy(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal'] or tries == max_tries:
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=Decimal(rate),
amount=Decimal(amount)))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return data['orderId']
@ -123,19 +219,29 @@ class Binance(Exchange):
symbol = self._pair_to_symbol(pair)
try:
data = _API.order_limit_sell(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
Binance._handle_exception(e)
raise OperationalException(
'{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=rate,
amount=amount))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.order_limit_sell(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException(
'{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=rate,
amount=amount))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return data['orderId']
@ -144,10 +250,11 @@ class Binance(Exchange):
try:
data = _API.get_asset_balance(asset=currency)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({currency})'.format(
message=str(e),
currency=currency))
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({currency})'.format(
message=str(e),
currency=currency))
return float(data['free'] or 0.0)
@ -156,8 +263,9 @@ class Binance(Exchange):
try:
data = _API.get_account()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
balances = data['balances']
@ -180,13 +288,24 @@ class Binance(Exchange):
symbol = self._pair_to_symbol(pair)
try:
data = _API.get_ticker(symbol=symbol)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({pair})'.format(
message=str(e),
pair=pair))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_ticker(symbol=symbol)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({pair} {refresh})'.format(
message=str(e),
pair=pair,
refresh=refresh))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return {
'bid': float(data['bidPrice']),
@ -211,13 +330,24 @@ class Binance(Exchange):
symbol = self._pair_to_symbol(pair)
try:
data = _API.get_klines(symbol=symbol, interval=INTERVAL_ENUM)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({pair})'.format(
message=str(e),
pair=pair))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_klines(symbol=symbol, interval=INTERVAL_ENUM)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({pair} {tick_interval})'.format(
message=str(e),
pair=pair,
tick_interval=tick_interval))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
tick_data = []
@ -239,15 +369,25 @@ class Binance(Exchange):
symbol = self._pair_to_symbol(pair)
try:
data = _API.get_all_orders(symbol=symbol, orderId=order_id)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException(
'{message} params=({symbol},{order_id})'.format(
message=str(e),
symbol=symbol,
order_id=order_id))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_all_orders(symbol=symbol, orderId=order_id)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException(
'{message} params=({symbol},{order_id})'.format(
message=str(e),
symbol=symbol,
order_id=order_id))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
order = {}
@ -273,13 +413,24 @@ class Binance(Exchange):
symbol = self._pair_to_symbol(pair)
try:
data = _API.cancel_order(symbol=symbol, orderId=order_id)
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message} params=({order_id})'.format(
message=str(e),
order_id=order_id))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.cancel_order(symbol=symbol, orderId=order_id)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({order_id}, {pair})'.format(
message=str(e),
order_id=order_id),
pair=pair)
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return data
@ -291,8 +442,9 @@ class Binance(Exchange):
try:
data = _API.get_all_tickers()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
markets = []
@ -313,8 +465,9 @@ class Binance(Exchange):
try:
data = _API.get_ticker()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
market_summaries = []
@ -343,11 +496,21 @@ class Binance(Exchange):
def get_trade_qty(self, pair: str) -> tuple:
try:
data = _API.get_exchange_info()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_exchange_info()
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
symbol = self._pair_to_symbol(pair)
@ -368,8 +531,9 @@ class Binance(Exchange):
try:
data = _API.get_exchange_info()
except Exception as e:
Binance._handle_exception(e)
raise OperationalException('{message}'.format(message=str(e)))
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
wallet_health = []

View File

@ -5,6 +5,7 @@ import logging
import time
import os
import re
import sys
from datetime import datetime
from typing import Any, Callable, Dict, List
@ -91,8 +92,14 @@ def load_config(path: str) -> Dict:
:param path: path as str
:return: configuration as dictionary
"""
with open(path) as file:
conf = json.load(file)
try:
with open(path) as file:
conf = json.load(file)
except json.decoder.JSONDecodeError as e:
logger.fatal('Syntax configuration error: invalid JSON format in {path}: {error}'.format(
path=path, error=e))
sys.exit(1)
if 'internals' not in conf:
conf['internals'] = {}
logger.info('Validating configuration ...')

View File

@ -22,6 +22,7 @@ setup(name='freqtrade',
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[
'python-bittrex',
'python-binance',
'SQLAlchemy',
'python-telegram-bot',
'arrow',

View File

@ -98,7 +98,7 @@ function config_generator () {
read -p "Fiat currency: (Default: USD) " fiat_currency
echo "------------------------"
echo "Bittrex config generator"
echo "Exchange config generator"
echo "------------------------"
echo
read -p "Exchange API key: " api_key
@ -205,4 +205,4 @@ plot
help
;;
esac
exit 0
exit 0