From 4ef45314ddde0f4d5ab02ef1d56694d3f4a228c4 Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Fri, 22 Mar 2024 20:18:52 +0100 Subject: [PATCH 1/9] JW: adding first draft for deslist schedule --- freqtrade/exchange/binance.py | 41 ++++++++++++++++++++++++++++++++++ tests/exchange/test_binance.py | 8 +++++++ 2 files changed, 49 insertions(+) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 89b983d91..def3b91a8 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -217,3 +217,44 @@ class Binance(Exchange): raise OperationalException(e) from e else: return {} + + def get_spot_delist_schedule(self, refresh: bool) -> list: + """ + Calculates bid/ask target + bid rate - between current ask price and last price + ask rate - either using ticker bid or first bid based on orderbook + or remain static in any other case since it's not updating. + :param pair: Pair to get rate for + :param refresh: allow cached data + :param side: "buy" or "sell" + :return: float: Price + :raises PricingError if orderbook price could not be determined. + """ + if not refresh: + with self._cache_lock: + rate = cache_rate.get(pair) + # Check if cache has been invalidated + if rate: + logger.debug(f"Using cached {side} rate for {pair}.") + return rate + + + if conf_strategy.get('use_order_book', False): + + order_book_top = conf_strategy.get('order_book_top', 1) + if order_book is None: + order_book = self.fetch_l2_order_book(pair, order_book_top) + rate = self._get_rate_from_ob(pair, side, order_book, name, price_side, + order_book_top) + else: + logger.debug(f"Using Last {price_side.capitalize()} / Last Price") + if ticker is None: + ticker = self.fetch_ticker(pair) + rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side) + + if rate is None: + raise PricingError(f"{name}-Rate for {pair} was empty.") + with self._cache_lock: + cache_rate[pair] = rate + + return delistings \ No newline at end of file diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 625033645..ac87c0ca3 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -617,3 +617,11 @@ def test_get_maintenance_ratio_and_amt_binance( exchange._leverage_tiers = leverage_tiers (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) + + +def test_get_spot_delist_schedule(mocker, default_conf) -> None: + exchange = get_patched_exchange(mocker, default_conf, id='binance') + exchange._api.sapi_get_spot_delist_schedule = get_mock_coro([{'delistTime': '1712113200000', 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT']}]) + + + assert exchange.get_spot_delist_schedule(True) == ['DREP/BTC', 'DREP/USDT', 'MOB/BTC', 'MOB/USDT', 'PNT/USDT'] \ No newline at end of file From cbc98d384eb0268231366bf5ec139f2e4e410566 Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Fri, 22 Mar 2024 21:17:17 +0100 Subject: [PATCH 2/9] Updating delisting function to return the time when it will delist if it is delisting --- freqtrade/exchange/binance.py | 69 ++++++++++++++++++---------------- tests/exchange/test_binance.py | 4 +- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index def3b91a8..62b5e39da 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -5,6 +5,8 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple import ccxt +from cachetools import TTLCache +from threading import Lock from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError @@ -50,6 +52,11 @@ class Binance(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] + def __init__(self, *args, **kwargs) -> None: + super(Binance, self).__init__(*args, **kwargs) + self._spot_delist_schedule_cache: TTLCache = TTLCache(maxsize=100, ttl=300) + self._spot_delist_schedule_cache_lock = Lock() + def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: tickers = super().get_tickers(symbols=symbols, cached=cached) if self.trading_mode == TradingMode.FUTURES: @@ -218,43 +225,41 @@ class Binance(Exchange): else: return {} - def get_spot_delist_schedule(self, refresh: bool) -> list: + def get_spot_pair_delist_time(self, pair, refresh: bool) -> int | None: """ - Calculates bid/ask target - bid rate - between current ask price and last price - ask rate - either using ticker bid or first bid based on orderbook - or remain static in any other case since it's not updating. - :param pair: Pair to get rate for - :param refresh: allow cached data - :param side: "buy" or "sell" - :return: float: Price - :raises PricingError if orderbook price could not be determined. + Get the delisting time for a pair if it will be delisted + :param pair: Pair to get the delisting time for + :param refresh: true if you need fresh data + :return: int: delisting time None if not delisting """ + + cache = self._spot_delist_schedule_cache + lock = self._spot_delist_schedule_cache_lock + if not refresh: - with self._cache_lock: - rate = cache_rate.get(pair) - # Check if cache has been invalidated - if rate: - logger.debug(f"Using cached {side} rate for {pair}.") - return rate + with lock: + delist_time = cache.get(f"{pair}") + if delist_time: + return delist_time + try: + delist_schedule = self._api.sapi_get_spot_delist_schedule() - if conf_strategy.get('use_order_book', False): + if delist_schedule is None: + return - order_book_top = conf_strategy.get('order_book_top', 1) - if order_book is None: - order_book = self.fetch_l2_order_book(pair, order_book_top) - rate = self._get_rate_from_ob(pair, side, order_book, name, price_side, - order_book_top) - else: - logger.debug(f"Using Last {price_side.capitalize()} / Last Price") - if ticker is None: - ticker = self.fetch_ticker(pair) - rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side) + with lock: + for schedule in delist_schedule: + for pair in schedule['symbols']: + cache[f"{pair}"] = int(schedule['delistTime']) - if rate is None: - raise PricingError(f"{name}-Rate for {pair} was empty.") - with self._cache_lock: - cache_rate[pair] = rate + with lock: + return cache.get(f"{pair}") - return delistings \ No newline at end of file + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get delist schedule {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e \ No newline at end of file diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index ac87c0ca3..605dc9b43 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -621,7 +621,7 @@ def test_get_maintenance_ratio_and_amt_binance( def test_get_spot_delist_schedule(mocker, default_conf) -> None: exchange = get_patched_exchange(mocker, default_conf, id='binance') - exchange._api.sapi_get_spot_delist_schedule = get_mock_coro([{'delistTime': '1712113200000', 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT']}]) + exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=[{'delistTime': '1712113200000', 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT']}]) - assert exchange.get_spot_delist_schedule(True) == ['DREP/BTC', 'DREP/USDT', 'MOB/BTC', 'MOB/USDT', 'PNT/USDT'] \ No newline at end of file + assert exchange.get_spot_pair_delist_time('DREP/USDT', False) == 1712113200000 \ No newline at end of file From ff2eaeb3b49c49ac1c6e44fe7fed7df9a41ee9e1 Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Fri, 22 Mar 2024 21:29:33 +0100 Subject: [PATCH 3/9] making ruff happy --- freqtrade/exchange/binance.py | 23 ++++++++----- tests/exchange/test_binance.py | 63 ++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 62b5e39da..b02130d93 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -53,8 +53,9 @@ class Binance(Exchange): ] def __init__(self, *args, **kwargs) -> None: - super(Binance, self).__init__(*args, **kwargs) - self._spot_delist_schedule_cache: TTLCache = TTLCache(maxsize=100, ttl=300) + super(__class__, self).__init__(*args, **kwargs) + self._spot_delist_schedule_cache: TTLCache = TTLCache( + maxsize=100, ttl=300) self._spot_delist_schedule_cache_lock = Lock() def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: @@ -63,7 +64,8 @@ class Binance(Exchange): # Binance's future result has no bid/ask values. # Therefore we must fetch that from fetch_bids_asks and combine the two results. bidsasks = self.fetch_bids_asks(symbols, cached) - tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False) + tickers = deep_merge_dicts( + bidsasks, tickers, allow_null_overrides=False) return tickers @retrier @@ -76,9 +78,11 @@ class Binance(Exchange): try: if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: position_side = self._api.fapiPrivateGetPositionSideDual() - self._log_exchange_response('position_side_setting', position_side) + self._log_exchange_response( + 'position_side_setting', position_side) assets_margin = self._api.fapiPrivateGetMultiAssetsMargin() - self._log_exchange_response('multi_asset_margin', assets_margin) + self._log_exchange_response( + 'multi_asset_margin', assets_margin) msg = "" if position_side.get('dualSidePosition') is True: msg += ( @@ -94,7 +98,7 @@ class Binance(Exchange): except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' - ) from e + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -182,7 +186,8 @@ class Binance(Exchange): # mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100% # maintenance_amt: (CUM) Maintenance Amount of position - mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount) + mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt( + pair, stake_amount) if (maintenance_amt is None): raise OperationalException( @@ -242,7 +247,7 @@ class Binance(Exchange): if delist_time: return delist_time - try: + try: delist_schedule = self._api.sapi_get_spot_delist_schedule() if delist_schedule is None: @@ -262,4 +267,4 @@ class Binance(Exchange): raise TemporaryError( f'Could not get delist schedule {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) from e \ No newline at end of file + raise OperationalException(e) from e diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 605dc9b43..7214d157f 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -18,10 +18,11 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ('buy', 'limit', 'PO', {'timeInForce': 'PO'}), ('sell', 'limit', 'PO', {'timeInForce': 'PO'}), ('sell', 'market', 'PO', {}), - ]) +]) def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): exchange = get_patched_exchange(mocker, default_conf, id='binance') - assert exchange._get_params(side, type, 1, False, time_in_force) == expected + assert exchange._get_params( + side, type, 1, False, time_in_force) == expected @pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT]) @@ -58,7 +59,8 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte amount=1, stop_price=190, side=side, - order_types={'stoploss': 'limit', 'stoploss_on_exchange_limit_ratio': 1.05}, + order_types={'stoploss': 'limit', + 'stoploss_on_exchange_limit_ratio': 1.05}, leverage=1.0 ) @@ -88,13 +90,16 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte if trademode == TradingMode.SPOT: params_dict = {'stopPrice': 220} else: - params_dict = {'stopPrice': 220, 'reduceOnly': True, 'workingType': 'MARK_PRICE'} + params_dict = {'stopPrice': 220, + 'reduceOnly': True, 'workingType': 'MARK_PRICE'} assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + api_mock.create_order = MagicMock( + side_effect=ccxt.InsufficientFunds("0 balance")) + exchange = get_patched_exchange( + mocker, default_conf, api_mock, 'binance') exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -106,7 +111,8 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + exchange = get_patched_exchange( + mocker, default_conf, api_mock, 'binance') exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -383,7 +389,8 @@ def test_fill_leverage_tiers_binance(default_conf, mocker): default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange( + mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_tiers() assert exchange._leverage_tiers == { @@ -486,7 +493,8 @@ def test_fill_leverage_tiers_binance(default_conf, mocker): api_mock = MagicMock() api_mock.load_leverage_tiers = MagicMock() - type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) + type(api_mock).has = PropertyMock( + return_value={'fetchLeverageTiers': True}) ccxt_exceptionhandlers( mocker, @@ -502,7 +510,8 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers api_mock = MagicMock() default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange( + mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_tiers() assert len(exchange._leverage_tiers.keys()) > 100 for key, value in leverage_tiers.items(): @@ -514,17 +523,23 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers def test_additional_exchange_init_binance(default_conf, mocker): api_mock = MagicMock() - api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": True}) - api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": True}) + api_mock.fapiPrivateGetPositionSideDual = MagicMock( + return_value={"dualSidePosition": True}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock( + return_value={"multiAssetsMargin": True}) default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED with pytest.raises(OperationalException, match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*"): - get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) - api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": False}) - api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": False}) - exchange = get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + get_patched_exchange(mocker, default_conf, + id="binance", api_mock=api_mock) + api_mock.fapiPrivateGetPositionSideDual = MagicMock( + return_value={"dualSidePosition": False}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock( + return_value={"multiAssetsMargin": False}) + exchange = get_patched_exchange( + mocker, default_conf, id="binance", api_mock=api_mock) assert exchange ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'binance', "additional_exchange_init", "fapiPrivateGetPositionSideDual") @@ -539,7 +554,8 @@ def test__set_leverage_binance(mocker, default_conf): default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange( + mocker, default_conf, api_mock, id="binance") exchange._set_leverage(3.2, 'BTC/USDT:USDT') assert api_mock.set_leverage.call_count == 1 # Leverage is rounded to 3. @@ -592,7 +608,8 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c # Called twice - one "init" call - and one to get the actual data. assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv - assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + assert log_has_re( + r"Candle-data for ETH/BTC available starting with .*", caplog) @pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [ @@ -615,13 +632,15 @@ def test_get_maintenance_ratio_and_amt_binance( mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_tiers = leverage_tiers - (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) + (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt( + pair, nominal_value) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) def test_get_spot_delist_schedule(mocker, default_conf) -> None: exchange = get_patched_exchange(mocker, default_conf, id='binance') - exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=[{'delistTime': '1712113200000', 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT']}]) + exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=[ + {'delistTime': '1712113200000', 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT']}]) - - assert exchange.get_spot_pair_delist_time('DREP/USDT', False) == 1712113200000 \ No newline at end of file + assert exchange.get_spot_pair_delist_time( + 'DREP/USDT', False) == 1712113200000 From 4257e1ca9676713dd47aaf565ba71f3777d8429f Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Fri, 22 Mar 2024 21:33:38 +0100 Subject: [PATCH 4/9] JW: reverting pep8 changes --- freqtrade/exchange/binance.py | 17 +++------ tests/exchange/test_binance.py | 67 +++++++++++++--------------------- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b02130d93..2acc3dcf1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -54,8 +54,7 @@ class Binance(Exchange): def __init__(self, *args, **kwargs) -> None: super(__class__, self).__init__(*args, **kwargs) - self._spot_delist_schedule_cache: TTLCache = TTLCache( - maxsize=100, ttl=300) + self._spot_delist_schedule_cache: TTLCache = TTLCache(maxsize=100, ttl=300) self._spot_delist_schedule_cache_lock = Lock() def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: @@ -64,8 +63,7 @@ class Binance(Exchange): # Binance's future result has no bid/ask values. # Therefore we must fetch that from fetch_bids_asks and combine the two results. bidsasks = self.fetch_bids_asks(symbols, cached) - tickers = deep_merge_dicts( - bidsasks, tickers, allow_null_overrides=False) + tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False) return tickers @retrier @@ -78,11 +76,9 @@ class Binance(Exchange): try: if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: position_side = self._api.fapiPrivateGetPositionSideDual() - self._log_exchange_response( - 'position_side_setting', position_side) + self._log_exchange_response('position_side_setting', position_side) assets_margin = self._api.fapiPrivateGetMultiAssetsMargin() - self._log_exchange_response( - 'multi_asset_margin', assets_margin) + self._log_exchange_response('multi_asset_margin', assets_margin) msg = "" if position_side.get('dualSidePosition') is True: msg += ( @@ -98,7 +94,7 @@ class Binance(Exchange): except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' - ) from e + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -186,8 +182,7 @@ class Binance(Exchange): # mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100% # maintenance_amt: (CUM) Maintenance Amount of position - mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt( - pair, stake_amount) + mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount) if (maintenance_amt is None): raise OperationalException( diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 7214d157f..1669c5050 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -18,11 +18,10 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ('buy', 'limit', 'PO', {'timeInForce': 'PO'}), ('sell', 'limit', 'PO', {'timeInForce': 'PO'}), ('sell', 'market', 'PO', {}), -]) + ]) def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): exchange = get_patched_exchange(mocker, default_conf, id='binance') - assert exchange._get_params( - side, type, 1, False, time_in_force) == expected + assert exchange._get_params(side, type, 1, False, time_in_force) == expected @pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT]) @@ -59,8 +58,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte amount=1, stop_price=190, side=side, - order_types={'stoploss': 'limit', - 'stoploss_on_exchange_limit_ratio': 1.05}, + order_types={'stoploss': 'limit', 'stoploss_on_exchange_limit_ratio': 1.05}, leverage=1.0 ) @@ -90,16 +88,13 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte if trademode == TradingMode.SPOT: params_dict = {'stopPrice': 220} else: - params_dict = {'stopPrice': 220, - 'reduceOnly': True, 'workingType': 'MARK_PRICE'} + params_dict = {'stopPrice': 220, 'reduceOnly': True, 'workingType': 'MARK_PRICE'} assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock( - side_effect=ccxt.InsufficientFunds("0 balance")) - exchange = get_patched_exchange( - mocker, default_conf, api_mock, 'binance') + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -111,8 +106,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) - exchange = get_patched_exchange( - mocker, default_conf, api_mock, 'binance') + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -389,8 +383,7 @@ def test_fill_leverage_tiers_binance(default_conf, mocker): default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - exchange = get_patched_exchange( - mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_tiers() assert exchange._leverage_tiers == { @@ -493,8 +486,7 @@ def test_fill_leverage_tiers_binance(default_conf, mocker): api_mock = MagicMock() api_mock.load_leverage_tiers = MagicMock() - type(api_mock).has = PropertyMock( - return_value={'fetchLeverageTiers': True}) + type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) ccxt_exceptionhandlers( mocker, @@ -510,8 +502,7 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers api_mock = MagicMock() default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - exchange = get_patched_exchange( - mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_tiers() assert len(exchange._leverage_tiers.keys()) > 100 for key, value in leverage_tiers.items(): @@ -523,23 +514,17 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers def test_additional_exchange_init_binance(default_conf, mocker): api_mock = MagicMock() - api_mock.fapiPrivateGetPositionSideDual = MagicMock( - return_value={"dualSidePosition": True}) - api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock( - return_value={"multiAssetsMargin": True}) + api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": True}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": True}) default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED with pytest.raises(OperationalException, match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*"): - get_patched_exchange(mocker, default_conf, - id="binance", api_mock=api_mock) - api_mock.fapiPrivateGetPositionSideDual = MagicMock( - return_value={"dualSidePosition": False}) - api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock( - return_value={"multiAssetsMargin": False}) - exchange = get_patched_exchange( - mocker, default_conf, id="binance", api_mock=api_mock) + get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": False}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": False}) + exchange = get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) assert exchange ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'binance', "additional_exchange_init", "fapiPrivateGetPositionSideDual") @@ -554,8 +539,7 @@ def test__set_leverage_binance(mocker, default_conf): default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - exchange = get_patched_exchange( - mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange._set_leverage(3.2, 'BTC/USDT:USDT') assert api_mock.set_leverage.call_count == 1 # Leverage is rounded to 3. @@ -608,8 +592,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c # Called twice - one "init" call - and one to get the actual data. assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv - assert log_has_re( - r"Candle-data for ETH/BTC available starting with .*", caplog) + assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) @pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [ @@ -632,15 +615,17 @@ def test_get_maintenance_ratio_and_amt_binance( mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_tiers = leverage_tiers - (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt( - pair, nominal_value) + (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) def test_get_spot_delist_schedule(mocker, default_conf) -> None: exchange = get_patched_exchange(mocker, default_conf, id='binance') - exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=[ - {'delistTime': '1712113200000', 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT']}]) + return_value = [{ + 'delistTime': '1712113200000', + 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT'] + }] - assert exchange.get_spot_pair_delist_time( - 'DREP/USDT', False) == 1712113200000 + exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=return_value) + + assert exchange.get_spot_pair_delist_time('DREP/USDT', False) == 1712113200000 From 2b0b85330e06b9b7f7fe65d7981afbc5e801e0cb Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Fri, 22 Mar 2024 21:40:36 +0100 Subject: [PATCH 5/9] works also without __class__ --- freqtrade/exchange/binance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 2acc3dcf1..3076d9602 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -53,7 +53,7 @@ class Binance(Exchange): ] def __init__(self, *args, **kwargs) -> None: - super(__class__, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._spot_delist_schedule_cache: TTLCache = TTLCache(maxsize=100, ttl=300) self._spot_delist_schedule_cache_lock = Lock() @@ -246,7 +246,7 @@ class Binance(Exchange): delist_schedule = self._api.sapi_get_spot_delist_schedule() if delist_schedule is None: - return + return None with lock: for schedule in delist_schedule: From 78d189564f1a0a80dbe8c192ad711548ee9392e1 Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Sat, 23 Mar 2024 11:26:26 +0100 Subject: [PATCH 6/9] JW; testing with sandbox mode --- freqtrade/exchange/exchange.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 428083f5f..ffd37af6e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -187,6 +187,11 @@ class Exchange: self._api_async = self._init_ccxt( exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) + if exchange_config.get('sandboxMode'): + self._api.set_sandbox_mode(True) + self._api_async.set_sandbox_mode(True) + logger.info('Using ccxt with sandbox mode for paper trading') + logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 if validate: From 80dd586dc7edf701e29785c5be604c98dd26d85c Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Sat, 23 Mar 2024 11:28:34 +0100 Subject: [PATCH 7/9] fix --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ffd37af6e..784d220bc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -187,7 +187,7 @@ class Exchange: self._api_async = self._init_ccxt( exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) - if exchange_config.get('sandboxMode'): + if exchange_conf.get('sandboxMode'): self._api.set_sandbox_mode(True) self._api_async.set_sandbox_mode(True) logger.info('Using ccxt with sandbox mode for paper trading') From 26afad8bc158474cd0f27d534958e136ed1d1c24 Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Sat, 23 Mar 2024 12:27:59 +0100 Subject: [PATCH 8/9] adding some addtional test and makine cache a bit better --- freqtrade/exchange/binance.py | 70 +++++++++++++++++++++------------- tests/exchange/test_binance.py | 3 +- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3076d9602..8d944ef2b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple import ccxt -from cachetools import TTLCache +from cachetools import TTLCache, cached from threading import Lock from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode @@ -225,37 +225,16 @@ class Binance(Exchange): else: return {} - def get_spot_pair_delist_time(self, pair, refresh: bool) -> int | None: - """ - Get the delisting time for a pair if it will be delisted - :param pair: Pair to get the delisting time for - :param refresh: true if you need fresh data - :return: int: delisting time None if not delisting - """ - cache = self._spot_delist_schedule_cache - lock = self._spot_delist_schedule_cache_lock - - if not refresh: - with lock: - delist_time = cache.get(f"{pair}") - if delist_time: - return delist_time + @retrier + @cached(cache=TTLCache(maxsize=100, ttl=10), lock=Lock()) + def get_deslist_schedule(self): try: + delist_schedule = self._api.sapi_get_spot_delist_schedule() - if delist_schedule is None: - return None - - with lock: - for schedule in delist_schedule: - for pair in schedule['symbols']: - cache[f"{pair}"] = int(schedule['delistTime']) - - with lock: - return cache.get(f"{pair}") - + return delist_schedule except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: @@ -263,3 +242,40 @@ class Binance(Exchange): f'Could not get delist schedule {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def get_spot_pair_delist_time(self, pair, refresh: bool = True) -> int | None: + """ + Get the delisting time for a pair if it will be delisted + :param pair: Pair to get the delisting time for + :param refresh: true if you need fresh data + :return: int: delisting time None if not delisting + """ + + if not pair: + return + + cache = self._spot_delist_schedule_cache + lock = self._spot_delist_schedule_cache_lock + + schedule_pair = pair.replace('/', '') + + if not refresh: + with lock: + delist_time = cache[f"{schedule_pair}"] + + if delist_time: + return delist_time + + delist_schedule = self.get_deslist_schedule() + + if delist_schedule is None: + return None + + with lock: + for schedule in delist_schedule: + for symbol in schedule['symbols']: + cache[f"{symbol}"] = int(schedule['delistTime']) + + + with lock: + return cache.get(f"{schedule_pair}") diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 1669c5050..cebc78e83 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -628,4 +628,5 @@ def test_get_spot_delist_schedule(mocker, default_conf) -> None: exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=return_value) - assert exchange.get_spot_pair_delist_time('DREP/USDT', False) == 1712113200000 + assert exchange.get_spot_pair_delist_time('DREP/USDT') == 1712113200000 + assert exchange.get_spot_pair_delist_time('BTC/USDT') is None From 5ec04d168ee2e0707e8c54b5cc00fc7f256eca8f Mon Sep 17 00:00:00 2001 From: "Jakub Werner (jakubikan)" Date: Sat, 23 Mar 2024 12:39:40 +0100 Subject: [PATCH 9/9] removing sandbox mode --- freqtrade/exchange/exchange.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 784d220bc..428083f5f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -187,11 +187,6 @@ class Exchange: self._api_async = self._init_ccxt( exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) - if exchange_conf.get('sandboxMode'): - self._api.set_sandbox_mode(True) - self._api_async.set_sandbox_mode(True) - logger.info('Using ccxt with sandbox mode for paper trading') - logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 if validate: