diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 875cff6ee..55c7cff2b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,7 @@ import asyncio import inspect import logging import signal -from collections.abc import Coroutine +from collections.abc import Coroutine, Generator from copy import deepcopy from datetime import datetime, timedelta, timezone from math import floor, isnan @@ -705,14 +705,22 @@ class Exchange: f"Available currencies are: {', '.join(quote_currencies)}" ) - def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str: + def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> Generator[str, None, None]: """ Get valid pair combination of curr_1 and curr_2 by trying both combinations. """ - for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: + yielded = False + for pair in ( + f"{curr_1}/{curr_2}", + f"{curr_2}/{curr_1}", + f"{curr_1}/{curr_2}:{curr_2}", + f"{curr_2}/{curr_1}:{curr_1}", + ): if pair in self.markets and self.markets[pair].get("active"): - return pair - raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + yielded = True + yield pair + if not yielded: + raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: str | None) -> None: """ @@ -1868,26 +1876,25 @@ class Exchange: return 1.0 tickers = self.get_tickers(cached=True) try: - pair = self.get_valid_pair_combination(coin, currency) + for pair in self.get_valid_pair_combination(coin, currency): + ticker: Ticker | None = tickers.get(pair, None) + if not ticker: + tickers_other: Tickers = self.get_tickers( + cached=True, + market_type=( + TradingMode.SPOT + if self.trading_mode != TradingMode.SPOT + else TradingMode.FUTURES + ), + ) + ticker = tickers_other.get(pair, None) + if ticker: + rate: float | None = ticker.get("last", None) + if rate and pair.startswith(currency) and not pair.endswith(currency): + rate = 1.0 / rate + return rate except ValueError: return None - - ticker: Ticker | None = tickers.get(pair, None) - if not ticker: - tickers_other: Tickers = self.get_tickers( - cached=True, - market_type=( - TradingMode.SPOT - if self.trading_mode != TradingMode.SPOT - else TradingMode.FUTURES - ), - ) - ticker = tickers_other.get(pair, None) - if ticker: - rate: float | None = ticker.get("last", None) - if rate and pair.startswith(currency) and not pair.endswith(currency): - rate = 1.0 / rate - return rate return None @retrier @@ -2244,10 +2251,13 @@ class Exchange: # If cost is None or 0.0 -> falsy, return None return None try: - comb = self.get_valid_pair_combination(fee_curr, self._config["stake_currency"]) - tick = self.fetch_ticker(comb) - - fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask") + for comb in self.get_valid_pair_combination( + fee_curr, self._config["stake_currency"] + ): + tick = self.fetch_ticker(comb) + fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask") + if tick: + break except (ValueError, ExchangeError): fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None) if not fee_to_quote_rate: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 085982d1d..260dd8f0e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2021,10 +2021,7 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): }, } tick2 = { - "XRP/USDT": { - "symbol": "XRP/USDT", - "bid": 0.5, - "ask": 1, + "ADA/USDT:USDT": { "last": 2.5, } } @@ -2044,7 +2041,7 @@ def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name): assert api_mock.fetch_tickers.call_count == 1 api_mock.fetch_tickers.reset_mock() - assert exchange.get_conversion_rate("XRP", "USDT") == 2.5 + assert exchange.get_conversion_rate("ADA", "USDT") == 2.5 # Only the call to the "others" market assert api_mock.fetch_tickers.call_count == 1 @@ -4122,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): ) ex = Exchange(default_conf) - assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" - assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" + assert next(ex.get_valid_pair_combination("ETH", "BTC")) == "ETH/BTC" + assert next(ex.get_valid_pair_combination("BTC", "ETH")) == "ETH/BTC" + multicombs = list(ex.get_valid_pair_combination("ETH", "USDT")) + assert len(multicombs) == 2 + assert "ETH/USDT" in multicombs + assert "ETH/USDT:USDT" in multicombs + with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."): - ex.get_valid_pair_combination("NOPAIR", "ETH") + for x in ex.get_valid_pair_combination("NOPAIR", "ETH"): + pass @pytest.mark.parametrize( diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 21e5394a4..937bec8e5 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -4021,7 +4021,7 @@ def test_get_real_amount_fees_order( default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker ): tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[]) - mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value="BNB/USDT") + mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value=["BNB/USDT"]) mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200}) trade = Trade( pair="LTC/USDT", diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8fe59b5ea..225c5e364 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -586,7 +586,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): fetch_positions=MagicMock(return_value=mock_pos), get_tickers=tickers, get_valid_pair_combination=MagicMock( - side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}" + side_effect=lambda a, b: [f"{b}/{a}" if a == "USDT" else f"{a}/{b}"] ), ) default_conf_usdt["dry_run"] = False diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ee28bcc6c..221a8b666 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -556,7 +556,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot.config["dry_run"] = False mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance) mocker.patch(f"{EXMS}.get_tickers", tickers) - mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bc7746aef..845bce37c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -960,7 +960,7 @@ async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance default_conf["dry_run"] = False mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance) mocker.patch(f"{EXMS}.get_tickers", tickers) - mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -1049,7 +1049,7 @@ async def test_telegram_balance_handle_futures( mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance) mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos) mocker.patch(f"{EXMS}.get_tickers", tickers) - mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot)