From e5d68f12d20d9f00b84e3f47fcf77f2572dad1d7 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 10 Feb 2022 09:58:30 -0600 Subject: [PATCH 1/7] Added liquidation_buffer to freqtradebot --- freqtrade/freqtradebot.py | 10 +++++++++- tests/test_freqtradebot.py | 33 +++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 35df38e69..70b8e419d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -103,8 +103,8 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + self.liquidation_buffer = float(self.config.get('liquidation_buffer', '0.05')) self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot')) - self.margin_mode_type: Optional[MarginMode] = None if 'margin_mode' in self.config: self.margin_mode = MarginMode(self.config['margin_mode']) @@ -758,6 +758,14 @@ class FreqtradeBot(LoggingMixin): funding_fees = self.exchange.get_funding_fees( pair=pair, amount=amount, is_short=is_short, open_date=open_date) # This is a new trade + if isolated_liq: + liquidation_buffer = abs(enter_limit_filled_price - + isolated_liq) * self.liquidation_buffer + isolated_liq = ( + isolated_liq - liquidation_buffer + if is_short else + isolated_liq + liquidation_buffer + ) if trade is None: trade = Trade( pair=pair, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d8ebba8f9..da7e8692d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -707,23 +707,27 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) CandleType.SPOT) in refresh_mock.call_args[0][0] -@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [ - (False, 'spot', 'binance', None, None), - (True, 'spot', 'binance', None, None), - (False, 'spot', 'gateio', None, None), - (True, 'spot', 'gateio', None, None), - (False, 'spot', 'okx', None, None), - (True, 'spot', 'okx', None, None), - (True, 'futures', 'binance', 'isolated', 11.89108910891089), - (False, 'futures', 'binance', 'isolated', 8.070707070707071), - (True, 'futures', 'gateio', 'isolated', 11.87413417771621), - (False, 'futures', 'gateio', 'isolated', 8.085708510208207), - # (True, 'futures', 'okx', 'isolated', 11.87413417771621), - # (False, 'futures', 'okx', 'isolated', 8.085708510208207), +@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_buffer,liq_price", [ + (False, 'spot', 'binance', None, 0.0, None), + (True, 'spot', 'binance', None, 0.0, None), + (False, 'spot', 'gateio', None, 0.0, None), + (True, 'spot', 'gateio', None, 0.0, None), + (False, 'spot', 'okx', None, 0.0, None), + (True, 'spot', 'okx', None, 0.0, None), + (True, 'futures', 'binance', 'isolated', 0.0, 11.89108910891089), + (False, 'futures', 'binance', 'isolated', 0.0, 8.070707070707071), + (True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621), + (False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207), + (True, 'futures', 'binance', 'isolated', 0.05, 11.796534653465345), + (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), ]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode, - exchange_name, margin_mode, liq_price) -> None: + exchange_name, margin_mode, liq_buffer, liq_price) -> None: """ exchange_name = binance, is_short = true leverage = 5 @@ -747,6 +751,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, open_order = limit_order_open[enter_side(is_short)] order = limit_order[enter_side(is_short)] default_conf_usdt['trading_mode'] = trading_mode + default_conf_usdt['liquidation_buffer'] = liq_buffer leverage = 1.0 if trading_mode == 'spot' else 5.0 default_conf_usdt['exchange']['name'] = exchange_name if margin_mode: From 305d3738d9b1b11c051cf580a1e3a936095e2f4c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 10 Feb 2022 21:02:05 -0600 Subject: [PATCH 2/7] Documentation for liquidation_buffer --- docs/leverage.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/leverage.md b/docs/leverage.md index e8810fbb2..d84a572e2 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -15,7 +15,7 @@ ## Understand `trading_mode` -The possible values are: `spot` (default), `margin`(*coming soon*) or `futures`. +The possible values are: `spot` (default), `margin`(*Currently unavailable*) or `futures`. ### Spot @@ -69,6 +69,18 @@ One account is used to share collateral between markets (trading pairs). Margin "margin_mode": "cross" ``` +## Understand `liquidation_buffer` +*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 + +Possible values are any floats between 0.0 and 0.99 + +**ex:** If a trade is entered at a price of 10 coin/USDT, and the liquidation price of this trade is 8 coin/USDT, then with `liquidation_buffer` set to `0.05` the minimum stoploss for this trade would be 8 + ((10 - 8) * 0.05) = 8 + 0.1 = 8.1 + +!!! Danger "A `liquidation_buffer` of 0.0, or a low `liquidation_buffer` is likely to result in liquidations, and liquidation fees" +Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in accurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange`. + ### Developer #### Margin mode @@ -82,3 +94,4 @@ All Fees are included in `current_profit` calculations during the trade. #### FUTURES MODE Funding fees are either added or subtracted from the total amount of a trade + From 3c3675ea1ae0722f532f4505680e3c44edb30c94 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 10 Feb 2022 21:14:07 -0600 Subject: [PATCH 3/7] moved liquidation_buffer to exchange class, add check for valid liquidation_buffer values --- docs/configuration.md | 1 + freqtrade/exchange/exchange.py | 22 +++++++++++++++++++--- freqtrade/freqtradebot.py | 8 -------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7ca910431..7a42966b0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,6 +101,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String +| `liquidation_buffer` | 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 [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float | `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0434c3b9c..a5fc85f03 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -135,13 +135,18 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] + # Leverage properties self.trading_mode = TradingMode(config.get('trading_mode', 'spot')) - self.margin_mode: Optional[MarginMode] = ( MarginMode(config.get('margin_mode')) if config.get('margin_mode') else None ) + self.liquidation_buffer = config.get('liquidation_buffer', 0.05) + if self.liquidation_buffer < 0.0: + raise OperationalException('Cannot have a negative liquidation_buffer') + if self.liquidation_buffer > 0.99: + raise OperationalException('Liquidation_buffer must be below 0.99') # Initialize ccxt objects ccxt_config = self._ccxt_config @@ -2062,7 +2067,7 @@ class Exchange: if self._config['dry_run'] or not self.exchange_has("fetchPositions"): - return self.dry_run_liquidation_price( + isolated_liq = self.dry_run_liquidation_price( pair=pair, open_rate=open_rate, is_short=is_short, @@ -2076,7 +2081,7 @@ class Exchange: positions = self._api.fetch_positions([pair]) if len(positions) > 0: pos = positions[0] - return pos['liquidationPrice'] + isolated_liq = pos['liquidationPrice'] else: return None except ccxt.DDoSProtection as e: @@ -2087,6 +2092,17 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + if isolated_liq: + buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer + isolated_liq = ( + isolated_liq - buffer_amount + if is_short else + isolated_liq + buffer_amount + ) + return isolated_liq + else: + return None + def get_maintenance_ratio_and_amt( self, pair: str, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 70b8e419d..0906276f9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -758,14 +758,6 @@ class FreqtradeBot(LoggingMixin): funding_fees = self.exchange.get_funding_fees( pair=pair, amount=amount, is_short=is_short, open_date=open_date) # This is a new trade - if isolated_liq: - liquidation_buffer = abs(enter_limit_filled_price - - isolated_liq) * self.liquidation_buffer - isolated_liq = ( - isolated_liq - liquidation_buffer - if is_short else - isolated_liq + liquidation_buffer - ) if trade is None: trade = Trade( pair=pair, From fb3a6e2ce81adb453e277218a5314dee1b4662f7 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 11 Feb 2022 00:43:45 -0600 Subject: [PATCH 4/7] added liquidation_buffer to constants.py --- freqtrade/constants.py | 1 + freqtrade/exchange/exchange.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f5dae9473..7e3f4374c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,6 +156,7 @@ CONF_SCHEMA = { 'ignore_buying_expired_candle_after': {'type': 'number'}, 'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, 'margin_mode': {'type': 'string', 'enum': MARGIN_MODES}, + 'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99}, 'backtest_breakdown': { 'type': 'array', 'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a5fc85f03..acd0e3ea0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -143,10 +143,6 @@ class Exchange: else None ) self.liquidation_buffer = config.get('liquidation_buffer', 0.05) - if self.liquidation_buffer < 0.0: - raise OperationalException('Cannot have a negative liquidation_buffer') - if self.liquidation_buffer > 0.99: - raise OperationalException('Liquidation_buffer must be below 0.99') # Initialize ccxt objects ccxt_config = self._ccxt_config From 6ae85f9be1dee16a53095465c5b87581a13b9ba0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 11 Feb 2022 03:48:09 -0600 Subject: [PATCH 5/7] fixed liq-buffer tests --- freqtrade/exchange/exchange.py | 30 +++++++++++++++--------------- tests/exchange/test_exchange.py | 13 +++++++++++++ tests/test_freqtradebot.py | 5 +++++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index acd0e3ea0..90b63b57b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2072,21 +2072,21 @@ class Exchange: mm_ex_1=mm_ex_1, upnl_ex_1=upnl_ex_1 ) - - try: - positions = self._api.fetch_positions([pair]) - if len(positions) > 0: - pos = positions[0] - isolated_liq = pos['liquidationPrice'] - else: - return None - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + else: + try: + positions = self._api.fetch_positions([pair]) + if len(positions) > 0: + pos = positions[0] + isolated_liq = pos['liquidationPrice'] + else: + return None + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e if isolated_liq: buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f5d0d0fd2..220fd04e6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3664,6 +3664,7 @@ def test_get_liquidation_price(mocker, default_conf): default_conf['dry_run'] = False default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' + default_conf['liquidation_buffer'] = 0.0 exchange = get_patched_exchange(mocker, default_conf, api_mock) liq_price = exchange.get_liquidation_price( @@ -3675,6 +3676,17 @@ def test_get_liquidation_price(mocker, default_conf): ) assert liq_price == 17.47 + default_conf['liquidation_buffer'] = 0.05 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + liq_price = exchange.get_liquidation_price( + pair='NEAR/USDT:USDT', + open_rate=0.0, + is_short=False, + position=0.0, + wallet_balance=0.0, + ) + assert liq_price == 18.8133 + ccxt_exceptionhandlers( mocker, default_conf, @@ -4073,6 +4085,7 @@ def test_liquidation_price( ): default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = margin_mode + 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( diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index da7e8692d..1442186ea 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4813,6 +4813,7 @@ 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), @@ -4854,6 +4855,7 @@ def test_leverage_prep( open_rate, amount, expected_liq, + liquidation_buffer, ): """ position = 0.2 * 5 @@ -4907,6 +4909,7 @@ def test_leverage_prep( 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 @@ -4931,6 +4934,8 @@ def test_leverage_prep( 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) From 7a79403d2c077ade66b53951faec3e8723e6fbe7 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 11 Feb 2022 00:45:00 -0600 Subject: [PATCH 6/7] Update docs/leverage.md Co-authored-by: Matthias --- docs/leverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/leverage.md b/docs/leverage.md index d84a572e2..de0b0a981 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -79,7 +79,7 @@ Possible values are any floats between 0.0 and 0.99 **ex:** If a trade is entered at a price of 10 coin/USDT, and the liquidation price of this trade is 8 coin/USDT, then with `liquidation_buffer` set to `0.05` the minimum stoploss for this trade would be 8 + ((10 - 8) * 0.05) = 8 + 0.1 = 8.1 !!! Danger "A `liquidation_buffer` of 0.0, or a low `liquidation_buffer` is likely to result in liquidations, and liquidation fees" -Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in accurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange`. +Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in inaccurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange` if your exchange supports this. ### Developer From 19a282ddb48e24c774fdeb88665fa298e9f7be6f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 11 Feb 2022 04:00:50 -0600 Subject: [PATCH 7/7] fixed broken test_get_liquidation_price --- tests/exchange/test_exchange.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 220fd04e6..5366bbf0c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3669,10 +3669,10 @@ def test_get_liquidation_price(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf, api_mock) liq_price = exchange.get_liquidation_price( pair='NEAR/USDT:USDT', - open_rate=0.0, + open_rate=18.884, is_short=False, - position=0.0, - wallet_balance=0.0, + position=0.8, + wallet_balance=0.8, ) assert liq_price == 17.47 @@ -3680,12 +3680,12 @@ def test_get_liquidation_price(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf, api_mock) liq_price = exchange.get_liquidation_price( pair='NEAR/USDT:USDT', - open_rate=0.0, + open_rate=18.884, is_short=False, - position=0.0, - wallet_balance=0.0, + position=0.8, + wallet_balance=0.8, ) - assert liq_price == 18.8133 + assert liq_price == 17.540699999999998 ccxt_exceptionhandlers( mocker,