diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 981126232..b4e1f3d57 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -277,17 +277,15 @@ class Binance(Exchange): # 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( + def dry_run_liquidation_price( self, + pair: str, open_rate: float, # Entry price of position is_short: bool, - mm_ratio: float, position: float, # Absolute value of position size wallet_balance: float, # Or margin balance - taker_fee_rate: Optional[float] = None, # (Gateio & Okex) - maintenance_amt: Optional[float] = None, # (Binance) - mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only - upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only + mm_ex_1: float = 0.0, # (Binance) Cross only + upnl_ex_1: float = 0.0, # (Binance) Cross only ) -> Optional[float]: """ MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed @@ -296,14 +294,10 @@ class Binance(Exchange): :param exchange_name: :param open_rate: (EP1) Entry price of position :param is_short: True if the trade is a short, false otherwise - :param mm_ratio: (MMR) - # Binance's formula specifies maintenance margin rate which is mm_ratio * 100% :param position: Absolute value of position size (in base currency) - :param maintenance_amt: (CUM) Maintenance Amount of position :param wallet_balance: (WB) Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param taker_fee_rate: # * Not required by Binance :param maintenance_amt: # * Only required for Cross @@ -314,30 +308,20 @@ class Binance(Exchange): Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1. Isolated-Margin Mode: 0 """ - if self.trading_mode == TradingMode.SPOT: - return None - elif (self.collateral is None): - raise OperationalException('Binance.collateral must be set for liquidation_price') - - if (maintenance_amt is None): - raise OperationalException( - f"Parameter maintenance_amt is required by Binance.liquidation_price" - f"for {self.collateral.value} {self.trading_mode.value}" - ) - - if (self.collateral == Collateral.CROSS and (mm_ex_1 is None or upnl_ex_1 is None)): - raise OperationalException( - f"Parameters mm_ex_1 and upnl_ex_1 are required by Binance.liquidation_price" - f"for {self.collateral.value} {self.trading_mode.value}" - ) side_1 = -1 if is_short else 1 position = abs(position) - cross_vars = ( - upnl_ex_1 - mm_ex_1 # type: ignore - if self.collateral == Collateral.CROSS else - 0.0 - ) + cross_vars = upnl_ex_1 - mm_ex_1 if self.collateral == Collateral.CROSS else 0.0 + + # 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, position) + + if (maintenance_amt is None): + raise OperationalException( + "Parameter maintenance_amt is required by Binance.liquidation_price" + f"for {self.trading_mode.value}" + ) if self.trading_mode == TradingMode.FUTURES: return ( @@ -348,6 +332,6 @@ class Binance(Exchange): (position * mm_ratio) - (side_1 * position) ) ) - - raise OperationalException( - f"Binance does not support {self.collateral.value} {self.trading_mode.value} trading") + else: + raise OperationalException( + "Freqtrade only supports isolated futures for leverage trading") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f3b4feb40..59f1c5d52 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1984,18 +1984,55 @@ class Exchange: return 0.0 @retrier - def get_liquidation_price(self, pair: str): + def get_liquidation_price( + self, + pair: str, + # Dry-run + open_rate: Optional[float] = None, # Entry price of position + is_short: Optional[bool] = None, + position: Optional[float] = None, # Absolute value of position size + wallet_balance: Optional[float] = None, # Or margin balance + mm_ex_1: float = 0.0, # (Binance) Cross only + upnl_ex_1: float = 0.0, # (Binance) Cross only + ): """ Set's the margin mode on the exchange to cross or isolated for a specific pair :param pair: base/quote currency pair (e.g. "ADA/USDT") """ + if self.trading_mode == TradingMode.SPOT: + return None + elif (self.collateral is None): + raise OperationalException(f'{self.name}.collateral must be set for liquidation_price') + elif (self.trading_mode != TradingMode.FUTURES and self.collateral != Collateral.ISOLATED): + raise OperationalException( + f"{self.name} does not support {self.collateral.value} {self.trading_mode.value}") + if self._config['dry_run'] or not self.exchange_has("fetchPositions"): - return + if ( + open_rate is None or + is_short is None or + position is None or + wallet_balance is None + ): + raise OperationalException( + f"Parameters open_rate, is_short, position, wallet_balance are" + f"required by {self.name}.liquidation_price for dry_run" + ) + + return self.dry_run_liquidation_price( + pair=pair, + open_rate=open_rate, + is_short=is_short, + position=position, + wallet_balance=wallet_balance, + mm_ex_1=mm_ex_1, + upnl_ex_1=upnl_ex_1 + ) try: positions = self._api.fetch_positions([pair]) - position = positions[0] - return position['liquidationPrice'] + pos = positions[0] + return pos['liquidationPrice'] except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: @@ -2014,17 +2051,15 @@ class Exchange: """ raise OperationalException(self.name + ' does not support leverage futures trading') - def liquidation_price( + def dry_run_liquidation_price( self, + pair: str, open_rate: float, # Entry price of position is_short: bool, - mm_ratio: float, position: float, # Absolute value of position size wallet_balance: float, # Or margin balance - taker_fee_rate: Optional[float] = None, # (Gateio & Okex) - maintenance_amt: Optional[float] = None, # (Binance) - mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only - upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only + mm_ex_1: float = 0.0, # (Binance) Cross only + upnl_ex_1: float = 0.0, # (Binance) Cross only ) -> Optional[float]: """ PERPETUAL: @@ -2036,33 +2071,26 @@ class Exchange: :param open_rate: Entry price of position :param is_short: True if the trade is a short, false otherwise :param position: Absolute value of position size (in base currency) - :param mm_ratio: :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param collateral: Either ISOLATED or CROSS :param wallet_balance: Amount of collateral in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param taker_fee_rate: # * Not required by Gateio or OKX - :param maintenance_amt: :param mm_ex_1: :param upnl_ex_1: """ - if self.trading_mode == TradingMode.SPOT: - return None - elif (self.collateral is None): - raise OperationalException('Binance.collateral must be set for liquidation_price') - if (not taker_fee_rate): - raise OperationalException( - f"Parameter taker_fee_rate is required by {self.name}.liquidation_price" - ) + market = self.markets[pair] + taker_fee_rate = market['taker'] + mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, position) if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED: - # if is_inverse: - # raise OperationalException( - # "Freqtrade does not support inverse contracts at the moment") + + if market['inverse']: + raise OperationalException( + "Freqtrade does not yet support inverse contracts") value = wallet_balance / position @@ -2073,7 +2101,7 @@ class Exchange: return (open_rate - value) / (1 - mm_ratio_taker) else: raise OperationalException( - f"{self.name} does not support {self.collateral.value} {self.trading_mode.value}") + "Freqtrade only supports isolated futures for leverage trading") def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index b7babe6e9..86131faed 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -1,5 +1,6 @@ import logging from typing import Dict, List, Tuple + from freqtrade.enums import Collateral, TradingMode from freqtrade.exchange import Exchange diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f39c97b3a..0f97804ce 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,7 +19,7 @@ from freqtrade.edge import Edge from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) + InvalidOrderException, OperationalException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin @@ -606,38 +606,31 @@ class FreqtradeBot(LoggingMixin): is_short: bool ) -> Tuple[float, Optional[float]]: - interest_rate = 0.0 - isolated_liq = None - # if TradingMode == TradingMode.MARGIN: # interest_rate = self.exchange.get_interest_rate( # pair=pair, # open_rate=open_rate, # is_short=is_short # ) - - if self.collateral_type == Collateral.ISOLATED: - if self.config['dry_run']: - mm_ratio, maintenance_amt = self.exchange.get_maintenance_ratio_and_amt( - pair, - amount - ) - taker_fee_rate = self.exchange.markets[pair]['taker'] - isolated_liq = self.exchange.liquidation_price( - open_rate=open_rate, - is_short=is_short, - mm_ratio=mm_ratio, - position=amount, - wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross - taker_fee_rate=taker_fee_rate, - maintenance_amt=maintenance_amt, - mm_ex_1=0.0, - upnl_ex_1=0.0, - ) - else: - isolated_liq = self.exchange.get_liquidation_price(pair) - - return interest_rate, isolated_liq + if self.trading_mode == TradingMode.SPOT: + return (0.0, None) + elif ( + self.collateral_type == Collateral.ISOLATED and + self.trading_mode == TradingMode.FUTURES + ): + isolated_liq = self.exchange.get_liquidation_price( + pair=pair, + open_rate=open_rate, + is_short=is_short, + position=amount, + wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross + mm_ex_1=0.0, + upnl_ex_1=0.0, + ) + return (0.0, isolated_liq) + else: + raise OperationalException( + "Freqtrade only supports isolated futures for leverage trading") def execute_entry( self, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 884afb11d..afee0725f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -362,10 +362,7 @@ class LocalTrade(): self.stop_loss_pct = -1 * abs(percent) self.stoploss_last_update = datetime.utcnow() - def set_isolated_liq( - self, - isolated_liq: float, - ): + def set_isolated_liq(self, isolated_liq: float): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price diff --git a/tests/conftest.py b/tests/conftest.py index 2bacb498e..0b534b7d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -817,29 +817,42 @@ def get_markets(): 'symbol': 'ETH/USDT', 'base': 'ETH', 'quote': 'USDT', - 'spot': True, - 'future': True, - 'swap': True, - 'margin': True, + 'settle': 'USDT', + 'baseId': 'ETH', + 'quoteId': 'USDT', + 'settleId': 'USDT', 'type': 'spot', - 'contractSize': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'active': True, + 'contract': True, + 'linear': True, + 'inverse': False, 'taker': 0.0006, 'maker': 0.0002, + 'contractSize': 1, + 'expiry': 1680220800000, + 'expiryDateTime': '2023-03-31T00:00:00.000Z', + 'strike': None, + 'optionType': None, 'precision': { 'amount': 8, - 'price': 8 + 'price': 8, }, 'limits': { + 'leverage': { + 'min': 1, + 'max': 100, + }, 'amount': { 'min': 0.02214286, - 'max': None + 'max': None, }, 'price': { 'min': 1e-08, - 'max': None - }, - 'leverage': { - 'min': None, 'max': None, }, 'cost': { @@ -847,8 +860,9 @@ def get_markets(): 'max': None, }, }, - 'active': True, - 'info': {}, + 'info': { + 'maintenance_rate': '0.005', + }, }, 'LTC/USDT': { 'id': 'USDT-LTC', @@ -1110,7 +1124,6 @@ def get_markets(): 'swap': True, 'futures': False, 'option': False, - 'derivative': True, 'contract': True, 'linear': True, 'inverse': False, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cfdcaa596..08d0e255c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3975,13 +3975,13 @@ def test__amount_to_contracts( @pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [ # Bittrex - ('bittrex', 2.0, False, 3.0, spot, None), - ('bittrex', 2.0, False, 1.0, spot, cross), - ('bittrex', 2.0, True, 3.0, spot, isolated), + ('bittrex', 2.0, False, 3.0, 'spot', None), + ('bittrex', 2.0, False, 1.0, 'spot', 'cross'), + ('bittrex', 2.0, True, 3.0, 'spot', 'isolated'), # Binance - ('binance', 2.0, False, 3.0, spot, None), - ('binance', 2.0, False, 1.0, spot, cross), - ('binance', 2.0, True, 3.0, spot, isolated), + ('binance', 2.0, False, 3.0, 'spot', None), + ('binance', 2.0, False, 1.0, 'spot', 'cross'), + ('binance', 2.0, True, 3.0, 'spot', 'isolated'), ]) def test_liquidation_price_is_none( mocker, @@ -3996,14 +3996,12 @@ def test_liquidation_price_is_none( default_conf['trading_mode'] = trading_mode default_conf['collateral'] = collateral exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - assert exchange.liquidation_price( + assert exchange.get_liquidation_price( + pair='DOGE/USDT', open_rate=open_rate, is_short=is_short, - mm_ratio=1535443.01, position=71200.81144, wallet_balance=-56354.57, - taker_fee_rate=0.01, - maintenance_amt=3683.979, mm_ex_1=0.10, upnl_ex_1=0.0 ) is None @@ -4014,13 +4012,13 @@ def test_liquidation_price_is_none( 'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, ' 'mm_ratio, expected', [ - ("binance", False, 1, futures, isolated, 1535443.01, 0.0, + ("binance", False, 1, 'futures', 'isolated', 1535443.01, 0.0, 0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), - ("binance", False, 1, futures, isolated, 1535443.01, 0.0, + ("binance", False, 1, 'futures', 'isolated', 1535443.01, 0.0, 0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73), - ("binance", False, 1, futures, cross, 1535443.01, 71200.81144, + ("binance", False, 1, 'futures', 'cross', 1535443.01, 71200.81144, -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26), - ("binance", False, 1, futures, cross, 1535443.01, 356512.508, + ("binance", False, 1, 'futures', 'cross', 1535443.01, 356512.508, -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) ]) def test_liquidation_price( @@ -4030,13 +4028,13 @@ def test_liquidation_price( default_conf['trading_mode'] = trading_mode default_conf['collateral'] = collateral exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - assert isclose(round(exchange.liquidation_price( + exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt)) + assert isclose(round(exchange.get_liquidation_price( + pair='DOGE/USDT', open_rate=open_rate, is_short=is_short, wallet_balance=wallet_balance, mm_ex_1=mm_ex_1, upnl_ex_1=upnl_ex_1, - maintenance_amt=maintenance_amt, position=position, - mm_ratio=mm_ratio ), 2), expected) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e42a7ce88..07187ceb1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -735,11 +735,11 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) ((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071 - exchange_name = gateio, is_short = true + exchange_name = gateio/okex, is_short = true (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) (10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 - exchange_name = gateio, is_short = false + exchange_name = gateio/okex, is_short = false (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 """ @@ -747,10 +747,12 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order = limit_order[enter_side(is_short)] default_conf_usdt['trading_mode'] = trading_mode leverage = 1.0 if trading_mode == 'spot' else 5.0 + default_conf_usdt['exchange']['name'] = exchange_name if margin_mode: default_conf_usdt['collateral'] = margin_mode + mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes') patch_RPCManager(mocker) - patch_exchange(mocker) + patch_exchange(mocker, id=exchange_name) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.leverage = MagicMock(return_value=leverage)