Skip to content

Commit 3f782fc

Browse files
authored
Merge pull request freqtrade#12531 from stash86/bybit-delist
add delisting check for bybit futures
2 parents 50402c5 + 92fd941 commit 3f782fc

File tree

2 files changed

+79
-4
lines changed

2 files changed

+79
-4
lines changed

freqtrade/exchange/bybit.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
import ccxt
55

66
from freqtrade.constants import BuySell
7-
from freqtrade.enums import MarginMode, PriceType, TradingMode
7+
from freqtrade.enums import OPTIMIZE_MODES, MarginMode, PriceType, TradingMode
88
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
99
from freqtrade.exchange import Exchange
1010
from freqtrade.exchange.common import retrier
1111
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
1212
from freqtrade.misc import deep_merge_dicts
13+
from freqtrade.util import dt_from_ts, dt_ts
1314

1415

1516
logger = logging.getLogger(__name__)
@@ -54,6 +55,7 @@ class Bybit(Exchange):
5455
"exchange_has_overrides": {
5556
"fetchOrder": True,
5657
},
58+
"has_delisting": True,
5759
}
5860

5961
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
@@ -294,3 +296,35 @@ def get_leverage_tiers(self) -> dict[str, list[dict]]:
294296

295297
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
296298
return tiers
299+
300+
def check_delisting_time(self, pair: str) -> datetime | None:
301+
"""
302+
Check if the pair gonna be delisted.
303+
By default, it returns None.
304+
:param pair: Market symbol
305+
:return: Datetime if the pair gonna be delisted, None otherwise
306+
"""
307+
if self._config["runmode"] in OPTIMIZE_MODES:
308+
return None
309+
310+
if self.trading_mode == TradingMode.FUTURES:
311+
return self._check_delisting_futures(pair)
312+
return None
313+
314+
def _check_delisting_futures(self, pair: str) -> datetime | None:
315+
delivery_time = self.markets.get(pair, {}).get("info", {}).get("deliveryTime", 0)
316+
if delivery_time:
317+
if isinstance(delivery_time, str) and (delivery_time != ""):
318+
delivery_time = int(delivery_time)
319+
320+
if not isinstance(delivery_time, int) or delivery_time <= 0:
321+
return None
322+
323+
max_delivery = dt_ts() + (
324+
14 * 24 * 60 * 60 * 1000
325+
) # Assume exchange don't announce delisting more than 14 days in advance
326+
327+
if delivery_time < max_delivery:
328+
return dt_from_ts(delivery_time)
329+
330+
return None

tests/exchange/test_bybit.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
from copy import deepcopy
12
from datetime import UTC, datetime, timedelta
2-
from unittest.mock import MagicMock
3+
from unittest.mock import MagicMock, PropertyMock
34

45
import pytest
56

6-
from freqtrade.enums.marginmode import MarginMode
7-
from freqtrade.enums.tradingmode import TradingMode
7+
from freqtrade.enums import MarginMode, RunMode, TradingMode
8+
from freqtrade.util import dt_utc
89
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
910
from tests.exchange.test_exchange import ccxt_exceptionhandlers
1011

@@ -214,3 +215,43 @@ def test_bybit__order_needs_price(
214215
exchange.unified_account = uta
215216

216217
assert exchange._order_needs_price(side, order_type) == expected
218+
219+
220+
def test_check_delisting_time_bybit(default_conf_usdt, mocker):
221+
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
222+
exchange._config["runmode"] = RunMode.BACKTEST
223+
delist_fut_mock = MagicMock(return_value=None)
224+
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
225+
226+
# Invalid run mode
227+
resp = exchange.check_delisting_time("BTC/USDT:USDT")
228+
assert resp is None
229+
assert delist_fut_mock.call_count == 0
230+
231+
# Delist spot called
232+
exchange._config["runmode"] = RunMode.DRY_RUN
233+
resp1 = exchange.check_delisting_time("BTC/USDT")
234+
assert resp1 is None
235+
assert delist_fut_mock.call_count == 0
236+
237+
# Delist futures called
238+
exchange.trading_mode = TradingMode.FUTURES
239+
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
240+
assert resp1 is None
241+
assert delist_fut_mock.call_count == 1
242+
243+
244+
def test__check_delisting_futures_bybit(default_conf_usdt, mocker, markets):
245+
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
246+
markets["BTC/USDT:USDT"]["info"]["deliveryTime"] = "0"
247+
markets["SOL/BUSD:BUSD"]["info"]["deliveryTime"] = "0"
248+
markets["ADA/USDT:USDT"]["info"]["deliveryTime"] = "1760745600000" # 2025-10-18
249+
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
250+
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
251+
252+
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
253+
# SOL has no delisting date
254+
assert resp_sol is None
255+
# Actually has a delisting date
256+
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
257+
assert resp_ada == dt_utc(2025, 10, 18)

0 commit comments

Comments
 (0)