From a731b73457973d3ece525459654cd1aa9bc39d58 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 19 Nov 2025 12:36:34 +0900 Subject: [PATCH 1/6] add delisting check for bitget futures --- freqtrade/exchange/bitget.py | 43 +++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index a1d6db255..bb1fc5fb4 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -1,10 +1,11 @@ import logging -from datetime import timedelta +import time +from datetime import datetime, timedelta import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.enums import TRADE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import ( DDosProtection, OperationalException, @@ -14,7 +15,7 @@ from freqtrade.exceptions import ( from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_RETRY_COUNT, retrier from freqtrade.exchange.exchange_types import CcxtOrder, FtHas -from freqtrade.util.datetime_helpers import dt_now, dt_ts +from freqtrade.util.datetime_helpers import dt_from_ts, dt_now, dt_ts logger = logging.getLogger(__name__) @@ -37,6 +38,7 @@ class Bitget(Exchange): _ft_has_futures: FtHas = { "mark_ohlcv_timeframe": "4h", "funding_fee_candle_limit": 100, + "has_delisting": True, } _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ @@ -236,3 +238,38 @@ class Bitget(Exchange): raise OperationalException( "Freqtrade currently only supports isolated futures for bitget" ) + + def check_delisting_time(self, pair: str) -> datetime | None: + """ + Check if the pair gonna be delisted. + By default, it returns None. + :param pair: Market symbol + :return: Datetime if the pair gonna be delisted, None otherwise + """ + if self._config["runmode"] not in TRADE_MODES: + return None + + if self.trading_mode == TradingMode.FUTURES: + return self._check_delisting_futures(pair) + return None + + def _check_delisting_futures(self, pair: str) -> datetime | None: + delivery_time = self.markets.get(pair, {}).get("info", {}).get("limitOpenTime", None) + if delivery_time: + if delivery_time == "-1": + return None + + if isinstance(delivery_time, str) and (delivery_time != ""): + delivery_time = int(delivery_time) + + if not isinstance(delivery_time, int) or delivery_time <= 0: + return None + + max_delivery = int(time.time() * 1000) + ( + 14 * 24 * 60 * 60 * 1000 + ) # Assume exchange don't announce delisting more than 14 days in advance + + if delivery_time < max_delivery: + return dt_from_ts(delivery_time) + + return None From f437d4a55b652b4af5501c763caa888892eb4733 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 19 Nov 2025 14:53:57 +0900 Subject: [PATCH 2/6] use dt_ts --- freqtrade/exchange/bitget.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index bb1fc5fb4..c7fd39f66 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -1,5 +1,4 @@ import logging -import time from datetime import datetime, timedelta import ccxt @@ -265,7 +264,7 @@ class Bitget(Exchange): if not isinstance(delivery_time, int) or delivery_time <= 0: return None - max_delivery = int(time.time() * 1000) + ( + max_delivery = dt_ts() + ( 14 * 24 * 60 * 60 * 1000 ) # Assume exchange don't announce delisting more than 14 days in advance From 4017b010f499ecab5757d38be77201902c9507fc Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 20 Nov 2025 09:36:17 +0900 Subject: [PATCH 3/6] add test --- tests/exchange/test_bitget.py | 45 +++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_bitget.py b/tests/exchange/test_bitget.py index ca32ba536..b4dafb0ae 100644 --- a/tests/exchange/test_bitget.py +++ b/tests/exchange/test_bitget.py @@ -1,12 +1,13 @@ +from copy import deepcopy from datetime import timedelta from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode from freqtrade.exceptions import OperationalException, RetryableOrderError from freqtrade.exchange.common import API_RETRY_COUNT -from freqtrade.util import dt_now, dt_ts +from freqtrade.util import dt_now, dt_ts, dt_utc from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -193,3 +194,43 @@ def test__lev_prep_bitget(default_conf, mocker): assert api_mock.set_margin_mode.call_count == 0 assert api_mock.set_leverage.call_count == 1 api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99) + + +def test_check_delisting_time_bitget(default_conf_usdt, mocker): + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget") + exchange._config["runmode"] = RunMode.BACKTEST + delist_fut_mock = MagicMock(return_value=None) + mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock) + + # Invalid run mode + resp = exchange.check_delisting_time("BTC/USDT") + assert resp is None + assert delist_fut_mock.call_count == 0 + + # Delist spot called + exchange._config["runmode"] = RunMode.DRY_RUN + resp1 = exchange.check_delisting_time("BTC/USDT") + assert resp1 is None + assert delist_fut_mock.call_count == 0 + + # Delist futures called + exchange.trading_mode = TradingMode.FUTURES + resp1 = exchange.check_delisting_time("BTC/USDT:USDT") + assert resp1 is None + assert delist_fut_mock.call_count == 1 + + +def test__check_delisting_futures_bitget(default_conf_usdt, mocker, markets): + markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"]) + markets["BTC/USDT:USDT"]["info"]["limitOpenTime"] = "-1" + markets["SOL/BUSD:BUSD"]["info"]["limitOpenTime"] = "-1" + markets["ADA/USDT:USDT"]["info"]["limitOpenTime"] = "1760745600000" # 2025-10-18 + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget") + mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets)) + + resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD") + # No delisting date + assert resp_sol is None + # Has a delisting date + resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT") + assert resp_ada == dt_utc(2025, 10, 18) From b8d558a4558e166787ede52994bc6bd4268cf601 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 20 Nov 2025 09:41:57 +0900 Subject: [PATCH 4/6] remove unnecessary check because it will be catch anyway --- freqtrade/exchange/bitget.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index c7fd39f66..d4087550e 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -255,9 +255,6 @@ class Bitget(Exchange): def _check_delisting_futures(self, pair: str) -> datetime | None: delivery_time = self.markets.get(pair, {}).get("info", {}).get("limitOpenTime", None) if delivery_time: - if delivery_time == "-1": - return None - if isinstance(delivery_time, str) and (delivery_time != ""): delivery_time = int(delivery_time) From 060a1543e9705888b30f38fbd904141a63cdb6c7 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 20 Nov 2025 15:13:56 +0900 Subject: [PATCH 5/6] change to exclude optimize mode --- freqtrade/exchange/bitget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index d4087550e..1e4b3c13c 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import TRADE_MODES, CandleType, MarginMode, TradingMode +from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import ( DDosProtection, OperationalException, @@ -245,7 +245,7 @@ class Bitget(Exchange): :param pair: Market symbol :return: Datetime if the pair gonna be delisted, None otherwise """ - if self._config["runmode"] not in TRADE_MODES: + if self._config["runmode"] in OPTIMIZE_MODES: return None if self.trading_mode == TradingMode.FUTURES: From d02e5f2b90b9f5d654c4ba90b5a733ede53863c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Nov 2025 19:34:51 +0100 Subject: [PATCH 6/6] chore: simplify imports --- freqtrade/exchange/bitget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index 1e4b3c13c..3351cda88 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -14,7 +14,7 @@ from freqtrade.exceptions import ( from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_RETRY_COUNT, retrier from freqtrade.exchange.exchange_types import CcxtOrder, FtHas -from freqtrade.util.datetime_helpers import dt_from_ts, dt_now, dt_ts +from freqtrade.util import dt_from_ts, dt_now, dt_ts logger = logging.getLogger(__name__)