diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 6a2251d65..93a123708 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -169,11 +169,11 @@ class Binance(Exchange): + amt ) if old_ratio else 0.0 old_ratio = mm_ratio - brackets.append([ + brackets.append(( float(notional_floor), float(mm_ratio), amt, - ]) + )) self._leverage_brackets[pair] = brackets except ccxt.DDoSProtection as e: raise DDosProtection(e) from e @@ -272,34 +272,6 @@ class Binance(Exchange): """ return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15) - def get_maintenance_ratio_and_amt( - self, - pair: str, - nominal_value: Optional[float] = 0.0, - ) -> Tuple[float, Optional[float]]: - """ - Formula: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - - Maintenance amt = Floor of Position Bracket on Level n * - difference between - Maintenance Margin Rate on Level n and - Maintenance Margin Rate on Level n-1) - + Maintenance Amount on Level n-1 - :return: The maintenance margin ratio and maintenance amount - """ - if nominal_value is None: - raise OperationalException( - "nominal value is required for binance.get_maintenance_ratio_and_amt") - if pair not in self._leverage_brackets: - raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}") - pair_brackets = self._leverage_brackets[pair] - for [notional_floor, mm_ratio, amt] in reversed(pair_brackets): - if nominal_value >= notional_floor: - return (mm_ratio, amt) - raise OperationalException("nominal value can not be lower than 0") - # The lowest notional_floor for any pair in loadLeverageBrackets is always 0 because it - # describes the min amount for a bracket, and the lowest bracket will always go down to 0 - def dry_run_liquidation_price( self, pair: str, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 90b63b57b..581221b49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -73,7 +73,8 @@ class Exchange: "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) "mark_ohlcv_price": "mark", "mark_ohlcv_timeframe": "8h", - "ccxt_futures_name": "swap" + "ccxt_futures_name": "swap", + "mmr_key": None, } _ft_has: Dict = {} @@ -90,7 +91,7 @@ class Exchange: self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None self._markets: Dict = {} - self._leverage_brackets: Dict[str, List[List[float]]] = {} + self._leverage_brackets: Dict[str, List[Tuple[float, float, Optional(float)]]] = {} self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) @@ -2099,16 +2100,6 @@ class Exchange: else: return None - def get_maintenance_ratio_and_amt( - self, - pair: str, - nominal_value: Optional[float] = 0.0, - ) -> Tuple[float, Optional[float]]: - """ - :return: The maintenance margin ratio and maintenance amount - """ - raise OperationalException(self.name + ' does not support leverage futures trading') - def dry_run_liquidation_price( self, pair: str, @@ -2161,6 +2152,59 @@ class Exchange: raise OperationalException( "Freqtrade only supports isolated futures for leverage trading") + def get_leverage_tiers(self, pair: str): + # When exchanges can load all their leverage brackets at once in the constructor + # then this method does nothing, it should only be implemented when the leverage + # brackets requires per symbol fetching to avoid excess api calls + return None + + def get_maintenance_ratio_and_amt( + self, + pair: str, + nominal_value: Optional[float] = 0.0, + ) -> Tuple[float, Optional[float]]: + """ + :param pair: Market symbol + :param nominal_value: The total trade amount in quote currency including leverage + maintenance amount only on Binance + :return: (maintenance margin ratio, maintenance amount) + """ + if nominal_value is None: + raise OperationalException( + f"nominal value is required for {self.name}.get_maintenance_ratio_and_amt" + ) + if self._api.has['fetchLeverageTiers']: + if pair not in self._leverage_brackets: + # Used when fetchLeverageTiers cannot fetch all symbols at once + tiers = self.get_leverage_tiers(pair) + if not bool(tiers): + raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}") + else: + self._leverage_brackets[pair] = [] + for tier in tiers[pair]: + self._leverage_brackets[pair].append(( + tier['notionalFloor'], + tier['maintenanceMarginRatio'], + None, + )) + pair_brackets = self._leverage_brackets[pair] + for (notional_floor, mm_ratio, amt) in reversed(pair_brackets): + if nominal_value >= notional_floor: + return (mm_ratio, amt) + raise OperationalException("nominal value can not be lower than 0") + # The lowest notional_floor for any pair in loadLeverageBrackets is always 0 because it + # describes the min amt for a bracket, and the lowest bracket will always go down to 0 + else: + info = self.markets[pair]['info'] + mmr_key = self._ft_has['mmr_key'] + if mmr_key and mmr_key in info: + return (float(info[mmr_key]), None) + else: + raise OperationalException( + f"Cannot fetch maintenance margin. Dry-run for freqtrade {self.trading_mode}" + f"is not available for {self.name}" + ) + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index bcb4cce33..57ff29924 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Tuple from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException @@ -23,6 +23,7 @@ class Gateio(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, "ohlcv_volume_currency": "quote", + "mmr_key": "maintenance_rate", } _headers = {'X-Gate-Channel-Id': 'freqtrade'} @@ -40,14 +41,3 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') - - def get_maintenance_ratio_and_amt( - self, - pair: str, - nominal_value: Optional[float] = 0.0, - ) -> Tuple[float, Optional[float]]: - """ - :return: The maintenance margin ratio and maintenance amount - """ - info = self.markets[pair]['info'] - return (float(info['maintenance_rate']), None) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index e74a06dc0..051aebb1a 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -25,7 +25,7 @@ class Okx(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS), - # (TradingMode.FUTURES, MarginMode.ISOLATED) + (TradingMode.FUTURES, MarginMode.ISOLATED), ] def _lev_prep( @@ -46,3 +46,6 @@ class Okx(Exchange): "mgnMode": self.margin_mode.value, "posSide": "long" if side == "buy" else "short", }) + + def get_leverage_tiers(self, pair: str): + return self._api.fetch_leverage_tiers(pair) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 5bd383d6e..cc5410e26 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -210,28 +210,34 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, stake_amount, max_ def test_fill_leverage_brackets_binance(default_conf, mocker): api_mock = MagicMock() api_mock.load_leverage_brackets = MagicMock(return_value={ - 'ADA/BUSD': [[0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5]], - 'BTC/USDT': [[0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], + 'ADA/BUSD': [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5], + ], + 'BTC/USDT': [ + [0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5], + ], + "ZEC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5], + ], }) default_conf['dry_run'] = False @@ -241,28 +247,34 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): exchange.fill_leverage_brackets() assert exchange._leverage_brackets == { - 'ADA/BUSD': [[0.0, 0.025, 0.0], - [100000.0, 0.05, 2500.0], - [500000.0, 0.1, 27500.0], - [1000000.0, 0.15, 77499.99999999999], - [2000000.0, 0.25, 277500.0], - [5000000.0, 0.5, 1527500.0]], - 'BTC/USDT': [[0.0, 0.004, 0.0], - [50000.0, 0.005, 50.0], - [250000.0, 0.01, 1300.0], - [1000000.0, 0.025, 16300.000000000002], - [5000000.0, 0.05, 141300.0], - [20000000.0, 0.1, 1141300.0], - [50000000.0, 0.125, 2391300.0], - [100000000.0, 0.15, 4891300.0], - [200000000.0, 0.25, 24891300.0], - [300000000.0, 0.5, 99891300.0]], - "ZEC/USDT": [[0.0, 0.01, 0.0], - [5000.0, 0.025, 75.0], - [25000.0, 0.05, 700.0], - [100000.0, 0.1, 5700.0], - [250000.0, 0.125, 11949.999999999998], - [1000000.0, 0.5, 386950.0]] + 'ADA/BUSD': [ + (0.0, 0.025, 0.0), + (100000.0, 0.05, 2500.0), + (500000.0, 0.1, 27500.0), + (1000000.0, 0.15, 77499.99999999999), + (2000000.0, 0.25, 277500.0), + (5000000.0, 0.5, 1527500.0), + ], + 'BTC/USDT': [ + (0.0, 0.004, 0.0), + (50000.0, 0.005, 50.0), + (250000.0, 0.01, 1300.0), + (1000000.0, 0.025, 16300.000000000002), + (5000000.0, 0.05, 141300.0), + (20000000.0, 0.1, 1141300.0), + (50000000.0, 0.125, 2391300.0), + (100000000.0, 0.15, 4891300.0), + (200000000.0, 0.25, 24891300.0), + (300000000.0, 0.5, 99891300.0), + ], + "ZEC/USDT": [ + (0.0, 0.01, 0.0), + (5000.0, 0.025, 75.0), + (25000.0, 0.05, 700.0), + (100000.0, 0.1, 5700.0), + (250000.0, 0.125, 11949.999999999998), + (1000000.0, 0.5, 386950.0), + ] } api_mock = MagicMock() @@ -288,37 +300,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): leverage_brackets = { "1000SHIB/USDT": [ - [0.0, 0.01, 0.0], - [5000.0, 0.025, 75.0], - [25000.0, 0.05, 700.0], - [100000.0, 0.1, 5700.0], - [250000.0, 0.125, 11949.999999999998], - [1000000.0, 0.5, 386950.0], + (0.0, 0.01, 0.0), + (5000.0, 0.025, 75.0), + (25000.0, 0.05, 700.0), + (100000.0, 0.1, 5700.0), + (250000.0, 0.125, 11949.999999999998), + (1000000.0, 0.5, 386950.0), ], "1INCH/USDT": [ - [0.0, 0.012, 0.0], - [5000.0, 0.025, 65.0], - [25000.0, 0.05, 690.0], - [100000.0, 0.1, 5690.0], - [250000.0, 0.125, 11939.999999999998], - [1000000.0, 0.5, 386940.0], + (0.0, 0.012, 0.0), + (5000.0, 0.025, 65.0), + (25000.0, 0.05, 690.0), + (100000.0, 0.1, 5690.0), + (250000.0, 0.125, 11939.999999999998), + (1000000.0, 0.5, 386940.0), ], "AAVE/USDT": [ - [0.0, 0.01, 0.0], - [50000.0, 0.02, 500.0], - [250000.0, 0.05, 8000.000000000001], - [1000000.0, 0.1, 58000.0], - [2000000.0, 0.125, 107999.99999999999], - [5000000.0, 0.1665, 315500.00000000006], - [10000000.0, 0.25, 1150500.0], + (0.0, 0.01, 0.0), + (50000.0, 0.02, 500.0), + (250000.0, 0.05, 8000.000000000001), + (1000000.0, 0.1, 58000.0), + (2000000.0, 0.125, 107999.99999999999), + (5000000.0, 0.1665, 315500.00000000006), + (10000000.0, 0.25, 1150500.0), ], "ADA/BUSD": [ - [0.0, 0.025, 0.0], - [100000.0, 0.05, 2500.0], - [500000.0, 0.1, 27500.0], - [1000000.0, 0.15, 77499.99999999999], - [2000000.0, 0.25, 277500.0], - [5000000.0, 0.5, 1527500.0], + (0.0, 0.025, 0.0), + (100000.0, 0.05, 2500.0), + (500000.0, 0.1, 27500.0), + (1000000.0, 0.15, 77499.99999999999), + (2000000.0, 0.25, 277500.0), + (5000000.0, 0.5, 1527500.0), ] } diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index a4d91c35c..f344ee7cb 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -37,6 +37,7 @@ def test_validate_order_types_gateio(default_conf, mocker): ]) def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): api_mock = MagicMock() + type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") mocker.patch( 'freqtrade.exchange.Exchange.markets', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1442186ea..8226266b7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -722,8 +722,8 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717), (True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304), (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796), - # (True, 'futures', 'okex', 'isolated', 11.87413417771621), - # (False, 'futures', 'okex', 'isolated', 8.085708510208207), + (True, 'futures', 'okex', 'isolated', 11.87413417771621), + (False, 'futures', 'okex', 'isolated', 8.085708510208207), ]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode, @@ -778,7 +778,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, get_funding_fees=MagicMock(return_value=0), - name=exchange_name + name=exchange_name, + get_maintenance_ratio_and_amt=MagicMock(return_value=(0.01, 0.01)), ) pair = 'ETH/USDT' @@ -922,7 +923,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade.open_rate_requested == 10 # In case of custom entry price not float type - freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) freqtrade.exchange.name = exchange_name order['status'] = 'open' order['id'] = '5568'