From b5be462dd169d2adb9767971ebf47e8dea73d3f4 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 19 Nov 2025 12:25:35 +0900 Subject: [PATCH 1/5] add delisting check for bybit futures --- freqtrade/exchange/bybit.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 7db300acc..a5cba4997 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,15 +1,17 @@ import logging +import time from datetime import datetime, timedelta import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, PriceType, TradingMode +from freqtrade.enums import TRADE_MODES, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier from freqtrade.exchange.exchange_types import CcxtOrder, FtHas from freqtrade.misc import deep_merge_dicts +from freqtrade.util.datetime_helpers import dt_from_ts logger = logging.getLogger(__name__) @@ -54,6 +56,7 @@ class Bybit(Exchange): "exchange_has_overrides": { "fetchOrder": True, }, + "has_delisting": True, } _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ @@ -294,3 +297,35 @@ class Bybit(Exchange): self.cache_leverage_tiers(tiers, self._config["stake_currency"]) return tiers + + 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("deliveryTime", 0) + if delivery_time: + 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 e17936c407d77e480f24ec1d85bacb9a3b5fa849 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 19 Nov 2025 14:55:31 +0900 Subject: [PATCH 2/5] use dt_ts --- freqtrade/exchange/bybit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index a5cba4997..76daaed47 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,5 +1,4 @@ import logging -import time from datetime import datetime, timedelta import ccxt @@ -11,7 +10,7 @@ from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier from freqtrade.exchange.exchange_types import CcxtOrder, FtHas from freqtrade.misc import deep_merge_dicts -from freqtrade.util.datetime_helpers import dt_from_ts +from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts logger = logging.getLogger(__name__) @@ -321,7 +320,7 @@ class Bybit(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 7e178cb0320d429a3564fcbbc3db6752245c4520 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 20 Nov 2025 09:26:16 +0900 Subject: [PATCH 3/5] add test --- tests/exchange/test_bybit.py | 47 +++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index 511d8bab4..d5d169b3d 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -1,10 +1,11 @@ +from copy import deepcopy from datetime import UTC, datetime, timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.enums.marginmode import MarginMode -from freqtrade.enums.tradingmode import TradingMode +from freqtrade.enums import MarginMode, RunMode, TradingMode +from freqtrade.util.datetime_helpers import dt_utc from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -214,3 +215,43 @@ def test_bybit__order_needs_price( exchange.unified_account = uta assert exchange._order_needs_price(side, order_type) == expected + + +def test_check_delisting_time_bybit(default_conf_usdt, mocker): + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit") + 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: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_bybit(default_conf_usdt, mocker, markets): + markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"]) + markets["BTC/USDT:USDT"]["info"]["deliveryTime"] = "0" + markets["SOL/BUSD:BUSD"]["info"]["deliveryTime"] = "0" + markets["ADA/USDT:USDT"]["info"]["deliveryTime"] = "1760745600000" # 2025-10-18 + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit") + mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets)) + + resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD") + # SOL has no delisting date + assert resp_sol is None + # Actually has a delisting date + resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT") + assert resp_ada == dt_utc(2025, 10, 18) From 650cdf5eb34798329dfe55a04702efd3f12a6ffb Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 20 Nov 2025 15:16:11 +0900 Subject: [PATCH 4/5] change to exclude optimize mode --- freqtrade/exchange/bybit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 76daaed47..75e33fdfa 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import TRADE_MODES, MarginMode, PriceType, TradingMode +from freqtrade.enums import OPTIMIZE_MODES, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -304,7 +304,7 @@ class Bybit(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 92fd9411e51e26f013a8ba7144bf7aa499a5a891 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Nov 2025 19:31:28 +0100 Subject: [PATCH 5/5] chore: simplify import --- freqtrade/exchange/bybit.py | 2 +- tests/exchange/test_bybit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 75e33fdfa..300344e19 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -10,7 +10,7 @@ from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier from freqtrade.exchange.exchange_types import CcxtOrder, FtHas from freqtrade.misc import deep_merge_dicts -from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts +from freqtrade.util import dt_from_ts, dt_ts logger = logging.getLogger(__name__) diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index d5d169b3d..fa5f29dfe 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest from freqtrade.enums import MarginMode, RunMode, TradingMode -from freqtrade.util.datetime_helpers import dt_utc +from freqtrade.util import dt_utc from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers