From 208a139d2b2119cff7eda4da8fe89375d5ce0ffa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 07:08:16 +0100 Subject: [PATCH] Incorporate fetch_bids_asks to allow binance spread filter to work closes #6474 --- freqtrade/exchange/binance.py | 10 +++++ freqtrade/exchange/exchange.py | 30 ++++++++++++++- tests/exchange/test_exchange.py | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index bfc363c5e..314337c3e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -12,6 +12,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier +from freqtrade.misc import deep_merge_dicts logger = logging.getLogger(__name__) @@ -55,6 +56,15 @@ class Binance(Exchange): (side == "buy" and stop_loss < float(order['info']['stopPrice'])) ) + def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: + tickers = super().get_tickers(symbols=symbols, cached=cached) + if self.trading_mode == TradingMode.FUTURES: + # Binance's future result has no bid/ask values. + # Therefore we must fetch that from fetch_bids_asks and combine the two results. + bidsasks = self.fetch_bids_asks(symbols, cached) + tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False) + return tickers + @retrier def _set_leverage( self, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a0b1177ad..25c6cbd69 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -104,7 +104,7 @@ class Exchange: self._last_markets_refresh: int = 0 # Cache for 10 minutes ... - self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10) # Cache values for 1800 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. @@ -1289,6 +1289,34 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier + def fetch_bids_asks(self, symbols: List[str] = None, cached: bool = False) -> Dict: + """ + :param cached: Allow cached result + :return: fetch_tickers result + """ + if not self.exchange_has('fetchBidsAsks'): + return {} + if cached: + tickers = self._fetch_tickers_cache.get('fetch_bids_asks') + if tickers: + return tickers + try: + tickers = self._api.fetch_bids_asks(symbols) + self._fetch_tickers_cache['fetch_bids_asks'] = tickers + return tickers + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching bids/asks 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 bids/asks due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5c0b55efe..7438d900d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1572,6 +1572,59 @@ def test_fetch_positions(default_conf, mocker, exchange_name): "fetch_positions", "fetch_positions") +def test_fetch_bids_asks(default_conf, mocker): + api_mock = MagicMock() + tick = {'ETH/BTC': { + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + }, 'BCH/BTC': { + 'symbol': 'BCH/BTC', + 'bid': 0.6, + 'ask': 0.5, + 'last': 41, + } + } + exchange_name = 'binance' + api_mock.fetch_bids_asks = MagicMock(return_value=tick) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + # retrieve original ticker + bidsasks = exchange.fetch_bids_asks() + + assert 'ETH/BTC' in bidsasks + assert 'BCH/BTC' in bidsasks + assert bidsasks['ETH/BTC']['bid'] == 0.5 + assert bidsasks['ETH/BTC']['ask'] == 1 + assert bidsasks['BCH/BTC']['bid'] == 0.6 + assert bidsasks['BCH/BTC']['ask'] == 0.5 + assert api_mock.fetch_bids_asks.call_count == 1 + + api_mock.fetch_bids_asks.reset_mock() + + # Cached ticker should not call api again + tickers2 = exchange.fetch_bids_asks(cached=True) + assert tickers2 == bidsasks + assert api_mock.fetch_bids_asks.call_count == 0 + tickers2 = exchange.fetch_bids_asks(cached=False) + assert api_mock.fetch_bids_asks.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "fetch_bids_asks", "fetch_bids_asks") + + with pytest.raises(OperationalException): + api_mock.fetch_bids_asks = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.fetch_bids_asks() + + api_mock.fetch_bids_asks = MagicMock(return_value={}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.fetch_bids_asks() + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + assert exchange.fetch_bids_asks() == {} + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_tickers(default_conf, mocker, exchange_name): api_mock = MagicMock() @@ -1588,6 +1641,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): } } api_mock.fetch_tickers = MagicMock(return_value=tick) + api_mock.fetch_bids_asks = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker tickers = exchange.get_tickers() @@ -1599,6 +1653,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 assert api_mock.fetch_tickers.call_count == 1 + assert api_mock.fetch_bids_asks.call_count == 0 api_mock.fetch_tickers.reset_mock() @@ -1606,8 +1661,10 @@ def test_get_tickers(default_conf, mocker, exchange_name): tickers2 = exchange.get_tickers(cached=True) assert tickers2 == tickers assert api_mock.fetch_tickers.call_count == 0 + assert api_mock.fetch_bids_asks.call_count == 0 tickers2 = exchange.get_tickers(cached=False) assert api_mock.fetch_tickers.call_count == 1 + assert api_mock.fetch_bids_asks.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") @@ -1621,6 +1678,17 @@ def test_get_tickers(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() + api_mock.fetch_tickers.reset_mock() + api_mock.fetch_bids_asks.reset_mock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + exchange.get_tickers() + assert api_mock.fetch_tickers.call_count == 1 + assert api_mock.fetch_bids_asks.call_count == (1 if exchange_name == 'binance' else 0) + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_ticker(default_conf, mocker, exchange_name):