diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 04967b532..86da18017 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1899,22 +1899,22 @@ class Exchange: # When exchanges can load all their leverage tiers at once in the constructor # then this method does nothing, it should only be implemented when the leverage # tiers requires per symbol fetching to avoid excess api calls - if ( - self._api.has['fetchLeverageTiers'] and - not self._ft_has['can_fetch_multiple_tiers'] and - self.trading_mode == TradingMode.FUTURES - ): + if pair not in self._leverage_tiers: self._leverage_tiers[pair] = [] - try: - tiers = self._api.fetch_leverage_tiers(pair) - for tier in tiers[pair]: - self._leverage_tiers[pair].append(self.parse_leverage_tier(tier)) + if ( + self._api.has['fetchLeverageTiers'] and + not self._ft_has['can_fetch_multiple_tiers'] and + self.trading_mode == TradingMode.FUTURES + ): + try: + tiers = self._api.fetch_leverage_tiers(pair) + for tier in tiers[pair]: + self._leverage_tiers[pair].append(self.parse_leverage_tier(tier)) - return tiers - except ccxt.BadRequest: - return [] - else: - return [] + except ccxt.BadRequest: + return [] + + return self._leverage_tiers[pair] def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float: """ @@ -1934,10 +1934,7 @@ class Exchange: f'{self.name}.get_max_leverage requires argument stake_amount' ) - if pair not in self._leverage_tiers: - self.get_leverage_tiers_for_pair(pair) - - pair_tiers = self._leverage_tiers[pair] + pair_tiers = self.get_leverage_tiers_for_pair(pair) num_tiers = len(pair_tiers) if num_tiers < 1: return 1.0 @@ -2282,23 +2279,30 @@ class Exchange: """ if self._api.has['fetchLeverageTiers']: - if pair not in self._leverage_tiers: - # Used when fetchLeverageTiers cannot fetch all symbols at once - tiers = self.get_leverage_tiers_for_pair(pair) - if not bool(tiers): - raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}") - pair_tiers = self._leverage_tiers[pair] + + pair_tiers = self.get_leverage_tiers_for_pair(pair) + + if len(pair_tiers) < 1: + raise InvalidOrderException( + f"Maintenance margin rate for {pair} is unavailable for {self.name}" + ) + for tier in reversed(pair_tiers): if nominal_value >= tier['min']: return (tier['mmr'], tier['maintAmt']) + raise OperationalException("nominal value can not be lower than 0") # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it # describes the min amt for a tier, and the lowest tier will always go down to 0 else: + if pair not in self.markets: + raise InvalidOrderException( + f"{pair} is not tradeable on {self.name} {self.trading_mode.value}" + ) mmr = self.markets[pair]['maintenanceMarginRate'] if mmr is None: - raise OperationalException( - f"Maintenance margin rate is unavailable for {self.name}" + raise InvalidOrderException( + f"Maintenance margin rate for {pair} is unavailable for {self.name}" ) return (mmr, None) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index a5116920c..cfe3cde89 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -403,7 +403,6 @@ def test_fill_leverage_tiers_binance(default_conf, mocker): } } ], - }) default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d692baceb..d1fd357a8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4300,15 +4300,63 @@ def test_parse_leverage_tier(mocker, default_conf): } -def test_get_leverage_tiers_for_pair(mocker, default_conf, leverage_tiers): +def test_get_leverage_tiers_for_pair( + mocker, + default_conf, + leverage_tiers, +): api_mock = MagicMock() - api_mock.fetch_leverage_tiers = MagicMock() + api_mock.fetch_leverage_tiers = MagicMock(return_value={ + 'DOGE/USDT:USDT': [ + { + 'tier': 1, + 'notionalFloor': 0, + 'notionalCap': 500, + 'maintenanceMarginRatio': 0.02, + 'maxLeverage': 75, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.013', + 'instId': '', + 'maxLever': '75', + 'maxSz': '500', + 'minSz': '0', + 'mmr': '0.01', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '1', + 'uly': 'DOGE-USDT' + } + }, + { + 'tier': 2, + 'notionalFloor': 501, + 'notionalCap': 1000, + 'maintenanceMarginRatio': 0.025, + 'maxLeverage': 50, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.02', + 'instId': '', + 'maxLever': '50', + 'maxSz': '1000', + 'minSz': '501', + 'mmr': '0.015', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '2', + 'uly': 'DOGE-USDT' + } + } + ], + }) + # Spot type(api_mock)._ft_has = PropertyMock(return_value={'fetchLeverageTiers': True}) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._ft_has['can_fetch_multiple_tiers'] = False - assert exchange.get_leverage_tiers_for_pair('ADA/USDT') == [] + assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == [] # 'can_fetch_multiple_tiers': True default_conf['trading_mode'] = 'futures' @@ -4316,20 +4364,36 @@ def test_get_leverage_tiers_for_pair(mocker, default_conf, leverage_tiers): type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._ft_has['can_fetch_multiple_tiers'] = True - assert exchange.get_leverage_tiers_for_pair('ADA/USDT:USDT') == [] + assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == [] # 'fetchLeverageTiers': False type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False}) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._ft_has['can_fetch_multiple_tiers'] = False - assert exchange.get_leverage_tiers_for_pair('ADA/USDT:USDT') == [] + assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == [] - # 'fetchLeverageTiers': False + # 'fetchLeverageTiers': True type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._ft_has['can_fetch_multiple_tiers'] = False - assert exchange.get_leverage_tiers_for_pair('ADA/USDT:USDT') != [] + assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == [ + { + 'min': 0, + 'max': 500, + 'mmr': 0.02, + 'lev': 75, + 'maintAmt': None + }, + { + 'min': 501, + 'max': 1000, + 'mmr': 0.025, + 'lev': 50, + 'maintAmt': None + } + ] + # exception_handlers type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, api_mock) @@ -4341,7 +4405,7 @@ def test_get_leverage_tiers_for_pair(mocker, default_conf, leverage_tiers): "binance", "get_leverage_tiers_for_pair", "fetch_leverage_tiers", - pair='ETH/USDT:USDT', + pair='DOGE/USDT:USDT', ) @@ -4350,26 +4414,25 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) - pair = '1000SHIB/USDT' - - exchange._leverage_tiers = {} - exchange.get_leverage_tiers_for_pair = MagicMock(return_value=[]) - - with pytest.raises( - InvalidOrderException, - match=f"Cannot calculate liquidation price for {pair}", - ): - exchange.get_maintenance_ratio_and_amt(pair, 10000) exchange._leverage_tiers = leverage_tiers with pytest.raises( OperationalException, match='nominal value can not be lower than 0', ): - exchange.get_maintenance_ratio_and_amt(pair, -1) + exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', -1) + + exchange._leverage_tiers = {} + exchange.get_leverage_tiers_for_pair = MagicMock(return_value=[]) + + with pytest.raises( + InvalidOrderException, + match="Maintenance margin rate for 1000SHIB/USDT is unavailable for", + ): + exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', 10000) -@pytest.mark.parametrize('pair,value,mmr,maintAmt', [ +@ pytest.mark.parametrize('pair,value,mmr,maintAmt', [ ('ADA/BUSD', 500, 0.025, 0.0), ('ADA/BUSD', 20000000, 0.5, 1527500.0), ('ZEC/USDT', 500, 0.01, 0.0), diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index d8c9d12b6..dc6089788 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import InvalidOrderException, OperationalException from freqtrade.exchange import Gateio from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange @@ -31,12 +31,10 @@ def test_validate_order_types_gateio(default_conf, mocker): ExchangeResolver.load_exchange('gateio', default_conf, True) -@pytest.mark.parametrize('pair,mm_ratio', [ - ("ETH/USDT:USDT", 0.005), - ("ADA/USDT:USDT", 0.003), -]) -def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): +def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker): api_mock = MagicMock() + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") mocker.patch( @@ -59,7 +57,29 @@ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_rat 'id': 'ADA_USDT', 'symbol': 'ADA/USDT:USDT', }, + 'DOGE/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'maintenanceMarginRate': None, + 'info': {}, + 'id': 'ADA_USDT', + 'symbol': 'ADA/USDT:USDT', + }, } ) ) - assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None) + + assert exchange.get_maintenance_ratio_and_amt("ETH/USDT:USDT") == (0.005, None) + assert exchange.get_maintenance_ratio_and_amt("ADA/USDT:USDT") == (0.003, None) + + with pytest.raises( + InvalidOrderException, + match="Maintenance margin rate for DOGE/USDT:USDT is unavailable for Gateio", + ): + exchange.get_maintenance_ratio_and_amt('DOGE/USDT:USDT') + + with pytest.raises( + InvalidOrderException, + match="SHIB/USDT:USDT is not tradeable on Gateio futures", + ): + exchange.get_maintenance_ratio_and_amt('SHIB/USDT:USDT')