diff --git a/docs/leverage.md b/docs/leverage.md index de0b0a981..55f644462 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -70,9 +70,14 @@ One account is used to share collateral between markets (trading pairs). Margin ``` ## Understand `liquidation_buffer` -*Defaults to `0.05`.* +*Defaults to `0.05`* -A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price +A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price. +This artificial liquidation price is calculated as + +`freqtrade_liquidation_price = liquidation_price ± (abs(open_rate - liquidation_price) * liquidation_buffer)` +- `±` = `+` for long trades +- `±` = `-` for short trades Possible values are any floats between 0.0 and 0.99 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 092420eab..0f693036e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2055,6 +2055,43 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def get_interest_rate(self) -> float: + """ + Retrieve interest rate - necessary for Margin trading. + Should not call the exchange directly when used from backtesting. + """ + return 0.0 + + def get_liquidation_price( + self, + pair: str, + open_rate: float, + amount: float, # quote currency, includes leverage + leverage: float, + is_short: bool + ) -> Optional[float]: + + if self.trading_mode in TradingMode.SPOT: + return None + elif ( + self.margin_mode == MarginMode.ISOLATED and + self.trading_mode == TradingMode.FUTURES + ): + wallet_balance = (amount * open_rate) / leverage + isolated_liq = self.get_or_calculate_liquidation_price( + pair=pair, + open_rate=open_rate, + is_short=is_short, + position=amount, + wallet_balance=wallet_balance, + mm_ex_1=0.0, + upnl_ex_1=0.0, + ) + return isolated_liq + else: + raise OperationalException( + "Freqtrade only supports isolated futures for leverage trading") + def funding_fee_cutoff(self, open_date: datetime): """ :param open_date: The open date for a trade @@ -2195,7 +2232,7 @@ class Exchange: return 0.0 @retrier - def get_liquidation_price( + def get_or_calculate_liquidation_price( self, pair: str, # Dry-run @@ -2271,6 +2308,7 @@ class Exchange: gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price okex: https://www.okex.com/support/hc/en-us/articles/ 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin + Important: Must be fetching data from cached values as this is used by backtesting! :param exchange_name: :param open_rate: Entry price of position @@ -2314,6 +2352,7 @@ class Exchange: nominal_value: float = 0.0, ) -> Tuple[float, Optional[float]]: """ + Important: Must be fetching data from cached values as this is used by backtesting! :param pair: Market symbol :param nominal_value: The total trade amount in quote currency including leverage maintenance amount only on Binance diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0026667d3..0410b436d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,7 +19,7 @@ from freqtrade.edge import Edge from freqtrade.enums import (MarginMode, RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, OperationalException, PricingError) + InvalidOrderException, 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 @@ -577,42 +577,6 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def leverage_prep( - self, - pair: str, - open_rate: float, - amount: float, # quote currency, includes leverage - leverage: float, - is_short: bool - ) -> Tuple[float, Optional[float]]: - - # if TradingMode == TradingMode.MARGIN: - # interest_rate = self.exchange.get_interest_rate( - # pair=pair, - # open_rate=open_rate, - # is_short=is_short - # ) - if self.trading_mode == TradingMode.SPOT: - return (0.0, None) - elif ( - self.margin_mode == MarginMode.ISOLATED and - self.trading_mode == TradingMode.FUTURES - ): - wallet_balance = (amount * open_rate)/leverage - isolated_liq = self.exchange.get_liquidation_price( - pair=pair, - open_rate=open_rate, - is_short=is_short, - position=amount, - wallet_balance=wallet_balance, - 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, pair: str, @@ -727,13 +691,14 @@ class FreqtradeBot(LoggingMixin): enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') # TODO: this might be unnecessary, as we're calling it in update_trade_state. - interest_rate, isolated_liq = self.leverage_prep( + isolated_liq = self.exchange.get_liquidation_price( leverage=leverage, pair=pair, amount=amount, open_rate=enter_limit_filled_price, is_short=is_short ) + interest_rate = self.exchange.get_interest_rate() # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') @@ -1603,15 +1568,16 @@ class FreqtradeBot(LoggingMixin): if order.get('side', None) == trade.enter_side: trade = self.cancel_stoploss_on_exchange(trade) # TODO: Margin will need to use interest_rate as well. - _, isolated_liq = self.leverage_prep( + # interest_rate = self.exchange.get_interest_rate() + trade.set_isolated_liq(self.exchange.get_liquidation_price( + leverage=trade.leverage, pair=trade.pair, amount=trade.amount, open_rate=trade.open_rate, is_short=trade.is_short - ) - if isolated_liq: - trade.set_isolated_liq(isolated_liq) + )) + # Updating wallets when order is closed self.wallets.update() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4e881d271..fa3deb86f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,7 +19,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import BacktestState, CandleType, SellType, TradingMode +from freqtrade.enums import BacktestState, CandleType, MarginMode, SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import get_strategy_run_id @@ -130,6 +130,7 @@ class Backtesting: # TODO-lev: This should come from the configuration setting or better a # TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) + self.margin_mode: MarginMode = config.get('margin_mode', MarginMode.NONE) self._can_short = self.trading_mode != TradingMode.SPOT self.progress = BTProgress() @@ -638,6 +639,8 @@ class Backtesting: # In case of pos adjust, still return the original trade # If not pos adjust, trade is None return trade + order_type = self.strategy.order_types['buy'] + time_in_force = self.strategy.order_time_in_force['buy'] if not pos_adjust: max_leverage = self.exchange.get_max_leverage(pair, stake_amount) @@ -651,22 +654,23 @@ class Backtesting: ) if self._can_short else 1.0 # Cap leverage between 1.0 and max_leverage. leverage = min(max(leverage, 1.0), max_leverage) - else: - leverage = trade.leverage if trade else 1.0 - order_type = self.strategy.order_types['buy'] - time_in_force = self.strategy.order_time_in_force['buy'] - # Confirm trade entry: - if not pos_adjust: + # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, time_in_force=time_in_force, current_time=current_time, entry_tag=entry_tag, side=direction): - return None + return trade + else: + leverage = trade.leverage if trade else 1.0 if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): self.order_id_counter += 1 amount = round((stake_amount / propose_rate) * leverage, 8) + is_short = (direction == 'short') + # Necessary for Margin trading. Disabled until support is enabled. + # interest_rate = self.exchange.get_interest_rate() + if trade is None: # Enter trade self.trade_id_counter += 1 @@ -685,14 +689,23 @@ class Backtesting: is_open=True, enter_tag=entry_tag, exchange=self._exchange_name, - is_short=(direction == 'short'), + is_short=is_short, trading_mode=self.trading_mode, leverage=leverage, - orders=[] + # interest_rate=interest_rate, + orders=[], ) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) + trade.set_isolated_liq(self.exchange.get_liquidation_price( + pair=pair, + open_rate=propose_rate, + amount=amount, + leverage=leverage, + is_short=is_short, + )) + order = Order( id=self.order_id_counter, ft_trade_id=trade.id, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8d85c775b..b6ae496fb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -425,11 +425,13 @@ 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: Optional[float]): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ + if not isolated_liq: + return if self.stop_loss is not None: if self.is_short: self.stop_loss = min(self.stop_loss, isolated_liq) diff --git a/tests/conftest.py b/tests/conftest.py index 407dc2678..33b2e92b5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1175,7 +1175,7 @@ def get_markets(): 'spot': False, 'margin': False, 'swap': True, - 'futures': False, + 'future': True, # Binance mode ... 'option': False, 'contract': True, 'linear': True, @@ -1278,7 +1278,7 @@ def get_markets(): 'spot': False, 'margin': False, 'swap': True, - 'future': False, + 'future': True, # Binance mode ... 'option': False, 'active': True, 'contract': True, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 894f5b75b..fadcacc53 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3666,7 +3666,7 @@ def test_calculate_funding_fees( ) == kraken_fee -def test_get_liquidation_price(mocker, default_conf): +def test_get_or_calculate_liquidation_price(mocker, default_conf): api_mock = MagicMock() positions = [ @@ -3705,7 +3705,7 @@ def test_get_liquidation_price(mocker, default_conf): default_conf['liquidation_buffer'] = 0.0 exchange = get_patched_exchange(mocker, default_conf, api_mock) - liq_price = exchange.get_liquidation_price( + liq_price = exchange.get_or_calculate_liquidation_price( pair='NEAR/USDT:USDT', open_rate=18.884, is_short=False, @@ -3716,7 +3716,7 @@ def test_get_liquidation_price(mocker, default_conf): default_conf['liquidation_buffer'] = 0.05 exchange = get_patched_exchange(mocker, default_conf, api_mock) - liq_price = exchange.get_liquidation_price( + liq_price = exchange.get_or_calculate_liquidation_price( pair='NEAR/USDT:USDT', open_rate=18.884, is_short=False, @@ -3730,7 +3730,7 @@ def test_get_liquidation_price(mocker, default_conf): default_conf, api_mock, "binance", - "get_liquidation_price", + "get_or_calculate_liquidation_price", "fetch_positions", pair="XRP/USDT", open_rate=0.0, @@ -4088,7 +4088,7 @@ def test_liquidation_price_is_none( default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = margin_mode exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - assert exchange.get_liquidation_price( + assert exchange.get_or_calculate_liquidation_price( pair='DOGE/USDT', open_rate=open_rate, is_short=is_short, @@ -4122,7 +4122,7 @@ def test_liquidation_price( default_conf['liquidation_buffer'] = 0.0 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt)) - assert isclose(round(exchange.get_liquidation_price( + assert isclose(round(exchange.get_or_calculate_liquidation_price( pair='DOGE/USDT', open_rate=open_rate, is_short=is_short, @@ -4527,3 +4527,126 @@ def test__get_params(mocker, default_conf, exchange_name): time_in_force='ioc', leverage=3.0, ) == params2 + + +@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05]) +@pytest.mark.parametrize( + "is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [ + (False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), + (False, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), + (False, 'spot', 'okx', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'okx', '', 5.0, 10.0, 1.0, None), + # Binance, short + (True, 'futures', 'binance', 'isolated', 5.0, 10.0, 1.0, 11.89108910891089), + (True, 'futures', 'binance', 'isolated', 3.0, 10.0, 1.0, 13.211221122079207), + (True, 'futures', 'binance', 'isolated', 5.0, 8.0, 1.0, 9.514851485148514), + (True, 'futures', 'binance', 'isolated', 5.0, 10.0, 0.6, 12.557755775577558), + # Binance, long + (False, 'futures', 'binance', 'isolated', 5, 10, 1.0, 8.070707070707071), + (False, 'futures', 'binance', 'isolated', 5, 8, 1.0, 6.454545454545454), + (False, 'futures', 'binance', 'isolated', 3, 10, 1.0, 6.717171717171718), + (False, 'futures', 'binance', 'isolated', 5, 10, 0.6, 7.39057239057239), + # Gateio/okx, short + (True, 'futures', 'gateio', 'isolated', 5, 10, 1.0, 11.87413417771621), + (True, 'futures', 'gateio', 'isolated', 5, 10, 2.0, 11.87413417771621), + (True, 'futures', 'gateio', 'isolated', 3, 10, 1.0, 13.476180850346978), + (True, 'futures', 'gateio', 'isolated', 5, 8, 1.0, 9.499307342172967), + # Gateio/okx, long + (False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), + (False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), + # (True, 'futures', 'okx', 'isolated', 11.87413417771621), + # (False, 'futures', 'okx', 'isolated', 8.085708510208207), + ] +) +def test_get_liquidation_price( + mocker, + default_conf_usdt, + is_short, + trading_mode, + exchange_name, + margin_mode, + leverage, + open_rate, + amount, + expected_liq, + liquidation_buffer, +): + """ + position = 0.2 * 5 + wb: wallet balance (stake_amount if isolated) + cum_b: maintenance amount + side_1: -1 if is_short else 1 + ep1: entry price + mmr_b: maintenance margin ratio + + Binance, Short + leverage = 5, open_rate = 10, amount = 1.0 + ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + ((2 + 0.01) - ((-1) * 1 * 10)) / ((1 * 0.01) - ((-1) * 1)) = 11.89108910891089 + leverage = 3, open_rate = 10, amount = 1.0 + ((3.3333333333 + 0.01) - ((-1) * 1.0 * 10)) / ((1.0 * 0.01) - ((-1) * 1.0)) = 13.2112211220 + leverage = 5, open_rate = 8, amount = 1.0 + ((1.6 + 0.01) - ((-1) * 1 * 8)) / ((1 * 0.01) - ((-1) * 1)) = 9.514851485148514 + leverage = 5, open_rate = 10, amount = 0.6 + ((1.6 + 0.01) - ((-1) * 0.6 * 10)) / ((0.6 * 0.01) - ((-1) * 0.6)) = 12.557755775577558 + + Binance, Long + leverage = 5, open_rate = 10, amount = 1.0 + ((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 + leverage = 5, open_rate = 8, amount = 1.0 + ((1.6 + 0.01) - (1 * 1 * 8)) / ((1 * 0.01) - (1 * 1)) = 6.454545454545454 + leverage = 3, open_rate = 10, amount = 1.0 + ((2 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 6.717171717171718 + leverage = 5, open_rate = 10, amount = 0.6 + ((1.6 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 7.39057239057239 + + Gateio/Okx, Short + leverage = 5, open_rate = 10, amount = 1.0 + (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) + (10 + (2 / 1.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 + leverage = 5, open_rate = 10, amount = 2.0 + (10 + (4 / 2.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 + leverage = 3, open_rate = 10, amount = 1.0 + (10 + (3.3333333333333 / 1.0)) / (1 - (0.01 + 0.0006)) = 13.476180850346978 + leverage = 5, open_rate = 8, amount = 1.0 + (8 + (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 9.499307342172967 + + Gateio/Okx, Long + leverage = 5, open_rate = 10, amount = 1.0 + (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) + (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 + leverage = 5, open_rate = 10, amount = 2.0 + (10 - (4 / 2.0)) / (1 + (0.01 + 0.0006)) = 7.916089451810806 + leverage = 3, open_rate = 10, amount = 1.0 + (10 - (3.333333333333333333 / 1.0)) / (1 - (0.01 + 0.0006)) = 6.738090425173506 + leverage = 5, open_rate = 8, amount = 1.0 + (8 - (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 6.332871561448645 + """ + default_conf_usdt['liquidation_buffer'] = liquidation_buffer + default_conf_usdt['trading_mode'] = trading_mode + default_conf_usdt['exchange']['name'] = exchange_name + default_conf_usdt['margin_mode'] = margin_mode + mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes') + exchange = get_patched_exchange(mocker, default_conf_usdt) + + exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) + exchange.name = exchange_name + # default_conf_usdt.update({ + # "dry_run": False, + # }) + liq = exchange.get_liquidation_price( + pair='ETH/USDT:USDT', + open_rate=open_rate, + amount=amount, + leverage=leverage, + is_short=is_short, + ) + if expected_liq is None: + assert liq is None + else: + buffer_amount = liquidation_buffer * abs(open_rate - expected_liq) + expected_liq = expected_liq - buffer_amount if is_short else expected_liq + buffer_amount + isclose(expected_liq, liq) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 7644385a5..da8751566 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -562,6 +562,71 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert trade assert trade.stake_amount == 300.0 + +def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: + default_conf_usdt['use_sell_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=100) + patch_exchange(mocker) + default_conf_usdt['stake_amount'] = 300 + default_conf_usdt['max_open_trades'] = 2 + default_conf_usdt['trading_mode'] = 'futures' + default_conf_usdt['margin_mode'] = 'isolated' + default_conf_usdt['stake_currency'] = 'USDT' + default_conf_usdt['exchange']['pair_whitelist'] = ['.*'] + backtesting = Backtesting(default_conf_usdt) + backtesting._set_strategy(backtesting.strategylist[0]) + pair = 'UNITTEST/USDT:USDT' + row = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 1, # Buy + 0.001, # Open + 0.0011, # Close + 0, # Sell + 0.00099, # Low + 0.0012, # High + '', # Buy Signal Name + ] + + backtesting.strategy.leverage = MagicMock(return_value=5.0) + mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", + return_value=(0.01, 0.01)) + + # leverage = 5 + # ep1(trade.open_rate) = 0.001 + # position(trade.amount) = 1500000 + # stake_amount = 300 -> wb = 300 / 5 = 60 + # mmr = 0.01 + # cum_b = 0.01 + # side_1: -1 if is_short else 1 + # liq_buffer = 0.05 + # + # Binance, Long + # liquidation_price + # = ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + # = ((300 + 0.01) - (1 * 1500000 * 0.001)) / ((1500000 * 0.01) - (1 * 1500000)) + # = 0.0008080740740740741 + # freqtrade_liquidation_price = liq + (abs(open_rate - liq) * liq_buffer * side_1) + # = 0.0008080740740740741 + ((0.001 - 0.0008080740740740741) * 0.05 * 1) + # = 0.0008176703703703704 + + trade = backtesting._enter_trade(pair, row=row, direction='long') + assert pytest.approx(trade.isolated_liq) == 0.00081767037 + + # Binance, Short + # liquidation_price + # = ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + # = ((300 + 0.01) - ((-1) * 1500000 * 0.001)) / ((1500000 * 0.01) - ((-1) * 1500000)) + # = 0.0011881254125412541 + # freqtrade_liquidation_price = liq + (abs(open_rate - liq) * liq_buffer * side_1) + # = 0.0011881254125412541 + (abs(0.001 - 0.0011881254125412541) * 0.05 * -1) + # = 0.0011787191419141915 + + trade = backtesting._enter_trade(pair, row=row, direction='short') + assert pytest.approx(trade.isolated_liq) == 0.0011787191 + # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d6930bc24..cc844963d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4839,132 +4839,6 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd < proposed_price -@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05]) -@pytest.mark.parametrize( - "is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [ - (False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), - (True, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), - (False, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), - (True, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), - (False, 'spot', 'okx', '', 5.0, 10.0, 1.0, None), - (True, 'spot', 'okx', '', 5.0, 10.0, 1.0, None), - # Binance, short - (True, 'futures', 'binance', 'isolated', 5.0, 10.0, 1.0, 11.89108910891089), - (True, 'futures', 'binance', 'isolated', 3.0, 10.0, 1.0, 13.211221122079207), - (True, 'futures', 'binance', 'isolated', 5.0, 8.0, 1.0, 9.514851485148514), - (True, 'futures', 'binance', 'isolated', 5.0, 10.0, 0.6, 12.557755775577558), - # Binance, long - (False, 'futures', 'binance', 'isolated', 5, 10, 1.0, 8.070707070707071), - (False, 'futures', 'binance', 'isolated', 5, 8, 1.0, 6.454545454545454), - (False, 'futures', 'binance', 'isolated', 3, 10, 1.0, 6.717171717171718), - (False, 'futures', 'binance', 'isolated', 5, 10, 0.6, 7.39057239057239), - # Gateio/okx, short - (True, 'futures', 'gateio', 'isolated', 5, 10, 1.0, 11.87413417771621), - (True, 'futures', 'gateio', 'isolated', 5, 10, 2.0, 11.87413417771621), - (True, 'futures', 'gateio', 'isolated', 3, 10, 1.0, 13.476180850346978), - (True, 'futures', 'gateio', 'isolated', 5, 8, 1.0, 9.499307342172967), - # Gateio/okx, long - (False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), - (False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), - # (True, 'futures', 'okx', 'isolated', 11.87413417771621), - # (False, 'futures', 'okx', 'isolated', 8.085708510208207), - ] -) -def test_leverage_prep( - mocker, - default_conf_usdt, - is_short, - trading_mode, - exchange_name, - margin_mode, - leverage, - open_rate, - amount, - expected_liq, - liquidation_buffer, -): - """ - position = 0.2 * 5 - wb: wallet balance (stake_amount if isolated) - cum_b: maintenance amount - side_1: -1 if is_short else 1 - ep1: entry price - mmr_b: maintenance margin ratio - - Binance, Short - leverage = 5, open_rate = 10, amount = 1.0 - ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) - ((2 + 0.01) - ((-1) * 1 * 10)) / ((1 * 0.01) - ((-1) * 1)) = 11.89108910891089 - leverage = 3, open_rate = 10, amount = 1.0 - ((3.3333333333 + 0.01) - ((-1) * 1.0 * 10)) / ((1.0 * 0.01) - ((-1) * 1.0)) = 13.2112211220 - leverage = 5, open_rate = 8, amount = 1.0 - ((1.6 + 0.01) - ((-1) * 1 * 8)) / ((1 * 0.01) - ((-1) * 1)) = 9.514851485148514 - leverage = 5, open_rate = 10, amount = 0.6 - ((1.6 + 0.01) - ((-1) * 0.6 * 10)) / ((0.6 * 0.01) - ((-1) * 0.6)) = 12.557755775577558 - - Binance, Long - leverage = 5, open_rate = 10, amount = 1.0 - ((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 - leverage = 5, open_rate = 8, amount = 1.0 - ((1.6 + 0.01) - (1 * 1 * 8)) / ((1 * 0.01) - (1 * 1)) = 6.454545454545454 - leverage = 3, open_rate = 10, amount = 1.0 - ((2 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 6.717171717171718 - leverage = 5, open_rate = 10, amount = 0.6 - ((1.6 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 7.39057239057239 - - Gateio/Okx, Short - leverage = 5, open_rate = 10, amount = 1.0 - (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) - (10 + (2 / 1.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 - leverage = 5, open_rate = 10, amount = 2.0 - (10 + (4 / 2.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 - leverage = 3, open_rate = 10, amount = 1.0 - (10 + (3.3333333333333 / 1.0)) / (1 - (0.01 + 0.0006)) = 13.476180850346978 - leverage = 5, open_rate = 8, amount = 1.0 - (8 + (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 9.499307342172967 - - Gateio/Okx, Long - leverage = 5, open_rate = 10, amount = 1.0 - (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) - (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 - leverage = 5, open_rate = 10, amount = 2.0 - (10 - (4 / 2.0)) / (1 + (0.01 + 0.0006)) = 7.916089451810806 - leverage = 3, open_rate = 10, amount = 1.0 - (10 - (3.333333333333333333 / 1.0)) / (1 - (0.01 + 0.0006)) = 6.738090425173506 - leverage = 5, open_rate = 8, amount = 1.0 - (8 - (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 6.332871561448645 - """ - default_conf_usdt['liquidation_buffer'] = liquidation_buffer - default_conf_usdt['trading_mode'] = trading_mode - default_conf_usdt['exchange']['name'] = exchange_name - default_conf_usdt['margin_mode'] = margin_mode - mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes') - patch_RPCManager(mocker) - patch_exchange(mocker, id=exchange_name) - freqtrade = FreqtradeBot(default_conf_usdt) - - freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) - freqtrade.exchange.name = exchange_name - # default_conf_usdt.update({ - # "dry_run": False, - # }) - (interest, liq) = freqtrade.leverage_prep( - pair='ETH/USDT:USDT', - open_rate=open_rate, - amount=amount, - leverage=leverage, - is_short=is_short, - ) - assert interest == 0.0 - if expected_liq is None: - assert liq is None - else: - buffer_amount = liquidation_buffer * abs(open_rate - expected_liq) - expected_liq = expected_liq - buffer_amount if is_short else expected_liq + buffer_amount - isclose(expected_liq, liq) - - @pytest.mark.parametrize('trading_mode,calls,t1,t2', [ ('spot', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), ('margin', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),