diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 058ca42da..b677f374b 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional from pandas import DataFrame from freqtrade.data.history import load_pair_history -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode from freqtrade.constants import ListPairsWithTimeframes @@ -105,7 +105,7 @@ class DataProvider: """ try: return self._exchange.fetch_ticker(pair) - except DependencyException: + except ExchangeError: return {} def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 7cfed87e8..c85fccc4b 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,21 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(FreqtradeException): +class RetryableOrderError(InvalidOrderException): + """ + This is returned when the order is not found. + This Error will be repeated with increasing backof (in line with DDosError). + """ + + +class ExchangeError(DependencyException): + """ + Error raised out of the exchange. + Has multiple Errors to determine the appropriate error. + """ + + +class TemporaryError(ExchangeError): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user @@ -45,6 +59,13 @@ class TemporaryError(FreqtradeException): """ +class DDosProtection(TemporaryError): + """ + Temporary error caused by DDOS protection. + Bot will wait for a second and then retry. + """ + + class StrategyError(FreqtradeException): """ Errors with custom user-code deteced. diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4279f392c..08e84ee34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,9 +4,11 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -39,6 +41,7 @@ class Binance(Exchange): """ return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss limit order. @@ -77,7 +80,7 @@ class Binance(Exchange): 'stop price: %s. limit: %s', pair, stop_price, rate) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e @@ -88,6 +91,8 @@ class Binance(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index a10d41247..0610e8447 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,10 @@ +import asyncio import logging +import time +from functools import wraps -from freqtrade.exceptions import TemporaryError +from freqtrade.exceptions import (DDosProtection, RetryableOrderError, + TemporaryError) logger = logging.getLogger(__name__) @@ -88,6 +92,13 @@ MAP_EXCHANGE_CHILDCLASS = { } +def calculate_backoff(retrycount, max_retries): + """ + Calculate backoff + """ + return (max_retries - retrycount) ** 2 + 1 + + def retrier_async(f): async def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) @@ -99,6 +110,10 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + await asyncio.sleep(backoff_delay) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -106,19 +121,31 @@ def retrier_async(f): return wrapper -def retrier(f): - def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return f(*args, **kwargs) - except TemporaryError as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - return wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper +def retrier(_func=None, retries=API_RETRY_COUNT): + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + count = kwargs.pop('count', retries) + try: + return f(*args, **kwargs) + except (TemporaryError, RetryableOrderError) as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + # increasing backoff + backoff_delay = calculate_backoff(count + 1, retries) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + time.sleep(backoff_delay) + return wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper + # Support both @retrier and @retrier(retries=2) syntax + if _func is None: + return decorator + else: + return decorator(_func) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b62410c34..a3a548176 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + RetryableOrderError, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback -from freqtrade.constants import ListPairsWithTimeframes CcxtModuleType = Any @@ -351,7 +352,7 @@ class Exchange: for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: Optional[str]) -> None: """ @@ -518,15 +519,17 @@ class Exchange: amount, rate_for_order, params) except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: - raise DependencyException( + raise ExchangeError( f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e @@ -606,6 +609,8 @@ class Exchange: balances.pop("used", None) return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -620,6 +625,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e @@ -630,9 +637,11 @@ class Exchange: def fetch_ticker(self, pair: str) -> dict: try: if pair not in self._api.markets or not self._api.markets[pair].get('active'): - raise DependencyException(f"Pair {pair} not available") + raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e @@ -766,6 +775,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical ' f'candle (OHLCV) data. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' f'for pair {pair} due to {e.__class__.__name__}. ' @@ -802,6 +813,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical trade data.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. ' f'Message: {e}') from e @@ -933,7 +946,7 @@ class Exchange: def check_order_canceled_empty(self, order: Dict) -> bool: """ Verify if an order has been cancelled without being partially filled - :param order: Order dict as returned from get_order() + :param order: Order dict as returned from fetch_order() :return: True if order has been cancelled without being filled, False otherwise. """ return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0 @@ -948,13 +961,15 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes + # Assign method to fetch_stoploss_order to allow easy overriding in other classes cancel_stoploss_order = cancel_order def is_cancel_order_result_suitable(self, corder) -> bool: @@ -968,7 +983,7 @@ class Exchange: """ Cancel order returning a result. Creates a fake result if cancel order returns a non-usable result - and get_order does not work (certain exchanges don't return cancelled orders) + and fetch_order does not work (certain exchanges don't return cancelled orders) :param order_id: Orderid to cancel :param pair: Pair corresponding to order_id :param amount: Amount to use for fake response @@ -981,7 +996,7 @@ class Exchange: except InvalidOrderException: logger.warning(f"Could not cancel order {order_id}.") try: - order = self.get_order(order_id, pair) + order = self.fetch_order(order_id, pair) except InvalidOrderException: logger.warning(f"Could not fetch cancelled order {order_id}.") order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} @@ -989,7 +1004,7 @@ class Exchange: return order @retrier - def get_order(self, order_id: str, pair: str) -> Dict: + def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] @@ -1000,17 +1015,22 @@ class Exchange: f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: return self._api.fetch_order(order_id, pair) + except ccxt.OrderNotFound as e: + raise RetryableOrderError( + f'Order not found (id: {order_id}). Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = get_order + # Assign method to fetch_stoploss_order to allow easy overriding in other classes + fetch_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: @@ -1027,6 +1047,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e @@ -1063,7 +1085,8 @@ class Exchange: matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades - + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e @@ -1080,6 +1103,8 @@ class Exchange: return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, price=price, takerOrMaker=taker_or_maker)['rate'] + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e @@ -1129,7 +1154,7 @@ class Exchange: fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask') return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) - except DependencyException: + except ExchangeError: return None def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f16db96f5..b75f77ca4 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -26,6 +27,7 @@ class Ftx(Exchange): """ return order['type'] == 'stop' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss order. @@ -59,7 +61,7 @@ class Ftx(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e @@ -68,6 +70,8 @@ class Ftx(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e @@ -75,7 +79,7 @@ class Ftx(Exchange): raise OperationalException(e) from e @retrier - def get_stoploss_order(self, order_id: str, pair: str) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] @@ -96,6 +100,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -111,6 +117,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..2ca4ba167 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -45,6 +46,8 @@ class Kraken(Exchange): balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -58,6 +61,7 @@ class Kraken(Exchange): """ return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. @@ -84,7 +88,7 @@ class Kraken(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e @@ -93,6 +97,8 @@ class Kraken(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..bd6bba344 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from requests.exceptions import RequestException from freqtrade import __version__, constants, persistence from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback from freqtrade.pairlist.pairlistmanager import PairListManager @@ -755,7 +755,7 @@ class FreqtradeBot: logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) - except DependencyException: + except ExchangeError: trade.stoploss_order_id = None logger.exception('Unable to place a stoploss order on exchange.') return False @@ -773,8 +773,8 @@ class FreqtradeBot: try: # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ - if trade.stoploss_order_id else None + stoploss_order = self.exchange.fetch_stoploss_order( + trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) @@ -890,8 +890,8 @@ class FreqtradeBot: try: if not trade.open_order_id: continue - order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException, InvalidOrderException): + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) + except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -923,7 +923,7 @@ class FreqtradeBot: for trade in Trade.get_open_order_trades(): try: - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (DependencyException, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -1202,7 +1202,7 @@ class FreqtradeBot: # Update trade with order values logger.info('Found open order for %s', trade) try: - order = action_order or self.exchange.get_order(order_id, trade.pair) + order = action_order or self.exchange.fetch_order(order_id, trade.pair) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 097a2f984..a6c1de402 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -360,7 +360,7 @@ class Trade(_DECL_BASE): def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. - :param order: order retrieved by exchange.get_order() + :param order: order retrieved by exchange.fetch_order() :return: None """ order_type = order['type'] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aeaf82662..cc5f35f0d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from numpy import NAN, mean -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -126,11 +126,11 @@ class RPC: for trade in trades: order = None if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (ExchangeError, PricingError): current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) @@ -174,7 +174,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) @@ -286,7 +286,7 @@ class RPC: # Get current rate try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -352,7 +352,7 @@ class RPC: total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers() - except (TemporaryError, DependencyException): + except (ExchangeError): raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) @@ -373,7 +373,7 @@ class RPC: if pair.startswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total - except (TemporaryError, DependencyException): + except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) @@ -442,7 +442,7 @@ class RPC: def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # Cancel open LIMIT_BUY orders and close trade if order and order['status'] == 'open' \ diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index def3ad535..2f91dcd38 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode from tests.conftest import get_patched_exchange @@ -164,7 +164,7 @@ def test_ticker(mocker, default_conf, tickers): assert 'symbol' in res assert res['symbol'] == 'ETH/BTC' - ticker_mock = MagicMock(side_effect=DependencyException('Pair not found')) + ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found')) mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 52faa284b..72da708b4 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -5,8 +5,9 @@ import ccxt import pytest from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) + OperationalException) from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers @pytest.mark.parametrize('limitratio,expected', [ @@ -62,15 +63,9 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_binance(default_conf, mocker): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 700aff969..251f257f7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4,17 +4,17 @@ import copy import logging from datetime import datetime, timezone from random import randint -from unittest.mock import MagicMock, Mock, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock, patch import arrow import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import API_RETRY_COUNT +from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, timeframe_to_minutes, timeframe_to_msecs, @@ -37,12 +37,20 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - fun, mock_ccxt_fun, **kwargs): + fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): + + with patch('freqtrade.exchange.common.time.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -51,12 +59,21 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 -async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): +async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, + retries=API_RETRY_COUNT + 1, **kwargs): + + with patch('freqtrade.exchange.common.asyncio.sleep', get_mock_coro(None)): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + await getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -1127,9 +1144,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -def test_get_balances_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balances_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.get_balances() == {} @@ -1847,36 +1865,48 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_order(default_conf, mocker, exchange_name): +def test_fetch_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_order('Y', 'TKN/BTC') + exchange.fetch_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_order(order_id='_', pair='TKN/BTC') + exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + with patch('freqtrade.exchange.common.time.sleep') as tm: + with pytest.raises(InvalidOrderException): + exchange.fetch_order(order_id='_', pair='TKN/BTC') + # Ensure backoff is called + assert tm.call_args_list[0][0][0] == 1 + assert tm.call_args_list[1][0][0] == 2 + assert tm.call_args_list[2][0][0] == 5 + assert tm.call_args_list[3][0][0] == 10 + assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_order', 'fetch_order', + 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_stoploss_order(default_conf, mocker, exchange_name): +def test_fetch_stoploss_order(default_conf, mocker, exchange_name): # Don't test FTX here - that needs a seperate test if exchange_name == 'ftx': return @@ -1885,25 +1915,25 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name): order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_stoploss_order', 'fetch_order', + 'fetch_stoploss_order', 'fetch_order', order_id='_', pair='TKN/BTC') @@ -2111,6 +2141,13 @@ def test_get_markets(default_conf, mocker, markets, assert sorted(pairs.keys()) == sorted(expected_keys) +def test_get_markets_error(default_conf, mocker): + ex = get_patched_exchange(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None)) + with pytest.raises(OperationalException, match="Markets were not loaded."): + ex.get_markets('LTC', 'USDT', True, False) + + def test_timeframe_to_minutes(): assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("10m") == 10 @@ -2271,3 +2308,15 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: ex = get_patched_exchange(mocker, default_conf) assert ex.calculate_fee_rate(order) == expected + + +@pytest.mark.parametrize('retrycount,max_retries,expected', [ + (0, 3, 10), + (1, 3, 5), + (2, 3, 2), + (3, 3, 1), + (0, 1, 2), + (1, 1, 1), +]) +def test_calculate_backoff(retrycount, max_retries, expected): + assert calculate_backoff(retrycount, max_retries) == expected diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 75e98740c..eb7d83be3 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange + from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' @@ -85,15 +85,9 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_ftx(default_conf, mocker): @@ -130,34 +124,34 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_get_stoploss_order(default_conf, mocker): +def test_fetch_stoploss_order(default_conf, mocker): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id='ftx') exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' + assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456' api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): - exchange.get_stoploss_order('X', 'TKN/BTC')['status'] + exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_orders.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', - 'get_stoploss_order', 'fetch_orders', + 'fetch_stoploss_order', 'fetch_orders', order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 0950979cf..9451c0b9e 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -6,8 +6,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -206,15 +205,9 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_kraken(default_conf, mocker): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0ffbaa72a..de9327ba9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,12 +8,13 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, TemporaryError from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import get_patched_freqtradebot, patch_get_signal, create_mock_trades +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + patch_get_signal) # Functions for recurrent object patching @@ -106,7 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_rate']) @@ -209,7 +210,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41% (-0.06)' == result[0][3] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] @@ -365,7 +366,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test non-available pair mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' @@ -606,7 +607,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, cancel_order=cancel_order_mock, - get_order=MagicMock( + fetch_order=MagicMock( return_value={ 'status': 'closed', 'type': 'limit', @@ -652,7 +653,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -671,7 +672,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -688,7 +689,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..654e6ca4f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -9,13 +9,12 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -import requests from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, PricingError, - TemporaryError) +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, OperationalException, + PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -763,7 +762,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -832,7 +831,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -859,7 +858,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -1064,7 +1063,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1126,7 +1125,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1139,7 +1138,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1164,7 +1163,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None @@ -1172,18 +1171,18 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, mocker.patch( 'freqtrade.exchange.Exchange.stoploss', - side_effect=DependencyException() + side_effect=ExchangeError() ) trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange.', caplog) assert trade.stoploss_order_id is None - # Fifth case: get_order returns InvalidOrder + # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) @@ -1194,7 +1193,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = None trade.is_open = False stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_order') + mocker.patch('freqtrade.exchange.Exchange.fetch_order') mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1215,8 +1214,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), - stoploss=MagicMock(side_effect=DependencyException()), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}), + stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1249,7 +1248,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=sell_mock, get_fee=fee, - get_order=MagicMock(return_value={'status': 'canceled'}), + fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) freqtrade = FreqtradeBot(default_conf) @@ -1332,7 +1331,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1432,7 +1431,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c } mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1442,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) + mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1512,7 +1511,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -1589,7 +1588,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1613,7 +1612,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = None @@ -1634,7 +1633,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1673,8 +1672,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1698,8 +1697,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ limit_buy_order, mocker, caplog): trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1724,7 +1723,7 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ def test_update_trade_state_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = '123' @@ -1741,7 +1740,7 @@ def test_update_trade_state_exception(mocker, default_conf, def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=InvalidOrderException)) trade = MagicMock() @@ -1757,8 +1756,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) wallet_mock = MagicMock() mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) @@ -1973,7 +1972,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2022,7 +2021,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order_with_result=cancel_order_mock, get_fee=fee ) @@ -2052,7 +2051,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2079,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - get_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=ExchangeError), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2105,7 +2104,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2152,7 +2151,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2183,7 +2182,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2210,7 +2209,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2238,7 +2237,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2276,7 +2275,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2320,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), + fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2774,7 +2773,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None @@ -4017,7 +4016,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, @pytest.mark.usefixtures("init_persistence") def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[DependencyException(), limit_sell_order, limit_buy_order]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') diff --git a/tests/test_integration.py b/tests/test_integration.py index 57960503e..168286e6d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -62,7 +62,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - get_stoploss_order=stoploss_order_mock, + fetch_stoploss_order=stoploss_order_mock, cancel_stoploss_order=cancel_order_mock, )