Merge pull request #11655 from freqtrade/fix/11650

Fix max-stake-amount with high leverage
This commit is contained in:
Matthias
2025-04-19 14:19:39 +02:00
committed by GitHub
3 changed files with 59 additions and 43 deletions

View File

@@ -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,

View File

@@ -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(

View File

@@ -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