Merge pull request #12531 from stash86/bybit-delist
add delisting check for bybit futures
This commit is contained in:
@@ -4,12 +4,13 @@ from datetime import datetime, timedelta
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
from freqtrade.util import dt_from_ts, dt_ts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -54,6 +55,7 @@ class Bybit(Exchange):
|
|||||||
"exchange_has_overrides": {
|
"exchange_has_overrides": {
|
||||||
"fetchOrder": True,
|
"fetchOrder": True,
|
||||||
},
|
},
|
||||||
|
"has_delisting": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
@@ -294,3 +296,35 @@ class Bybit(Exchange):
|
|||||||
|
|
||||||
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
|
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
|
||||||
return tiers
|
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"] in OPTIMIZE_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 = dt_ts() + (
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
from copy import deepcopy
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums import MarginMode, RunMode, TradingMode
|
||||||
from freqtrade.enums.tradingmode import TradingMode
|
from freqtrade.util import dt_utc
|
||||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
@@ -214,3 +215,43 @@ def test_bybit__order_needs_price(
|
|||||||
exchange.unified_account = uta
|
exchange.unified_account = uta
|
||||||
|
|
||||||
assert exchange._order_needs_price(side, order_type) == expected
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user