diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 42e86db3e..91b278077 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -551,7 +551,7 @@ class Exchange: amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) amount_reserve_percent = ( - amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) @@ -999,94 +999,64 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def get_buy_rate(self, pair: str, refresh: bool) -> float: + def get_rate(self, pair: str, refresh: bool, side: str) -> float: """ - Calculates bid target between current ask price and last price + Calculates bid/ask target + bid rate - between current ask price and last price + ask rate - either using ticker bid or first bid based on orderbook + or remain static in any other case since it's not updating. :param pair: Pair to get rate for :param refresh: allow cached data + :param side: "buy" or "sell" :return: float: Price :raises PricingError if orderbook price could not be determined. """ + cache_rate: TTLCache = self._buy_rate_cache if side == "buy" else self._sell_rate_cache + [strat_name, name] = ['bid_strategy', 'Buy'] if side == "buy" else ['ask_strategy', 'Sell'] + if not refresh: - rate = self._buy_rate_cache.get(pair) + rate = cache_rate.get(pair) # Check if cache has been invalidated if rate: - logger.debug(f"Using cached buy rate for {pair}.") + logger.debug(f"Using cached {side} rate for {pair}.") return rate - bid_strategy = self._config.get('bid_strategy', {}) - if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): + conf_strategy = self._config.get(strat_name, {}) - order_book_top = bid_strategy.get('order_book_top', 1) + if conf_strategy.get('use_order_book', False) and ('use_order_book' in conf_strategy): + + order_book_top = conf_strategy.get('order_book_top', 1) order_book = self.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) # top 1 = index 0 try: - rate_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0] + rate = order_book[f"{conf_strategy['price_side']}s"][order_book_top - 1][0] except (IndexError, KeyError) as e: logger.warning( - "Buy Price from orderbook could not be determined." - f"Orderbook: {order_book}" - ) - raise PricingError from e - logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " - f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") - used_rate = rate_from_l2 - else: - logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") - ticker = self.fetch_ticker(pair) - ticker_rate = ticker[bid_strategy['price_side']] - if ticker['last'] and ticker_rate > ticker['last']: - balance = bid_strategy['ask_last_balance'] - ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) - used_rate = ticker_rate - - self._buy_rate_cache[pair] = used_rate - - return used_rate - - def get_sell_rate(self, pair: str, refresh: bool) -> float: - """ - Get sell rate - either using ticker bid or first bid based on orderbook - or remain static in any other case since it's not updating. - :param pair: Pair to get rate for - :param refresh: allow cached data - :return: Bid rate - :raises PricingError if price could not be determined. - """ - if not refresh: - rate = self._sell_rate_cache.get(pair) - # Check if cache has been invalidated - if rate: - logger.debug(f"Using cached sell rate for {pair}.") - return rate - - ask_strategy = self._config.get('ask_strategy', {}) - if ask_strategy.get('use_order_book', False): - logger.debug( - f"Getting price from order book {ask_strategy['price_side'].capitalize()} side." - ) - order_book_top = ask_strategy.get('order_book_top', 1) - order_book = self.fetch_l2_order_book(pair, order_book_top) - try: - rate = order_book[f"{ask_strategy['price_side']}s"][order_book_top - 1][0] - except (IndexError, KeyError) as e: - logger.warning( - f"Sell Price at location {order_book_top} from orderbook could not be " + f"{name} Price at location {order_book_top} from orderbook could not be " f"determined. Orderbook: {order_book}" ) raise PricingError from e + + logger.info(f"{name} price from orderbook {conf_strategy['price_side'].capitalize()}" + f"side - top {order_book_top} order book {side} rate {rate:.8f}") else: + logger.info(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price") ticker = self.fetch_ticker(pair) - ticker_rate = ticker[ask_strategy['price_side']] - if ticker['last'] and ticker_rate < ticker['last']: - balance = ask_strategy.get('bid_last_balance', 0.0) - ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) + ticker_rate = ticker[conf_strategy['price_side']] + if ticker['last']: + if side == 'buy' and ticker_rate > ticker['last']: + balance = conf_strategy['ask_last_balance'] + ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) + elif side == 'sell' and ticker_rate < ticker['last']: + balance = conf_strategy.get('bid_last_balance', 0.0) + ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) rate = ticker_rate if rate is None: - raise PricingError(f"Sell-Rate for {pair} was empty.") - self._sell_rate_cache[pair] = rate + raise PricingError(f"{name}-Rate for {pair} was empty.") + cache_rate[pair] = rate + return rate # Fee handling @@ -1318,8 +1288,8 @@ class Exchange: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache ohlcv_df = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=self._ohlcv_partial_candle) + ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) results_df[(pair, timeframe)] = ohlcv_df if cache: self._klines[(pair, timeframe)] = ohlcv_df diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5ef109387..d430dbc48 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -475,7 +475,7 @@ class FreqtradeBot(LoggingMixin): buy_limit_requested = price else: # Calculate price - buy_limit_requested = self.exchange.get_buy_rate(pair, True) + buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") if not buy_limit_requested: raise PricingError('Could not determine buy price.') @@ -609,7 +609,7 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a buy cancel occurred. """ - current_rate = self.exchange.get_buy_rate(trade.pair, False) + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") msg = { 'trade_id': trade.id, @@ -695,7 +695,7 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) logger.debug('checking sell') - sell_rate = self.exchange.get_sell_rate(trade.pair, True) + sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True @@ -1132,7 +1132,8 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.exchange.get_sell_rate(trade.pair, False) if not fill else None + current_rate = self.exchange.get_rate( + trade.pair, refresh=False, side="sell") if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" @@ -1177,7 +1178,7 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_sell_rate(trade.pair, False) + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b3198fa1c..902975fde 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -154,7 +154,8 @@ class RPC: # calculate profit and send message to user if trade.is_open: try: - current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_rate( + trade.pair, refresh=False, side="sell") except (ExchangeError, PricingError): current_rate = NAN else: @@ -213,7 +214,8 @@ class RPC: for trade in trades: # calculate profit and send message to user try: - current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_rate( + trade.pair, refresh=False, side="sell") except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) @@ -272,10 +274,10 @@ class RPC: 'date': key, 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( - value['amount'], - stake_currency, - fiat_display_currency - ) if self._fiat_converter else 0, + value['amount'], + stake_currency, + fiat_display_currency + ) if self._fiat_converter else 0, 'trade_count': value["trades"], } for key, value in profit_days.items() @@ -372,7 +374,8 @@ class RPC: else: # Get current rate try: - current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_rate( + trade.pair, refresh=False, side="sell") except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -551,7 +554,8 @@ class RPC: if not fully_canceled: # Get current rate and execute sell - current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) + current_rate = self._freqtrade.exchange.get_rate( + trade.pair, refresh=False, side="sell") sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 524dc873c..02adf01c4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1783,14 +1783,14 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid}) - assert exchange.get_buy_rate('ETH/BTC', True) == expected + assert exchange.get_rate('ETH/BTC', refresh=True, side="buy") == expected assert not log_has("Using cached buy rate for ETH/BTC.", caplog) - assert exchange.get_buy_rate('ETH/BTC', False) == expected + assert exchange.get_rate('ETH/BTC', refresh=False, side="buy") == expected assert log_has("Using cached buy rate for ETH/BTC.", caplog) # Running a 2nd time with Refresh on! caplog.clear() - assert exchange.get_buy_rate('ETH/BTC', True) == expected + assert exchange.get_rate('ETH/BTC', refresh=True, side="buy") == expected assert not log_has("Using cached buy rate for ETH/BTC.", caplog) @@ -1825,12 +1825,12 @@ def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, # Test regular mode exchange = get_patched_exchange(mocker, default_conf) - rate = exchange.get_sell_rate(pair, True) + rate = exchange.get_rate(pair, refresh=True, side="sell") assert not log_has("Using cached sell rate for ETH/BTC.", caplog) assert isinstance(rate, float) assert rate == expected # Use caching - rate = exchange.get_sell_rate(pair, False) + rate = exchange.get_rate(pair, refresh=False, side="sell") assert rate == expected assert log_has("Using cached sell rate for ETH/BTC.", caplog) @@ -1848,11 +1848,11 @@ def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, o pair = "ETH/BTC" mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) exchange = get_patched_exchange(mocker, default_conf) - rate = exchange.get_sell_rate(pair, True) + rate = exchange.get_rate(pair, refresh=True, side="sell") assert not log_has("Using cached sell rate for ETH/BTC.", caplog) assert isinstance(rate, float) assert rate == expected - rate = exchange.get_sell_rate(pair, False) + rate = exchange.get_rate(pair, refresh=False, side="sell") assert rate == expected assert log_has("Using cached sell rate for ETH/BTC.", caplog) @@ -1868,7 +1868,7 @@ def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog): return_value={'bids': [[]], 'asks': [[]]}) exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError): - exchange.get_sell_rate(pair, True) + exchange.get_rate(pair, refresh=True, side="sell") assert log_has_re(r"Sell Price at location 1 from orderbook could not be determined\..*", caplog) @@ -1881,18 +1881,18 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): return_value={'ask': None, 'bid': 0.12, 'last': None}) exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): - exchange.get_sell_rate(pair, True) + exchange.get_rate(pair, refresh=True, side="sell") exchange._config['ask_strategy']['price_side'] = 'bid' - assert exchange.get_sell_rate(pair, True) == 0.12 + assert exchange.get_rate(pair, refresh=True, side="sell") == 0.12 # Reverse sides mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': 0.13, 'bid': None, 'last': None}) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): - exchange.get_sell_rate(pair, True) + exchange.get_rate(pair, refresh=True, side="sell") exchange._config['ask_strategy']['price_side'] = 'ask' - assert exchange.get_sell_rate(pair, True) == 0.13 + assert exchange.get_rate(pair, refresh=True, side="sell") == 0.13 def make_fetch_ohlcv_mock(data): @@ -2203,7 +2203,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): ({'status': 'canceled', 'filled': 10.0}, False), ({'status': 'unknown', 'filled': 10.0}, False), ({'result': 'testest123'}, False), - ]) +]) def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, result): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.check_order_canceled_empty(order) == result diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0049e59bb..fad24f9e2 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,7 +109,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'exchange': 'binance', } - mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) @@ -217,7 +217,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41% (-0.06)' == result[0][3] assert '-0.06' == f'{fiat_profit_sum:.2f}' - mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] @@ -427,7 +427,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert prec_satoshi(stats['best_rate'], 6.2) # Test non-available pair - mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 2 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index eeb829719..921d8160d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -879,7 +879,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'exchange': 'binance', } - mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', + mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) rc = client_get(client, f"{BASE_URI}/status") diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index addf72bbb..4912a2a4d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -161,7 +161,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: (True, 0.0022, 3, 0.5, [0.001, 0.001, 0.0]), (True, 0.0027, 3, 0.5, [0.001, 0.001, 0.000673]), (True, 0.0022, 3, 1, [0.001, 0.001, 0.0]), - ]) +]) def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order_open, amend_last, wallet, max_open, lsamr, expected) -> None: patch_RPCManager(mocker) @@ -784,7 +784,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_buy_rate=buy_rate_mock, + get_rate=buy_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, @@ -824,7 +824,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order_open['id'] = '33' fix_price = 0.06 assert freqtrade.execute_buy(pair, stake_amount, fix_price) - # Make sure get_buy_rate wasn't called again + # Make sure get_rate wasn't called again assert buy_rate_mock.call_count == 0 assert buy_mm.call_count == 2 @@ -893,7 +893,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order assert not freqtrade.execute_buy(pair, stake_amount) # Fail to get price... - mocker.patch('freqtrade.exchange.Exchange.get_buy_rate', MagicMock(return_value=0.0)) + mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) with pytest.raises(PricingError, match="Could not determine buy price."): freqtrade.execute_buy(pair, stake_amount) @@ -909,7 +909,7 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - 'last': 0.00001172 }), buy=MagicMock(return_value=limit_buy_order), - get_buy_rate=MagicMock(return_value=0.11), + get_rate=MagicMock(return_value=0.11), get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) @@ -2513,7 +2513,7 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: 'freqtrade.exchange.Exchange', cancel_order=cancel_order_mock, ) - mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', return_value=0.245441) + mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441) freqtrade = FreqtradeBot(default_conf) @@ -3956,7 +3956,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: """ - test if function get_buy_rate will return the order book price + test if function get_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) @@ -3974,7 +3974,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.exchange.get_buy_rate('ETH/BTC', True) == 0.043935 + assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 assert ticker_mock.call_count == 0 @@ -3996,8 +3996,8 @@ def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None freqtrade = FreqtradeBot(default_conf) # orderbook shall be used even if tickers would be lower. with pytest.raises(PricingError): - freqtrade.exchange.get_buy_rate('ETH/BTC', refresh=True) - assert log_has_re(r'Buy Price from orderbook could not be determined.', caplog) + freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") + assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: