diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b78d98b99..9072d5948 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -961,7 +961,7 @@ class Exchange: return 1 / pow(10, precision) def get_min_pair_stake_amount( - self, pair: str, price: float, stoploss: float, leverage: float | None = 1.0 + self, pair: str, price: float, stoploss: float, leverage: float = 1.0 ) -> float | None: return self._get_stake_amount_limit(pair, price, stoploss, "min", leverage) @@ -980,7 +980,7 @@ class Exchange: price: float, stoploss: float, limit: Literal["min", "max"], - leverage: float | None = 1.0, + leverage: float = 1.0, ) -> float | None: isMin = limit == "min" @@ -989,6 +989,8 @@ class Exchange: except KeyError: raise ValueError(f"Can't get market information for symbol {pair}") + stake_limits = [] + limits = market["limits"] if isMin: # reserve some percent defined in config (5% default) + stoploss margin_reserve: float = 1.0 + self._config.get( @@ -998,11 +1000,12 @@ class Exchange: # it should not be more than 50% stoploss_reserve = max(min(stoploss_reserve, 1.5), 1) else: + # is_max margin_reserve = 1.0 stoploss_reserve = 1.0 + if max_from_tiers := self._get_max_notional_from_tiers(pair, leverage=leverage): + stake_limits.append(max_from_tiers) - stake_limits = [] - limits = market["limits"] if limits["cost"][limit] is not None: stake_limits.append( self._contracts_to_amount(pair, limits["cost"][limit]) * stoploss_reserve @@ -3361,42 +3364,22 @@ class Exchange: pair_tiers = self._leverage_tiers[pair] if stake_amount == 0: - return self._leverage_tiers[pair][0]["maxLeverage"] # Max lev for lowest amount + return pair_tiers[0]["maxLeverage"] # Max lev for lowest amount - for tier_index in range(len(pair_tiers)): - tier = pair_tiers[tier_index] - lev = tier["maxLeverage"] + # Find the appropriate tier based on stake_amount + prior_max_lev = None + for tier in pair_tiers: + min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"]) + max_stake = tier["maxNotional"] / tier["maxLeverage"] + prior_max_lev = tier["maxLeverage"] + # Adjust notional by leverage to do a proper comparison + if min_stake <= stake_amount <= max_stake: + return tier["maxLeverage"] - if tier_index < len(pair_tiers) - 1: - next_tier = pair_tiers[tier_index + 1] - next_floor = next_tier["minNotional"] / next_tier["maxLeverage"] - if next_floor > stake_amount: # Next tier min too high for stake amount - return min((tier["maxNotional"] / stake_amount), lev) - # - # With the two leverage tiers below, - # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66 - # - stakes below 133.33 = max_lev of 75 - # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99 - # - stakes from 200 + 1000 = max_lev of 50 - # - # { - # "min": 0, # stake = 0.0 - # "max": 10000, # max_stake@75 = 10000/75 = 133.33333333333334 - # "lev": 75, - # }, - # { - # "min": 10000, # stake = 200.0 - # "max": 50000, # max_stake@50 = 50000/50 = 1000.0 - # "lev": 50, - # } - # - - else: # if on the last tier - if stake_amount > tier["maxNotional"]: - # If stake is > than max tradeable amount - raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}") - else: - return tier["maxLeverage"] + # else: # if on the last tier + if stake_amount > max_stake: + # If stake is > than max tradeable amount + raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}") raise OperationalException( "Looped through all tiers without finding a max leverage. Should never be reached" @@ -3411,6 +3394,23 @@ class Exchange: else: return 1.0 + def _get_max_notional_from_tiers(self, pair: str, leverage: float) -> float | None: + """ + get max_notional from leverage_tiers + :param pair: The base/quote currency pair being traded + :param leverage: The leverage to be used + :return: The maximum notional value for the given leverage or None if not found + """ + if self.trading_mode != TradingMode.FUTURES: + return None + if pair not in self._leverage_tiers: + return None + pair_tiers = self._leverage_tiers[pair] + for tier in reversed(pair_tiers): + if leverage <= tier["maxLeverage"]: + return tier["maxNotional"] + return None + @retrier def _set_leverage( self, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 63357dcd8..8ee65e2dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -760,12 +760,14 @@ class FreqtradeBot(LoggingMixin): current_exit_profit = trade.calc_profit_ratio(current_exit_rate) min_entry_stake = self.exchange.get_min_pair_stake_amount( - trade.pair, current_entry_rate, 0.0 + trade.pair, current_entry_rate, 0.0, trade.leverage ) min_exit_stake = self.exchange.get_min_pair_stake_amount( - trade.pair, current_exit_rate, self.strategy.stoploss + trade.pair, current_exit_rate, self.strategy.stoploss, trade.leverage + ) + max_entry_stake = self.exchange.get_max_pair_stake_amount( + trade.pair, current_entry_rate, trade.leverage ) - max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate) stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount, order_tag = self.strategy._adjust_trade_position_internal( diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c0023f0ad..f9f7dd9a1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5599,11 +5599,13 @@ def test_liquidation_price_is_none( def test_get_max_pair_stake_amount( mocker, default_conf, + leverage_tiers, ): api_mock = MagicMock() default_conf["margin_mode"] = "isolated" default_conf["trading_mode"] = "futures" exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange._leverage_tiers = leverage_tiers markets = { "XRP/USDT:USDT": { "limits": { @@ -5667,11 +5669,23 @@ def test_get_max_pair_stake_amount( "contractSize": 0.01, "spot": False, }, + "ZEC/USDT:USDT": { + "limits": { + "amount": {"min": 0.001, "max": None}, + "cost": {"min": 5, "max": None}, + }, + "contractSize": 1, + "spot": False, + }, } mocker.patch(f"{EXMS}.markets", markets) assert exchange.get_max_pair_stake_amount("XRP/USDT:USDT", 2.0) == 20000 assert exchange.get_max_pair_stake_amount("XRP/USDT:USDT", 2.0, 5) == 4000 + # limit leverage tiers + assert exchange.get_max_pair_stake_amount("ZEC/USDT:USDT", 2.0, 5) == 100_000 + assert exchange.get_max_pair_stake_amount("ZEC/USDT:USDT", 2.0, 50) == 1000 + assert exchange.get_max_pair_stake_amount("LTC/USDT:USDT", 2.0) == float("inf") assert exchange.get_max_pair_stake_amount("ETH/USDT:USDT", 2.0) == 200 assert exchange.get_max_pair_stake_amount("DOGE/USDT:USDT", 2.0) == 500 @@ -5902,8 +5916,8 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): assert exchange.get_max_leverage("XRP/USDT:USDT", 1.0) == 20.0 assert exchange.get_max_leverage("BNB/USDT:USDT", 100.0) == 75.0 assert exchange.get_max_leverage("BTC/USDT:USDT", 170.30) == 125.0 - assert pytest.approx(exchange.get_max_leverage("XRP/USDT:USDT", 99999.9)) == 5.000005 - assert pytest.approx(exchange.get_max_leverage("BNB/USDT:USDT", 1500)) == 33.333333333333333 + assert pytest.approx(exchange.get_max_leverage("XRP/USDT:USDT", 99999.9)) == 5 + assert pytest.approx(exchange.get_max_leverage("BNB/USDT:USDT", 1500)) == 25 assert exchange.get_max_leverage("BTC/USDT:USDT", 300000000) == 2.0 assert exchange.get_max_leverage("BTC/USDT:USDT", 600000000) == 1.0 # Last tier