Move get_buy_rate to exchange class
This commit is contained in:
		| @@ -22,8 +22,8 @@ from pandas import DataFrame | ||||
| from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes | ||||
| from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list | ||||
| from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, | ||||
|                                   InvalidOrderException, OperationalException, RetryableOrderError, | ||||
|                                   TemporaryError) | ||||
|                                   InvalidOrderException, OperationalException, PricingError, | ||||
|                                   RetryableOrderError, TemporaryError) | ||||
| from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, | ||||
|                                        EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, | ||||
|                                        retrier_async) | ||||
| @@ -88,6 +88,7 @@ class Exchange: | ||||
|  | ||||
|         # Cache for 10 minutes ... | ||||
|         self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) | ||||
|         self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) | ||||
|  | ||||
|         # Holds candles | ||||
|         self._klines: Dict[Tuple[str, str], DataFrame] = {} | ||||
| @@ -911,6 +912,52 @@ class Exchange: | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|     def get_buy_rate(self, pair: str, refresh: bool) -> float: | ||||
|         """ | ||||
|         Calculates bid target between current ask price and last price | ||||
|         :param pair: Pair to get rate for | ||||
|         :param refresh: allow cached data | ||||
|         :return: float: Price | ||||
|         :raises PricingError if orderbook price could not be determined. | ||||
|         """ | ||||
|         if not refresh: | ||||
|             rate = self._buy_rate_cache.get(pair) | ||||
|             # Check if cache has been invalidated | ||||
|             if rate: | ||||
|                 logger.debug(f"Using cached buy 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): | ||||
|  | ||||
|             order_book_top = bid_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] | ||||
|             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 | ||||
|  | ||||
|     # Fee handling | ||||
|  | ||||
|     @retrier | ||||
|   | ||||
| @@ -62,7 +62,6 @@ class FreqtradeBot(LoggingMixin): | ||||
|         # Caching only applies to RPC methods, so prices for open trades are still | ||||
|         # refreshed once every iteration. | ||||
|         self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) | ||||
|         self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) | ||||
|  | ||||
|         self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) | ||||
|  | ||||
| @@ -396,50 +395,6 @@ class FreqtradeBot(LoggingMixin): | ||||
|  | ||||
|         return trades_created | ||||
|  | ||||
|     def get_buy_rate(self, pair: str, refresh: bool) -> float: | ||||
|         """ | ||||
|         Calculates bid target between current ask price and last price | ||||
|         :param pair: Pair to get rate for | ||||
|         :param refresh: allow cached data | ||||
|         :return: float: Price | ||||
|         """ | ||||
|         if not refresh: | ||||
|             rate = self._buy_rate_cache.get(pair) | ||||
|             # Check if cache has been invalidated | ||||
|             if rate: | ||||
|                 logger.debug(f"Using cached buy 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): | ||||
|  | ||||
|             order_book_top = bid_strategy.get('order_book_top', 1) | ||||
|             order_book = self.exchange.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] | ||||
|             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.exchange.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 create_trade(self, pair: str) -> bool: | ||||
|         """ | ||||
| @@ -532,7 +487,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|             buy_limit_requested = price | ||||
|         else: | ||||
|             # Calculate price | ||||
|             buy_limit_requested = self.get_buy_rate(pair, True) | ||||
|             buy_limit_requested = self.exchange.get_buy_rate(pair, True) | ||||
|  | ||||
|         if not buy_limit_requested: | ||||
|             raise PricingError('Could not determine buy price.') | ||||
| @@ -657,7 +612,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|         """ | ||||
|         Sends rpc notification when a buy cancel occurred. | ||||
|         """ | ||||
|         current_rate = self.get_buy_rate(trade.pair, False) | ||||
|         current_rate = self.exchange.get_buy_rate(trade.pair, False) | ||||
|  | ||||
|         msg = { | ||||
|             'trade_id': trade.id, | ||||
|   | ||||
| @@ -1684,6 +1684,50 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): | ||||
|         exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ | ||||
|     ('ask', 20, 19, 10, 0.0, 20),  # Full ask side | ||||
|     ('ask', 20, 19, 10, 1.0, 10),  # Full last side | ||||
|     ('ask', 20, 19, 10, 0.5, 15),  # Between ask and last | ||||
|     ('ask', 20, 19, 10, 0.7, 13),  # Between ask and last | ||||
|     ('ask', 20, 19, 10, 0.3, 17),  # Between ask and last | ||||
|     ('ask', 5, 6, 10, 1.0, 5),  # last bigger than ask | ||||
|     ('ask', 5, 6, 10, 0.5, 5),  # last bigger than ask | ||||
|     ('ask', 10, 20, None, 0.5, 10),  # last not available - uses ask | ||||
|     ('ask', 4, 5, None, 0.5, 4),  # last not available - uses ask | ||||
|     ('ask', 4, 5, None, 1, 4),  # last not available - uses ask | ||||
|     ('ask', 4, 5, None, 0, 4),  # last not available - uses ask | ||||
|     ('bid', 21, 20, 10, 0.0, 20),  # Full bid side | ||||
|     ('bid', 21, 20, 10, 1.0, 10),  # Full last side | ||||
|     ('bid', 21, 20, 10, 0.5, 15),  # Between bid and last | ||||
|     ('bid', 21, 20, 10, 0.7, 13),  # Between bid and last | ||||
|     ('bid', 21, 20, 10, 0.3, 17),  # Between bid and last | ||||
|     ('bid', 6, 5, 10, 1.0, 5),  # last bigger than bid | ||||
|     ('bid', 6, 5, 10, 0.5, 5),  # last bigger than bid | ||||
|     ('bid', 21, 20, None, 0.5, 20),  # last not available - uses bid | ||||
|     ('bid', 6, 5, None, 0.5, 5),  # last not available - uses bid | ||||
|     ('bid', 6, 5, None, 1, 5),  # last not available - uses bid | ||||
|     ('bid', 6, 5, None, 0, 5),  # last not available - uses bid | ||||
| ]) | ||||
| def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, | ||||
|                       last, last_ab, expected) -> None: | ||||
|     caplog.set_level(logging.DEBUG) | ||||
|     default_conf['bid_strategy']['ask_last_balance'] = last_ab | ||||
|     default_conf['bid_strategy']['price_side'] = side | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|     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 not log_has("Using cached buy rate for ETH/BTC.", caplog) | ||||
|  | ||||
|     assert exchange.get_buy_rate('ETH/BTC', False) == 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 not log_has("Using cached buy rate for ETH/BTC.", caplog) | ||||
|  | ||||
|  | ||||
| def make_fetch_ohlcv_mock(data): | ||||
|     def fetch_ohlcv_mock(pair, timeframe, since): | ||||
|         if since: | ||||
|   | ||||
| @@ -751,49 +751,6 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: | ||||
|     assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ | ||||
|     ('ask', 20, 19, 10, 0.0, 20),  # Full ask side | ||||
|     ('ask', 20, 19, 10, 1.0, 10),  # Full last side | ||||
|     ('ask', 20, 19, 10, 0.5, 15),  # Between ask and last | ||||
|     ('ask', 20, 19, 10, 0.7, 13),  # Between ask and last | ||||
|     ('ask', 20, 19, 10, 0.3, 17),  # Between ask and last | ||||
|     ('ask', 5, 6, 10, 1.0, 5),  # last bigger than ask | ||||
|     ('ask', 5, 6, 10, 0.5, 5),  # last bigger than ask | ||||
|     ('ask', 10, 20, None, 0.5, 10),  # last not available - uses ask | ||||
|     ('ask', 4, 5, None, 0.5, 4),  # last not available - uses ask | ||||
|     ('ask', 4, 5, None, 1, 4),  # last not available - uses ask | ||||
|     ('ask', 4, 5, None, 0, 4),  # last not available - uses ask | ||||
|     ('bid', 21, 20, 10, 0.0, 20),  # Full bid side | ||||
|     ('bid', 21, 20, 10, 1.0, 10),  # Full last side | ||||
|     ('bid', 21, 20, 10, 0.5, 15),  # Between bid and last | ||||
|     ('bid', 21, 20, 10, 0.7, 13),  # Between bid and last | ||||
|     ('bid', 21, 20, 10, 0.3, 17),  # Between bid and last | ||||
|     ('bid', 6, 5, 10, 1.0, 5),  # last bigger than bid | ||||
|     ('bid', 6, 5, 10, 0.5, 5),  # last bigger than bid | ||||
|     ('bid', 21, 20, None, 0.5, 20),  # last not available - uses bid | ||||
|     ('bid', 6, 5, None, 0.5, 5),  # last not available - uses bid | ||||
|     ('bid', 6, 5, None, 1, 5),  # last not available - uses bid | ||||
|     ('bid', 6, 5, None, 0, 5),  # last not available - uses bid | ||||
| ]) | ||||
| def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, | ||||
|                       last, last_ab, expected) -> None: | ||||
|     caplog.set_level(logging.DEBUG) | ||||
|     default_conf['bid_strategy']['ask_last_balance'] = last_ab | ||||
|     default_conf['bid_strategy']['price_side'] = side | ||||
|     freqtrade = get_patched_freqtradebot(mocker, default_conf) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', | ||||
|                  return_value={'ask': ask, 'last': last, 'bid': bid}) | ||||
|  | ||||
|     assert freqtrade.get_buy_rate('ETH/BTC', True) == expected | ||||
|     assert not log_has("Using cached buy rate for ETH/BTC.", caplog) | ||||
|  | ||||
|     assert freqtrade.get_buy_rate('ETH/BTC', False) == expected | ||||
|     assert log_has("Using cached buy rate for ETH/BTC.", caplog) | ||||
|     # Running a 2nd time with Refresh on! | ||||
|     caplog.clear() | ||||
|     assert freqtrade.get_buy_rate('ETH/BTC', True) == expected | ||||
|     assert not log_has("Using cached buy rate for ETH/BTC.", caplog) | ||||
|  | ||||
|  | ||||
| def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: | ||||
|     patch_RPCManager(mocker) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user