Merge pull request #619 from gcarq/feature/catch-exchange-errors

granular exception handling and retrying mechanism for ccxt
This commit is contained in:
Michael Egger 2018-05-02 20:13:16 +02:00 committed by GitHub
commit c72d4665a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 188 additions and 158 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
# Freqtrade rules # Freqtrade rules
freqtrade/tests/testdata/*.json freqtrade/tests/testdata/*.json
hyperopt_conf.py hyperopt_conf.py
config.json config*.json
*.sqlite *.sqlite
.hyperopt .hyperopt
logfile.txt logfile.txt

View File

@ -16,9 +16,9 @@ class OperationalException(BaseException):
""" """
class NetworkException(BaseException): class TemporaryError(BaseException):
""" """
Network related error. Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time. has networking problems. Usually resolves itself after a time.
""" """

View File

@ -8,7 +8,7 @@ from datetime import datetime
import ccxt import ccxt
import arrow import arrow
from freqtrade import OperationalException, DependencyException, NetworkException from freqtrade import OperationalException, DependencyException, TemporaryError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
# Current selected exchange # Current selected exchange
_API: ccxt.Exchange = None _API: ccxt.Exchange = None
_CONF: dict = {} _CONF: Dict = {}
API_RETRY_COUNT = 4 API_RETRY_COUNT = 4
# Holds all open sell orders for dry_run # Holds all open sell orders for dry_run
@ -34,15 +34,16 @@ def retrier(f):
count = kwargs.pop('count', API_RETRY_COUNT) count = kwargs.pop('count', API_RETRY_COUNT)
try: try:
return f(*args, **kwargs) return f(*args, **kwargs)
except (NetworkException, DependencyException) as ex: except (TemporaryError, DependencyException) as ex:
logger.warning('%s returned exception: "%s"', f, ex) logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0: if count > 0:
count -= 1 count -= 1
kwargs.update({'count': count}) kwargs.update({'count': count})
logger.warning('retrying %s still for %s times', f, count) logger.warning('retrying %s() still for %s times', f.__name__, count)
return wrapper(*args, **kwargs) return wrapper(*args, **kwargs)
else: else:
raise OperationalException('Giving up retrying: %s', f) logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper return wrapper
@ -153,10 +154,10 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
'Tried to buy amount {} at rate {} (total {}).' 'Tried to buy amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e) 'Message: {}'.format(pair, amount, rate, rate*amount, e)
) )
except ccxt.NetworkError as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise NetworkException( raise TemporaryError(
'Could not place buy order due to networking error. Message: {}'.format(e) 'Could not place buy order due to {}. Message: {}'.format(
) e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@ -191,23 +192,30 @@ def sell(pair: str, rate: float, amount: float) -> Dict:
'Tried to sell amount {} at rate {} (total {}).' 'Tried to sell amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e) 'Message: {}'.format(pair, amount, rate, rate*amount, e)
) )
except ccxt.NetworkError as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise NetworkException( raise TemporaryError(
'Could not place sell order due to networking error. Message: {}'.format(e) 'Could not place sell order due to {}. Message: {}'.format(
) e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@retrier
def get_balance(currency: str) -> float: def get_balance(currency: str) -> float:
if _CONF['dry_run']: if _CONF['dry_run']:
return 999.9 return 999.9
# ccxt exception is already handled by get_balances # ccxt exception is already handled by get_balances
balances = get_balances() balances = get_balances()
return balances[currency]['free'] balance = balances.get(currency)
if balance is None:
raise TemporaryError(
'Could not get {} balance due to malformed exchange response: {}'.format(
currency, balances))
return balance['free']
@retrier
def get_balances() -> dict: def get_balances() -> dict:
if _CONF['dry_run']: if _CONF['dry_run']:
return {} return {}
@ -221,10 +229,10 @@ def get_balances() -> dict:
balances.pop("used", None) balances.pop("used", None)
return balances return balances
except ccxt.NetworkError as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise NetworkException( raise TemporaryError(
'Could not get balance due to networking error. Message: {}'.format(e) 'Could not get balance due to {}. Message: {}'.format(
) e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@ -233,17 +241,17 @@ def get_balances() -> dict:
def get_tickers() -> Dict: def get_tickers() -> Dict:
try: try:
return _API.fetch_tickers() return _API.fetch_tickers()
except ccxt.NetworkError as e:
raise NetworkException(
'Could not load tickers due to networking error. Message: {}'.format(e)
)
except ccxt.BaseError as e:
raise OperationalException(e)
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
'Exchange {} does not support fetching tickers in batch.' 'Exchange {} does not support fetching tickers in batch.'
'Message: {}'.format(_API.name, e) 'Message: {}'.format(_API.name, e)
) )
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load tickers due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
# TODO: remove refresh argument, keeping it to keep track of where it was intended to be used # TODO: remove refresh argument, keeping it to keep track of where it was intended to be used
@ -251,10 +259,10 @@ def get_tickers() -> Dict:
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
try: try:
return _API.fetch_ticker(pair) return _API.fetch_ticker(pair)
except ccxt.NetworkError as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise NetworkException( raise TemporaryError(
'Could not load tickers due to networking error. Message: {}'.format(e) 'Could not load ticker history due to {}. Message: {}'.format(
) e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@ -263,37 +271,39 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
def get_ticker_history(pair: str, tick_interval: str) -> List[Dict]: def get_ticker_history(pair: str, tick_interval: str) -> List[Dict]:
try: try:
return _API.fetch_ohlcv(pair, timeframe=tick_interval) return _API.fetch_ohlcv(pair, timeframe=tick_interval)
except ccxt.NetworkError as e:
raise NetworkException(
'Could not load ticker history due to networking error. Message: {}'.format(e)
)
except ccxt.BaseError as e:
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
'Exchange {} does not support fetching historical candlestick data.' 'Exchange {} does not support fetching historical candlestick data.'
'Message: {}'.format(_API.name, e) 'Message: {}'.format(_API.name, e)
) )
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load ticker history due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
@retrier
def cancel_order(order_id: str, pair: str) -> None: def cancel_order(order_id: str, pair: str) -> None:
if _CONF['dry_run']: if _CONF['dry_run']:
return return
try: try:
return _API.cancel_order(order_id, pair) return _API.cancel_order(order_id, pair)
except ccxt.NetworkError as e:
raise NetworkException(
'Could not get order due to networking error. Message: {}'.format(e)
)
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise DependencyException( raise DependencyException(
'Could not cancel order. Message: {}'.format(e) 'Could not cancel order. Message: {}'.format(e)
) )
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not cancel order due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@retrier
def get_order(order_id: str, pair: str) -> Dict: def get_order(order_id: str, pair: str) -> Dict:
if _CONF['dry_run']: if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id] order = _DRY_RUN_OPEN_ORDERS[order_id]
@ -303,18 +313,19 @@ def get_order(order_id: str, pair: str) -> Dict:
return order return order
try: try:
return _API.fetch_order(order_id, pair) return _API.fetch_order(order_id, pair)
except ccxt.NetworkError as e:
raise NetworkException(
'Could not get order due to networking error. Message: {}'.format(e)
)
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise DependencyException( raise DependencyException(
'Could not get order. Message: {}'.format(e) 'Could not get order. Message: {}'.format(e)
) )
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get order due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@retrier
def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List: def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
if _CONF['dry_run']: if _CONF['dry_run']:
return [] return []
@ -327,7 +338,7 @@ def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
return matched_trades return matched_trades
except ccxt.NetworkError as e: except ccxt.NetworkError as e:
raise NetworkException( raise TemporaryError(
'Could not get trades due to networking error. Message: {}'.format(e) 'Could not get trades due to networking error. Message: {}'.format(e)
) )
except ccxt.BaseError as e: except ccxt.BaseError as e:
@ -345,13 +356,14 @@ def get_pair_detail_url(pair: str) -> str:
return "" return ""
@retrier
def get_markets() -> List[dict]: def get_markets() -> List[dict]:
try: try:
return _API.fetch_markets() return _API.fetch_markets()
except ccxt.NetworkError as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise NetworkException( raise TemporaryError(
'Could not load markets due to networking error. Message: {}'.format(e) 'Could not load markets due to {}. Message: {}'.format(
) e.__class__.__name__, e))
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@ -364,14 +376,22 @@ def get_id() -> str:
return _API.id return _API.id
@retrier
def get_fee(symbol='ETH/BTC', type='', side='', amount=1, def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
price=1, taker_or_maker='maker') -> float: price=1, taker_or_maker='maker') -> float:
# validate that markets are loaded before trying to get fee try:
if _API.markets is None or len(_API.markets) == 0: # validate that markets are loaded before trying to get fee
_API.load_markets() if _API.markets is None or len(_API.markets) == 0:
_API.load_markets()
return _API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, return _API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate'] price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get fee info due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
def get_amount_lots(pair: str, amount: float) -> float: def get_amount_lots(pair: str, amount: float) -> float:

View File

@ -3,7 +3,6 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
""" """
import copy import copy
import json
import logging import logging
import time import time
import traceback import traceback
@ -15,7 +14,8 @@ import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from freqtrade import ( from freqtrade import (
DependencyException, OperationalException, exchange, persistence, __version__ DependencyException, OperationalException, TemporaryError,
exchange, persistence, __version__,
) )
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.constants import Constants from freqtrade.constants import Constants
@ -173,7 +173,7 @@ class FreqtradeBot(object):
self.check_handle_timedout(self.config['unfilledtimeout']) self.check_handle_timedout(self.config['unfilledtimeout'])
Trade.session.flush() Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error: except TemporaryError as error:
logger.warning('%s, retrying in 30 seconds...', error) logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(Constants.RETRY_TIMEOUT) time.sleep(Constants.RETRY_TIMEOUT)
except OperationalException: except OperationalException:
@ -360,27 +360,30 @@ class FreqtradeBot(object):
Tries to execute a sell trade Tries to execute a sell trade
:return: True if executed :return: True if executed
""" """
# Get order details for actual price per unit try:
if trade.open_order_id: # Get order details for actual price per unit
# Update trade with order values if trade.open_order_id:
logger.info('Found open order for %s', trade) # Update trade with order values
order = exchange.get_order(trade.open_order_id, trade.pair) logger.info('Found open order for %s', trade)
# Try update amount (binance-fix) order = exchange.get_order(trade.open_order_id, trade.pair)
try: # Try update amount (binance-fix)
new_amount = self.get_real_amount(trade, order) try:
if order['amount'] != new_amount: new_amount = self.get_real_amount(trade, order)
order['amount'] = new_amount if order['amount'] != new_amount:
# Fee was applied, so set to 0 order['amount'] = new_amount
trade.fee_open = 0 # Fee was applied, so set to 0
trade.fee_open = 0
except OperationalException as exception: except OperationalException as exception:
logger.warning("could not update trade amount: %s", exception) logger.warning("could not update trade amount: %s", exception)
trade.update(order) trade.update(order)
if trade.is_open and trade.open_order_id is None: if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair # Check if we can sell our current pair
return self.handle_trade(trade) return self.handle_trade(trade)
except DependencyException as exception:
logger.warning('Unable to sell trade: %s', exception)
return False return False
def get_real_amount(self, trade: Trade, order: Dict) -> float: def get_real_amount(self, trade: Trade, order: Dict) -> float:
@ -482,7 +485,7 @@ class FreqtradeBot(object):
"""Buy timeout - cancel order """Buy timeout - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
exchange.cancel_order(trade.open_order_id) exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
Trade.session.delete(trade) Trade.session.delete(trade)
@ -512,7 +515,7 @@ class FreqtradeBot(object):
""" """
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade # if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id) exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None trade.close_rate = None
trade.close_profit = None trade.close_profit = None
trade.close_date = None trade.close_date = None

View File

@ -4,15 +4,15 @@ import logging
from copy import deepcopy from copy import deepcopy
from random import randint from random import randint
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import ccxt
import ccxt
import pytest import pytest
from freqtrade import OperationalException, DependencyException, NetworkException import freqtrade.exchange as exchange
from freqtrade import OperationalException, DependencyException, TemporaryError
from freqtrade.exchange import (init, validate_pairs, buy, sell, get_balance, get_balances, from freqtrade.exchange import (init, validate_pairs, buy, sell, get_balance, get_balances,
get_ticker, get_ticker_history, cancel_order, get_name, get_fee, get_ticker, get_ticker_history, cancel_order, get_name, get_fee,
get_id, get_pair_detail_url, get_amount_lots) get_id, get_pair_detail_url, get_amount_lots)
import freqtrade.exchange as exchange
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
API_INIT = False API_INIT = False
@ -149,7 +149,7 @@ def test_buy_prod(default_conf, mocker):
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
buy(pair='ETH/BTC', rate=200, amount=1) buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(NetworkException): with pytest.raises(TemporaryError):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
buy(pair='ETH/BTC', rate=200, amount=1) buy(pair='ETH/BTC', rate=200, amount=1)
@ -199,7 +199,7 @@ def test_sell_prod(default_conf, mocker):
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
sell(pair='ETH/BTC', rate=200, amount=1) sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(NetworkException): with pytest.raises(TemporaryError):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
sell(pair='ETH/BTC', rate=200, amount=1) sell(pair='ETH/BTC', rate=200, amount=1)
@ -263,15 +263,17 @@ def test_get_balances_prod(default_conf, mocker):
assert get_balances()['1ST']['total'] == 10.0 assert get_balances()['1ST']['total'] == 10.0
assert get_balances()['1ST']['used'] == 0.0 assert get_balances()['1ST']['used'] == 0.0
with pytest.raises(NetworkException): with pytest.raises(TemporaryError):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError) api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
get_balances() get_balances()
assert api_mock.fetch_balance.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
get_balances() get_balances()
assert api_mock.fetch_balance.call_count == 1
# This test is somewhat redundant with # This test is somewhat redundant with
@ -311,7 +313,7 @@ def test_get_ticker(default_conf, mocker):
assert ticker['bid'] == 0.5 assert ticker['bid'] == 0.5
assert ticker['ask'] == 1 assert ticker['ask'] == 1
with pytest.raises(OperationalException): # test retrier with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError) api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
get_ticker(pair='ETH/BTC', refresh=True) get_ticker(pair='ETH/BTC', refresh=True)
@ -369,7 +371,7 @@ def test_get_ticker_history(default_conf, mocker):
assert ticks[0][4] == 9 assert ticks[0][4] == 9
assert ticks[0][5] == 10 assert ticks[0][5] == 10
with pytest.raises(OperationalException): # test retrier with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError) api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
# new symbol to get around cache # new symbol to get around cache
@ -398,20 +400,23 @@ def test_cancel_order(default_conf, mocker):
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
assert cancel_order(order_id='_', pair='TKN/BTC') == 123 assert cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(NetworkException): with pytest.raises(TemporaryError):
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
cancel_order(order_id='_', pair='TKN/BTC') cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
cancel_order(order_id='_', pair='TKN/BTC') cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError) api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
cancel_order(order_id='_', pair='TKN/BTC') cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
def test_get_order(default_conf, mocker): def test_get_order(default_conf, mocker):
@ -430,20 +435,23 @@ def test_get_order(default_conf, mocker):
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
assert exchange.get_order('X', 'TKN/BTC') == 456 assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(NetworkException): with pytest.raises(TemporaryError):
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError) api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
def test_get_name(default_conf, mocker): def test_get_name(default_conf, mocker):

View File

@ -6,7 +6,6 @@ import random
from copy import deepcopy from copy import deepcopy
from typing import List from typing import List
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -18,19 +17,6 @@ from freqtrade.arguments import Arguments
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
# Avoid to reinit the same object again and again
_BACKTESTING = None
_BACKTESTING_INITIALIZED = False
@pytest.fixture(scope='function')
def init_backtesting(default_conf, mocker):
global _BACKTESTING_INITIALIZED, _BACKTESTING
if not _BACKTESTING_INITIALIZED:
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
_BACKTESTING = Backtesting(default_conf)
_BACKTESTING_INITIALIZED = True
def get_args(args) -> List[str]: def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg() return Arguments(args, '').get_parsed_arg()
@ -96,8 +82,9 @@ def load_data_test(what):
return data return data
def simple_backtest(config, contour, num_results) -> None: def simple_backtest(config, contour, num_results, mocker) -> None:
backtesting = _BACKTESTING mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(config)
data = load_data_test(contour) data = load_data_test(contour)
processed = backtesting.tickerdata_to_dataframe(data) processed = backtesting.tickerdata_to_dataframe(data)
@ -128,12 +115,14 @@ def _load_pair_as_ticks(pair, tickfreq):
# FIX: fixturize this? # FIX: fixturize this?
def _make_backtest_conf(conf=None, pair='UNITTEST/BTC', record=None): def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair]) data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(conf)
return { return {
'stake_amount': conf['stake_amount'], 'stake_amount': conf['stake_amount'],
'processed': _BACKTESTING.tickerdata_to_dataframe(data), 'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10, 'max_open_trades': 10,
'realistic': True, 'realistic': True,
'record': record 'record': record
@ -169,21 +158,6 @@ def _trend_alternate(dataframe=None):
return dataframe return dataframe
def _run_backtest_1(fun, backtest_conf):
# strategy is a global (hidden as a singleton), so we
# emulate strategy being pure, by override/restore here
# if we dont do this, the override in strategy will carry over
# to other tests
old_buy = _BACKTESTING.populate_buy_trend
old_sell = _BACKTESTING.populate_sell_trend
_BACKTESTING.populate_buy_trend = fun # Override
_BACKTESTING.populate_sell_trend = fun # Override
results = _BACKTESTING.backtest(backtest_conf)
_BACKTESTING.populate_buy_trend = old_buy # restore override
_BACKTESTING.populate_sell_trend = old_sell # restore override
return results
# Unit tests # Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
""" """
@ -287,12 +261,13 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
) )
def test_start(mocker, init_backtesting, fee, default_conf, caplog) -> None: def test_start(mocker, fee, default_conf, caplog) -> None:
""" """
Test start() function Test start() function
""" """
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch('freqtrade.exchange.get_fee', fee) mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
@ -342,16 +317,16 @@ def test_backtesting_init(mocker, default_conf) -> None:
assert callable(backtesting.populate_sell_trend) assert callable(backtesting.populate_sell_trend)
def test_tickerdata_to_dataframe(init_backtesting, default_conf) -> None: def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
""" """
Test Backtesting.tickerdata_to_dataframe() method Test Backtesting.tickerdata_to_dataframe() method
""" """
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
timerange = ((None, 'line'), None, -100) timerange = ((None, 'line'), None, -100)
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
backtesting = _BACKTESTING backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(tickerlist) data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 100 assert len(data['UNITTEST/BTC']) == 100
@ -361,11 +336,12 @@ def test_tickerdata_to_dataframe(init_backtesting, default_conf) -> None:
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
def test_get_timeframe(init_backtesting) -> None: def test_get_timeframe(default_conf, mocker) -> None:
""" """
Test Backtesting.get_timeframe() method Test Backtesting.get_timeframe() method
""" """
backtesting = _BACKTESTING mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe( data = backtesting.tickerdata_to_dataframe(
optimize.load_data( optimize.load_data(
@ -379,11 +355,12 @@ def test_get_timeframe(init_backtesting) -> None:
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00' assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_generate_text_table(init_backtesting): def test_generate_text_table(default_conf, mocker):
""" """
Test Backtesting.generate_text_table() method Test Backtesting.generate_text_table() method
""" """
backtesting = _BACKTESTING mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
results = pd.DataFrame( results = pd.DataFrame(
{ {
@ -451,13 +428,13 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog.record_tuples)
def test_backtest(init_backtesting, default_conf, fee, mocker) -> None: def test_backtest(default_conf, fee, mocker) -> None:
""" """
Test Backtesting.backtest() method Test Backtesting.backtest() method
""" """
mocker.patch('freqtrade.exchange.get_fee', fee) mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = _BACKTESTING backtesting = Backtesting(default_conf)
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
@ -472,13 +449,13 @@ def test_backtest(init_backtesting, default_conf, fee, mocker) -> None:
assert not results.empty assert not results.empty
def test_backtest_1min_ticker_interval(init_backtesting, default_conf, fee, mocker) -> None: def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
""" """
Test Backtesting.backtest() method with 1 min ticker Test Backtesting.backtest() method with 1 min ticker
""" """
mocker.patch('freqtrade.exchange.get_fee', fee) mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = _BACKTESTING backtesting = Backtesting(default_conf)
# Run a backtesting for an exiting 5min ticker_interval # Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
@ -494,11 +471,12 @@ def test_backtest_1min_ticker_interval(init_backtesting, default_conf, fee, mock
assert not results.empty assert not results.empty
def test_processed(init_backtesting) -> None: def test_processed(default_conf, mocker) -> None:
""" """
Test Backtesting.backtest() method with offline data Test Backtesting.backtest() method with offline data
""" """
backtesting = _BACKTESTING mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
dict_of_tickerrows = load_data_test('raise') dict_of_tickerrows = load_data_test('raise')
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows) dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
@ -510,69 +488,90 @@ def test_processed(init_backtesting) -> None:
assert col in cols assert col in cols
def test_backtest_pricecontours(init_backtesting, default_conf, fee, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
tests = [['raise', 17], ['lower', 0], ['sine', 17]] tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests: for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres) simple_backtest(default_conf, contour, numres, mocker)
# Test backtest using offline data (testdata directory) # Test backtest using offline data (testdata directory)
def test_backtest_ticks(init_backtesting, default_conf, fee, mocker): def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.get_fee', fee) mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
ticks = [1, 5] ticks = [1, 5]
fun = _BACKTESTING.populate_buy_trend fun = Backtesting(default_conf).populate_buy_trend
for _ in ticks: for _ in ticks:
backtest_conf = _make_backtest_conf(conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
results = _run_backtest_1(fun, backtest_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert not results.empty assert not results.empty
def test_backtest_clash_buy_sell(init_backtesting, default_conf): def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None):
buy_value = 1 buy_value = 1
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
results = _run_backtest_1(fun, backtest_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
def test_backtest_only_sell(init_backtesting, default_conf): def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None):
buy_value = 0 buy_value = 0
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
results = _run_backtest_1(fun, backtest_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
def test_backtest_alternate_buy_sell(init_backtesting, default_conf, fee, mocker): def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(conf=default_conf, pair='UNITTEST/BTC') mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
results = _run_backtest_1(_trend_alternate, backtest_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf)
assert len(results) == 3 assert len(results) == 3
def test_backtest_record(init_backtesting, default_conf, fee, mocker): def test_backtest_record(default_conf, fee, mocker):
names = [] names = []
records = [] records = []
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
mocker.patch( mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json', 'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r)) new=lambda n, r: (names.append(n), records.append(r))
) )
backtest_conf = _make_backtest_conf( backtest_conf = _make_backtest_conf(
mocker,
conf=default_conf, conf=default_conf,
pair='UNITTEST/BTC', pair='UNITTEST/BTC',
record="trades" record="trades"
) )
results = _run_backtest_1(_trend_alternate, backtest_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf)
assert len(results) == 3 assert len(results) == 3
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
assert names == ['backtest-result.json'] assert names == ['backtest-result.json']
@ -595,7 +594,7 @@ def test_backtest_record(init_backtesting, default_conf, fee, mocker):
assert dur > 0 assert dur > 0
def test_backtest_start_live(init_backtesting, default_conf, mocker, caplog): def test_backtest_start_live(default_conf, mocker, caplog):
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.get_ticker_history', mocker.patch('freqtrade.exchange.get_ticker_history',

View File

@ -16,7 +16,7 @@ import pytest
import requests import requests
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
@ -451,7 +451,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_markets=markets, get_markets=markets,
buy=MagicMock(side_effect=requests.exceptions.RequestException) buy=MagicMock(side_effect=TemporaryError)
) )
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)