diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 149d824b6..436c6f75e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -163,11 +163,10 @@ class Binance(Exchange): if pair not in self._leverage_brackets: return 1.0 pair_brackets = self._leverage_brackets[pair] - max_lev = 1.0 - for [notional_floor, maint_margin_ratio] in pair_brackets: + for [notional_floor, mm_ratio, _] in reversed(pair_brackets): if nominal_value >= notional_floor: - max_lev = 1/maint_margin_ratio - return max_lev + return 1/mm_ratio + return 1.0 @retrier def _set_leverage( @@ -227,3 +226,114 @@ class Binance(Exchange): :return: The cutoff open time for when a funding fee is charged """ return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15) + + def get_maintenance_ratio_and_amt( + self, + pair: Optional[str], + nominal_value: Optional[float] + ): + ''' + 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 + https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + ''' + 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 liquidation_price_helper( + self, + open_rate: float, # Entry price of position + is_short: bool, + leverage: float, + trading_mode: TradingMode, + mm_ratio: float, + collateral: Collateral, + maintenance_amt: Optional[float] = None, # (Binance) + position: Optional[float] = None, # (Binance and Gateio) Absolute value of position size + wallet_balance: Optional[float] = None, # (Binance and Gateio) + taker_fee_rate: Optional[float] = None, # (Gateio & Okex) + liability: Optional[float] = None, # (Okex) + interest: Optional[float] = None, # (Okex) + position_assets: Optional[float] = None, # * (Okex) Might be same as position + mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only + upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only + ) -> Optional[float]: + """ + MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed + PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + + :param exchange_name: + :param open_rate: (EP1) Entry price of position + :param is_short: True if the trade is a short, false otherwise + :param leverage: The amount of leverage on the trade + :param trading_mode: SPOT, MARGIN, FUTURES, etc. + :param position: Absolute value of position size (in base currency) + :param mm_ratio: (MMR) + # Binance's formula specifies maintenance margin rate which is mm_ratio * 100% + :param collateral: Either ISOLATED or CROSS + :param maintenance_amt: (CUM) Maintenance Amount of position + :param wallet_balance: (WB) + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + :param position: Absolute value of position size (in base currency) + + # * Not required by Binance + :param taker_fee_rate: + :param liability: + :param interest: + :param position_assets: + + # * Only required for Cross + :param mm_ex_1: (TMM) + Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1 + Isolated-Margin Mode: 0 + :param upnl_ex_1: (UPNL) + Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1. + Isolated-Margin Mode: 0 + """ + if trading_mode == TradingMode.SPOT: + return None + + if not collateral: + raise OperationalException( + "Parameter collateral is required by liquidation_price when trading_mode is " + f"{trading_mode}" + ) + if ( + (wallet_balance is None or maintenance_amt is None or position is None) or + (collateral == Collateral.CROSS and (mm_ex_1 is None or upnl_ex_1 is None)) + ): + required_params = "wallet_balance, maintenance_amt, position" + if collateral == Collateral.CROSS: + required_params += ", mm_ex_1, upnl_ex_1" + raise OperationalException( + f"Parameters {required_params} are required by Binance.liquidation_price" + f"for {collateral.name} {trading_mode.name}" + ) + + side_1 = -1 if is_short else 1 + position = abs(position) + cross_vars = upnl_ex_1 - mm_ex_1 if collateral == Collateral.CROSS else 0.0 # type: ignore + + if trading_mode == TradingMode.FUTURES: + return ( + ( + (wallet_balance + cross_vars + maintenance_amt) - + (side_1 * position * open_rate) + ) / ( + (position * mm_ratio) - (side_1 * position) + ) + ) + + raise OperationalException( + f"Binance does not support {collateral.value} Mode {trading_mode.value} trading ") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 07bc0ae61..38f3a0b99 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2003,8 +2003,11 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier - def get_mm_amt_rate(self, pair: str, amount: float): + def get_maintenance_ratio_and_amt( + self, + pair: Optional[str], + nominal_value: Optional[float] + ): ''' :return: The maintenance amount, and maintenance margin rate ''' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 93eb27bb4..80656f209 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -616,7 +616,7 @@ class FreqtradeBot(LoggingMixin): # open_rate=open_rate, # is_short=is_short # ) - maintenance_amt, mm_rate = self.exchange.get_mm_amt_rate(pair, amount) + mm_ratio, maintenance_amt = self.exchange.get_maintenance_ratio_and_amt(pair, amount) if self.collateral_type == Collateral.ISOLATED: if self.config['dry_run']: @@ -630,9 +630,9 @@ class FreqtradeBot(LoggingMixin): mm_ex_1=0.0, upnl_ex_1=0.0, position=amount * open_rate, - wallet_balance=amount/leverage, # TODO-lev: Is this correct? + wallet_balance=amount/leverage, # TODO: Update for cross maintenance_amt=maintenance_amt, - mm_rate=mm_rate, + mm_ratio=mm_ratio, ) else: isolated_liq = self.exchange.get_liquidation_price(pair) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 30b8885ba..a5a9a6e56 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -35,12 +35,12 @@ def liquidation_price( ''' wallet_balance In Cross margin mode, WB is crossWalletBalance - In Isolated margin mode, WB is isolatedWalletBalance of the isolated position, + In Isolated margin mode, WB is isolatedWalletBalance of the isolated position, TMM=0, UPNL=0, substitute the position quantity, MMR, cum into the formula to calculate. Under the cross margin mode, the same ticker/symbol, - both long and short position share the same liquidation price except in the isolated mode. - Under the isolated mode, each isolated position will have different liquidation prices depending - on the margin allocated to the positions. + both long and short position share the same liquidation price except in the isolated mode. + Under the isolated mode, each isolated position will have different liquidation prices + depending on the margin allocated to the positions. position Absolute value of position size (in base currency) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 91f458cb6..ad4a513b4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -368,7 +368,7 @@ class LocalTrade(): wallet_balance: Optional[float] = None, current_price: Optional[float] = None, maintenance_amt: Optional[float] = None, - mm_rate: Optional[float] = None, + mm_ratio: Optional[float] = None, ): """ Method you should use to set self.liquidation price. @@ -380,6 +380,16 @@ class LocalTrade(): "wallet balance must be passed to LocalTrade.set_isolated_liq when param" "isolated_liq is None" ) + if ( + mm_ratio is None or + wallet_balance is None or + current_price is None or + maintenance_amt is None + ): + raise OperationalException( + 'mm_ratio, wallet_balance, current_price and maintenance_amt ' + 'required in set_isolated_liq when isolated_liq is None' + ) isolated_liq = liquidation_price( exchange_name=self.exchange, open_rate=self.open_rate, @@ -390,9 +400,9 @@ class LocalTrade(): mm_ex_1=0.0, upnl_ex_1=0.0, position=self.amount * current_price, - wallet_balance=self.amount / self.leverage, # TODO-lev: Is this correct? + wallet_balance=self.amount / self.leverage, # TODO: Update for cross maintenance_amt=maintenance_amt, - mm_rate=mm_rate, + mm_ratio=mm_ratio, ) if isolated_liq is None: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index ac7647e73..1239f55e0 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -173,30 +173,32 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev): exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_brackets = { - 'BNB/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]], - 'BNB/USDT': [[0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25]], - '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]], + 'BNB/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]], + 'BNB/USDT': [[0.0, 0.0065, 0.0], + [10000.0, 0.01, 35.00000000000001], + [50000.0, 0.02, 535.0], + [250000.0, 0.05, 8035.000000000001], + [1000000.0, 0.1, 58035.0], + [2000000.0, 0.125, 108034.99999999999], + [5000000.0, 0.15, 233034.99999999994], + [10000000.0, 0.25, 1233035.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] + ] + } assert exchange.get_max_leverage(pair, nominal_value) == max_lev @@ -235,28 +237,28 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): exchange.fill_leverage_brackets() assert exchange._leverage_brackets == { - '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, 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, 1827500.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() @@ -389,3 +391,49 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): default_conf['collateral'] = collateral exchange = get_patched_exchange(mocker, default_conf, id="binance") assert exchange._ccxt_config == config + + +@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [ + ("BNB/BUSD", 0.0, 0.025, 0), + ("BNB/USDT", 100.0, 0.0065, 0), + ("BTC/USDT", 170.30, 0.004, 0), + ("BNB/BUSD", 999999.9, 0.1, 0), + ("BNB/USDT", 5000000.0, 0.5, 0), + ("BTC/USDT", 300000000.1, 0.5, 0), +]) +def test_get_maintenance_ratio_and_amt_binance( + default_conf, + mocker, + pair, + nominal_value, + mm_ratio, + amt +): + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._leverage_brackets = { + 'BNB/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]], + 'BNB/USDT': [[0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25]], + '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]], + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index 300c316d9..4707a5680 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -90,7 +90,7 @@ def test_liquidation_price_exception_thrown( @pytest.mark.parametrize( 'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, ' 'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, ' - 'mm_rate, expected', + 'mm_ratio, expected', [ ("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, 0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), @@ -103,7 +103,7 @@ def test_liquidation_price_exception_thrown( ]) def test_liquidation_price( exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, - mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_rate, expected + mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected ): assert isclose(round(liquidation_price( exchange_name=exchange_name, @@ -117,5 +117,5 @@ def test_liquidation_price( upnl_ex_1=upnl_ex_1, maintenance_amt=maintenance_amt, position=position, - mm_rate=mm_rate + mm_ratio=mm_ratio ), 2), expected)