diff --git a/freqtrade/exchange/krakenfutures.py b/freqtrade/exchange/krakenfutures.py index 441cba75d..703fc9725 100644 --- a/freqtrade/exchange/krakenfutures.py +++ b/freqtrade/exchange/krakenfutures.py @@ -206,6 +206,62 @@ class Krakenfutures(Exchange): logger.warning(f"Could not update funding fees for {pair}.") return 0.0 + def get_leverage_tiers(self) -> dict[str, list[dict]]: + """ + Kraken Futures returns leverage tiers with contract-based thresholds. + CCXT maps tiers to min/maxNotional using "numNonContractUnits", but many markets + only expose "contracts". Fill missing min/maxNotional from contract data and + maxPositionSize to keep get_max_leverage functional. + """ + tiers = super().get_leverage_tiers() + if not tiers: + return tiers + + for pair, pair_tiers in tiers.items(): + self._fill_leverage_tier_notionals(pair, pair_tiers) + + return tiers + + def _fill_leverage_tier_notionals(self, pair: str, pair_tiers: list[dict]) -> None: + if not pair_tiers: + return + self._fill_missing_min_notional(pair_tiers) + self._fill_missing_max_notional(pair, pair_tiers) + + def _fill_missing_min_notional(self, pair_tiers: list[dict]) -> None: + for tier in pair_tiers: + if tier.get("minNotional") is None: + info = tier.get("info") or {} + contracts = self._safe_float(info.get("contracts")) + if contracts is not None: + tier["minNotional"] = contracts + + def _fill_missing_max_notional(self, pair: str, pair_tiers: list[dict]) -> None: + for i in range(len(pair_tiers) - 1): + if pair_tiers[i].get("maxNotional") is None: + next_min = pair_tiers[i + 1].get("minNotional") + if next_min is not None: + pair_tiers[i]["maxNotional"] = next_min + + last = pair_tiers[-1] + if last.get("maxNotional") is None: + max_notional = self._max_notional_from_market(pair) + if max_notional is not None: + last["maxNotional"] = max_notional + elif last.get("minNotional") is not None: + # Avoid None values, even if we cannot infer the real max. + last["maxNotional"] = last["minNotional"] + + def _max_notional_from_market(self, pair: str) -> float | None: + market = self.markets.get(pair) + if not market: + return None + max_pos = self._safe_float(market.get("info", {}).get("maxPositionSize")) + if max_pos is None: + return None + contract_size = self._safe_float(market.get("contractSize")) or 1.0 + return max_pos * contract_size + @staticmethod def _safe_float(v: Any) -> float | None: try: diff --git a/tests/exchange/test_krakenfutures.py b/tests/exchange/test_krakenfutures.py index b1f5efee1..d77f21285 100644 --- a/tests/exchange/test_krakenfutures.py +++ b/tests/exchange/test_krakenfutures.py @@ -24,6 +24,47 @@ def test_krakenfutures_ft_has_overrides(): assert ft_has["stop_price_type_field"] == "triggerSignal" +def test_krakenfutures_get_leverage_tiers_fills_contracts(mocker, default_conf): + """Fill missing min/maxNotional from contracts/maxPositionSize in leverage tiers.""" + mock_markets = { + "BTC/USD:USD": { + "info": {"maxPositionSize": 1000000}, + "contractSize": 1.0, + } + } + ex = get_patched_exchange( + mocker, default_conf, exchange="krakenfutures", mock_markets=mock_markets + ) + assert isinstance(ex, Krakenfutures) + + sample_tiers = { + "BTC/USD:USD": [ + { + "minNotional": None, + "maxNotional": None, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": {"contracts": 0}, + }, + { + "minNotional": None, + "maxNotional": None, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": {"contracts": 500000}, + }, + ] + } + + mocker.patch.object(Exchange, "get_leverage_tiers", return_value=sample_tiers) + tiers = ex.get_leverage_tiers() + pair_tiers = tiers["BTC/USD:USD"] + assert pair_tiers[0]["minNotional"] == 0.0 + assert pair_tiers[0]["maxNotional"] == 500000.0 + assert pair_tiers[1]["minNotional"] == 500000.0 + assert pair_tiers[1]["maxNotional"] == 1000000.0 + + def test_krakenfutures_ohlcv_candle_limit_uses_ccxt_limit(mocker, default_conf): """Test that OHLCV candle limit follows CCXT feature limit.""" ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures") diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 6ca4ffab4..ea4d5f0d7 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -562,7 +562,7 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = { "candle_count": 2000, "futures_pair": "BTC/USD:USD", "hasQuoteVolumeFutures": False, - "leverage_tiers_public": False, + "leverage_tiers_public": True, "leverage_in_spot_market": False, }, }