From 8e2d3445a771190ae6fa497393dc6ec8e7956257 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:27:48 +0100 Subject: [PATCH] Move leverage_prep calculations to exchange class --- freqtrade/exchange/exchange.py | 36 +++++++++ freqtrade/freqtradebot.py | 38 +-------- freqtrade/optimize/backtesting.py | 38 +-------- tests/exchange/test_exchange.py | 124 ++++++++++++++++++++++++++++ tests/optimize/test_backtesting.py | 2 +- tests/test_freqtradebot.py | 126 ----------------------------- 6 files changed, 163 insertions(+), 201 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 092420eab..67ebc6dd6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2055,6 +2055,42 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + 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.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.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 funding_fee_cutoff(self, open_date: datetime): """ :param open_date: The open date for a trade diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index abd38859b..70540c398 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -578,42 +578,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, @@ -724,7 +688,7 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - interest_rate, isolated_liq = self.leverage_prep( + interest_rate, isolated_liq = self.exchange.leverage_prep( leverage=leverage, pair=pair, amount=amount, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 82b576609..eae398010 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -592,42 +592,6 @@ class Backtesting: else: return self._get_sell_trade_entry_for_candle(trade, sell_row) - 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 _enter_trade(self, pair: str, row: Tuple, direction: str, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: @@ -702,7 +666,7 @@ class Backtesting: self.order_id_counter += 1 amount = round((stake_amount / propose_rate) * leverage, 8) is_short = (direction == 'short') - (interest_rate, isolated_liq) = self._leverage_prep( + (interest_rate, isolated_liq) = self.exchange.leverage_prep( pair=pair, open_rate=propose_rate, amount=amount, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 894f5b75b..211dd6654 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4527,3 +4527,127 @@ 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_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') + 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, + # }) + (interest, liq) = exchange.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) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index e9732171e..56d4571d8 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -3,6 +3,7 @@ import random from copy import deepcopy from datetime import datetime, timedelta, timezone +from math import isclose from pathlib import Path from unittest.mock import MagicMock, PropertyMock @@ -10,7 +11,6 @@ import numpy as np import pandas as pd import pytest from arrow import Arrow -from math import isclose from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting 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"),