Incorporate fetch_bids_asks to allow binance spread filter to work

closes #6474
This commit is contained in:
Matthias 2022-03-18 07:08:16 +01:00
parent fdce055061
commit 208a139d2b
3 changed files with 107 additions and 1 deletions

View File

@ -12,6 +12,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,6 +56,15 @@ class Binance(Exchange):
(side == "buy" and stop_loss < float(order['info']['stopPrice'])) (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 @retrier
def _set_leverage( def _set_leverage(
self, self,

View File

@ -104,7 +104,7 @@ class Exchange:
self._last_markets_refresh: int = 0 self._last_markets_refresh: int = 0
# Cache for 10 minutes ... # 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 # 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 # Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration. # refreshed once every iteration.
@ -1289,6 +1289,34 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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 @retrier
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
""" """

View File

@ -1572,6 +1572,59 @@ def test_fetch_positions(default_conf, mocker, exchange_name):
"fetch_positions", "fetch_positions") "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) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_tickers(default_conf, mocker, exchange_name): def test_get_tickers(default_conf, mocker, exchange_name):
api_mock = MagicMock() 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_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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
# retrieve original ticker # retrieve original ticker
tickers = exchange.get_tickers() 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']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5 assert tickers['BCH/BTC']['ask'] == 0.5
assert api_mock.fetch_tickers.call_count == 1 assert api_mock.fetch_tickers.call_count == 1
assert api_mock.fetch_bids_asks.call_count == 0
api_mock.fetch_tickers.reset_mock() 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) tickers2 = exchange.get_tickers(cached=True)
assert tickers2 == tickers assert tickers2 == tickers
assert api_mock.fetch_tickers.call_count == 0 assert api_mock.fetch_tickers.call_count == 0
assert api_mock.fetch_bids_asks.call_count == 0
tickers2 = exchange.get_tickers(cached=False) tickers2 = exchange.get_tickers(cached=False)
assert api_mock.fetch_tickers.call_count == 1 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, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
"get_tickers", "fetch_tickers") "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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_tickers() 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) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_ticker(default_conf, mocker, exchange_name): def test_fetch_ticker(default_conf, mocker, exchange_name):