From 6dbd249570d07d7519efbfc3e2e08a8e40e1a5c5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Feb 2022 10:07:00 -0600 Subject: [PATCH 01/19] backtesting._enter_trade get liquidation_price and backtesting._leverage_prep --- freqtrade/optimize/backtesting.py | 50 +++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0e3a70a93..3b26b4de6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -591,6 +591,42 @@ 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]: @@ -666,6 +702,14 @@ class Backtesting: amount = round((stake_amount / propose_rate) * leverage, 8) if trade is None: # Enter trade + is_short = (direction == 'short') + (interest_rate, isolated_liq) = self._leverage_prep( + pair=pair, + open_rate=propose_rate, + amount=amount, + leverage=leverage, + is_short=is_short, + ) self.trade_id_counter += 1 trade = LocalTrade( id=self.trade_id_counter, @@ -682,10 +726,12 @@ 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, + isolated_liq=isolated_liq, + orders=[], ) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) From 8af2ea754fe951ced3d592caace57a248d79dbb9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Feb 2022 14:11:21 -0600 Subject: [PATCH 02/19] add margin mode to backtesting --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3b26b4de6..4f6dec7df 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('trading_mode', MarginMode.NONE) self._can_short = self.trading_mode != TradingMode.SPOT self.progress = BTProgress() From 6fdcc714bfabb39877e3fdd7bfdda91a55715913 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 27 Feb 2022 11:59:27 -0600 Subject: [PATCH 03/19] backtesting margin_mode key fix --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4f6dec7df..1bda42ee0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -130,7 +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('trading_mode', MarginMode.NONE) + self.margin_mode: MarginMode = config.get('margin_mode', MarginMode.NONE) self._can_short = self.trading_mode != TradingMode.SPOT self.progress = BTProgress() From b103045a0598e8a02d801a3005aa5ee8da7e93c9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 27 Feb 2022 12:08:39 -0600 Subject: [PATCH 04/19] backtesting._enter_trade update liquidation price on increased position --- freqtrade/optimize/backtesting.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1bda42ee0..82b576609 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -701,16 +701,16 @@ class Backtesting: 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') + (interest_rate, isolated_liq) = self._leverage_prep( + pair=pair, + open_rate=propose_rate, + amount=amount, + leverage=leverage, + is_short=is_short, + ) if trade is None: # Enter trade - is_short = (direction == 'short') - (interest_rate, isolated_liq) = self._leverage_prep( - pair=pair, - open_rate=propose_rate, - amount=amount, - leverage=leverage, - is_short=is_short, - ) self.trade_id_counter += 1 trade = LocalTrade( id=self.trade_id_counter, @@ -737,6 +737,13 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) + if self.trading_mode == TradingMode.FUTURES: + if isolated_liq is None: + raise OperationalException( + f'isolated_liq is none for {pair} while trading futures, ' + 'this should never happen') + trade.set_isolated_liq(isolated_liq) + order = Order( id=self.order_id_counter, ft_trade_id=trade.id, From 1121965c6e8363b08337b519de98e9b57c196e6e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 27 Feb 2022 14:28:28 -0600 Subject: [PATCH 05/19] liq backtesting tests --- tests/optimize/test_backtesting.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 7644385a5..e9732171e 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -10,6 +10,7 @@ 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 @@ -562,6 +563,30 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert trade assert trade.stake_amount == 300.0 + 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) = 60000 + # stake_amount = 300 -> wb = 300 / 5 = 60 + # mmr = 0.01 + # cum_b = 0.01 + # side_1: -1 if is_short else 1 + # + # Binance, Short + # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + # ((60 + 0.01) - ((-1) * 60000 * 0.001)) / ((60000 * 0.01) - ((-1) * 60000)) = 0.00198036303630 + trade = backtesting._enter_trade(pair, row=row, direction='long') + assert isclose(trade.isolated_liq, 0.0019803630363036304) + + # Binance, Long + # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + # ((60 + 0.01) - (1 * 60000 * 0.001)) / ((60000 * 0.01) - (1 * 60000)) = -1.6835016835013486e-07 + trade = backtesting._enter_trade(pair, row=row, direction='short') + assert isclose(trade.isolated_liq, -1.6835016835013486e-07) + # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) From 8e2d3445a771190ae6fa497393dc6ec8e7956257 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:27:48 +0100 Subject: [PATCH 06/19] 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"), From 1d27cbd01f1fff08f5187bc052c56d12c84304a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:34:10 +0100 Subject: [PATCH 07/19] Simplify leverage_prep interface --- freqtrade/optimize/backtesting.py | 5 ----- freqtrade/persistence/models.py | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index eae398010..bde887998 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -695,17 +695,12 @@ class Backtesting: trading_mode=self.trading_mode, leverage=leverage, interest_rate=interest_rate, - isolated_liq=isolated_liq, orders=[], ) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) if self.trading_mode == TradingMode.FUTURES: - if isolated_liq is None: - raise OperationalException( - f'isolated_liq is none for {pair} while trading futures, ' - 'this should never happen') trade.set_isolated_liq(isolated_liq) order = Order( diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 18491d687..2c96248d3 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) From ab46476e638d0e60b222f61e8e18cf24ef686da2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:42:26 +0100 Subject: [PATCH 08/19] Rename get_liquidation method --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67ebc6dd6..bef2f766e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2077,7 +2077,7 @@ class Exchange: self.trading_mode == TradingMode.FUTURES ): wallet_balance = (amount * open_rate) / leverage - isolated_liq = self.get_liquidation_price( + isolated_liq = self.get_or_calculate_liquidation_price( pair=pair, open_rate=open_rate, is_short=is_short, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 211dd6654..f937038e0 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, From 79538368dbe0ced14a21a74a9122c6d90f3c9a69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:45:15 +0100 Subject: [PATCH 09/19] Simplify liquidation price calculation --- freqtrade/exchange/exchange.py | 24 ++++++++++++------------ freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 7 +++++-- tests/exchange/test_exchange.py | 5 ++--- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bef2f766e..4fd0b86d2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2055,23 +2055,23 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def leverage_prep( + def get_interest_rate(self) -> float: + """ + Calculate interest rate - necessary for Margin trading. + """ + return 0.0 + + def get_liquidation_price( self, pair: str, open_rate: float, amount: float, # quote currency, includes leverage leverage: float, is_short: bool - ) -> Tuple[float, Optional[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) + if self.trading_mode in (TradingMode.SPOT, TradingMode.MARGIN): + return None elif ( self.margin_mode == MarginMode.ISOLATED and self.trading_mode == TradingMode.FUTURES @@ -2086,7 +2086,7 @@ class Exchange: mm_ex_1=0.0, upnl_ex_1=0.0, ) - return (0.0, isolated_liq) + return isolated_liq else: raise OperationalException( "Freqtrade only supports isolated futures for leverage trading") @@ -2231,7 +2231,7 @@ class Exchange: return 0.0 @retrier - def get_liquidation_price( + def get_or_calculate_liquidation_price( self, pair: str, # Dry-run diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 70540c398..8f38ab8aa 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 @@ -688,13 +688,14 @@ 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.exchange.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') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bde887998..6d562b6ab 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -666,13 +666,16 @@ class Backtesting: self.order_id_counter += 1 amount = round((stake_amount / propose_rate) * leverage, 8) is_short = (direction == 'short') - (interest_rate, isolated_liq) = self.exchange.leverage_prep( + isolated_liq = self.exchange.get_liquidation_price( pair=pair, open_rate=propose_rate, amount=amount, leverage=leverage, is_short=is_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 @@ -694,7 +697,7 @@ class Backtesting: is_short=is_short, trading_mode=self.trading_mode, leverage=leverage, - interest_rate=interest_rate, + # interest_rate=interest_rate, orders=[], ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f937038e0..fadcacc53 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4560,7 +4560,7 @@ def test__get_params(mocker, default_conf, exchange_name): # (False, 'futures', 'okx', 'isolated', 8.085708510208207), ] ) -def test_leverage_prep( +def test_get_liquidation_price( mocker, default_conf_usdt, is_short, @@ -4637,14 +4637,13 @@ def test_leverage_prep( # default_conf_usdt.update({ # "dry_run": False, # }) - (interest, liq) = exchange.leverage_prep( + liq = exchange.get_liquidation_price( 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: From c39e7368ee2e7820f82ab9cded544d3762912c1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:58:44 +0100 Subject: [PATCH 10/19] Split backtesting test to properly initialize it --- tests/optimize/test_backtesting.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 56d4571d8..c342dadcc 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -21,6 +21,7 @@ from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.enums import RunMode, SellType +from freqtrade.enums.tradingmode import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.misc import get_strategy_run_id @@ -563,6 +564,33 @@ 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')) + patch_exchange(mocker) + default_conf_usdt['stake_amount'] = 'unlimited' + default_conf_usdt['max_open_trades'] = 2 + default_conf_usdt['trading_mode'] = 'futures' + default_conf_usdt['margin_mode'] = 'isolated' + default_conf_usdt['stake_currency'] = 'BUSD' + default_conf_usdt['exchange']['pair_whitelist'] = ['.*'] + backtesting = Backtesting(default_conf_usdt) + backtesting._set_strategy(backtesting.strategylist[0]) + pair = 'UNITTEST/BTC' + 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)) From c745f5828c533a350af692814e575e766eec43cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 20:05:14 +0100 Subject: [PATCH 11/19] Update comments to clarify it's supposed to be a "offline" call --- freqtrade/exchange/exchange.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4fd0b86d2..a47cf065a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2307,6 +2307,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 @@ -2350,6 +2351,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 From e8206bc75190d742c3882ad5749ae6fa5b7a1f37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 20:10:23 +0100 Subject: [PATCH 12/19] Simplify backtesting enter_Trade --- freqtrade/optimize/backtesting.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 365d8be37..fa3deb86f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -639,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) @@ -652,30 +654,20 @@ 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') - isolated_liq = self.exchange.get_liquidation_price( - pair=pair, - open_rate=propose_rate, - amount=amount, - leverage=leverage, - is_short=is_short, - ) # Necessary for Margin trading. Disabled until support is enabled. # interest_rate = self.exchange.get_interest_rate() @@ -706,8 +698,13 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - if self.trading_mode == TradingMode.FUTURES: - trade.set_isolated_liq(isolated_liq) + 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, From 736a9301526b091412540809693a0ea103922ab7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Mar 2022 19:23:14 +0100 Subject: [PATCH 13/19] Update small things --- freqtrade/exchange/exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a47cf065a..0f693036e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2057,7 +2057,8 @@ class Exchange: def get_interest_rate(self) -> float: """ - Calculate interest rate - necessary for Margin trading. + Retrieve interest rate - necessary for Margin trading. + Should not call the exchange directly when used from backtesting. """ return 0.0 @@ -2070,7 +2071,7 @@ class Exchange: is_short: bool ) -> Optional[float]: - if self.trading_mode in (TradingMode.SPOT, TradingMode.MARGIN): + if self.trading_mode in TradingMode.SPOT: return None elif ( self.margin_mode == MarginMode.ISOLATED and From 478d440e80e36e3672d5df0777453434405983e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Mar 2022 07:00:07 +0100 Subject: [PATCH 14/19] Test backtesting with USDT pairs --- tests/conftest.py | 4 ++-- tests/optimize/test_backtesting.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 1337f1fa6..486d4e0bc 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -574,11 +574,11 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: default_conf_usdt['max_open_trades'] = 2 default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt['margin_mode'] = 'isolated' - default_conf_usdt['stake_currency'] = 'BUSD' + 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/BTC' + pair = 'UNITTEST/USDT:USDT' row = [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), 1, # Buy @@ -606,13 +606,13 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) # ((60 + 0.01) - ((-1) * 60000 * 0.001)) / ((60000 * 0.01) - ((-1) * 60000)) = 0.00198036303630 trade = backtesting._enter_trade(pair, row=row, direction='long') - assert isclose(trade.isolated_liq, 0.0019803630363036304) + assert pytest.approx(trade.isolated_liq) == 0.0019803630363036304 # Binance, Long # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) # ((60 + 0.01) - (1 * 60000 * 0.001)) / ((60000 * 0.01) - (1 * 60000)) = -1.6835016835013486e-07 trade = backtesting._enter_trade(pair, row=row, direction='short') - assert isclose(trade.isolated_liq, -1.6835016835013486e-07) + assert pytest.approx(trade.isolated_liq) == -1.6835016835013486e-07 # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) From 1c4a7c25d7dce43e258a2918faffe9d93f3a07ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Mar 2022 07:14:36 +0100 Subject: [PATCH 15/19] Fix failing test --- tests/optimize/test_backtesting.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 486d4e0bc..7448739db 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -569,8 +569,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: 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'] = 'unlimited' + 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' @@ -602,17 +603,20 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: # cum_b = 0.01 # side_1: -1 if is_short else 1 # - # Binance, Short - # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) - # ((60 + 0.01) - ((-1) * 60000 * 0.001)) / ((60000 * 0.01) - ((-1) * 60000)) = 0.00198036303630 - trade = backtesting._enter_trade(pair, row=row, direction='long') - assert pytest.approx(trade.isolated_liq) == 0.0019803630363036304 - # Binance, Long # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) - # ((60 + 0.01) - (1 * 60000 * 0.001)) / ((60000 * 0.01) - (1 * 60000)) = -1.6835016835013486e-07 + # ((300 + 0.01) - (1 * 150000 * 0.001)) / ((150000 * 0.01) - (1 * 150000)) = -0.00101016835 + # TODO-lev: is the above formula correct? + # The values inserted above seem correct, but the result is different. + trade = backtesting._enter_trade(pair, row=row, direction='long') + assert pytest.approx(trade.isolated_liq) == 0.00081767037 + + # Binance, Short + # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + # ((300 + 0.01) - ((-1) * 150000 * 0.001)) / ((150000 * 0.01) - ((-1) * 150000)) = 0.002970363 + trade = backtesting._enter_trade(pair, row=row, direction='short') - assert pytest.approx(trade.isolated_liq) == -1.6835016835013486e-07 + 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) From c0fb6f7e85a6377ee38be6b2bb1c6c001f70ecde Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Mar 2022 01:26:47 -0600 Subject: [PATCH 16/19] test_backtest__enter_trade_futures - fixed formula in comments --- tests/optimize/test_backtesting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 7448739db..094aa5343 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -597,7 +597,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: # leverage = 5 # ep1(trade.open_rate) = 0.001 - # position(trade.amount) = 60000 + # position(trade.amount) = 1500000 # stake_amount = 300 -> wb = 300 / 5 = 60 # mmr = 0.01 # cum_b = 0.01 @@ -605,15 +605,15 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: # # Binance, Long # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) - # ((300 + 0.01) - (1 * 150000 * 0.001)) / ((150000 * 0.01) - (1 * 150000)) = -0.00101016835 - # TODO-lev: is the above formula correct? - # The values inserted above seem correct, but the result is different. + # ((300 + 0.01) - (1 * 1500000 * 0.001)) / ((1500000 * 0.01) - (1 * 1500000)) + # = 0.0008080740740740741 trade = backtesting._enter_trade(pair, row=row, direction='long') assert pytest.approx(trade.isolated_liq) == 0.00081767037 # Binance, Short # ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) - # ((300 + 0.01) - ((-1) * 150000 * 0.001)) / ((150000 * 0.01) - ((-1) * 150000)) = 0.002970363 + # ((300 + 0.01) - ((-1) * 1500000 * 0.001)) / ((1500000 * 0.01) - ((-1) * 1500000)) + # = 0.0011881254125412541 trade = backtesting._enter_trade(pair, row=row, direction='short') assert pytest.approx(trade.isolated_liq) == 0.0011787191 From c0e11becedbc6f41f9bfb7f304938a2fb168e513 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Mar 2022 01:30:52 -0600 Subject: [PATCH 17/19] linting --- tests/optimize/test_backtesting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 094aa5343..c11a1dc55 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -3,7 +3,6 @@ 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 From c9988e0aa23eb13673e58e20448f70050e716954 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Mar 2022 12:46:31 -0600 Subject: [PATCH 18/19] test_backtest__enter_trade_futures comment calculations include liquidation buffer --- tests/optimize/test_backtesting.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c11a1dc55..da8751566 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -601,18 +601,28 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: # mmr = 0.01 # cum_b = 0.01 # side_1: -1 if is_short else 1 + # liq_buffer = 0.05 # # Binance, Long - # ((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 + # 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 - # ((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 + # 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 From 8a9c6e27a537978f5a3ba8a10f56e3f52004b621 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Mar 2022 12:53:24 -0600 Subject: [PATCH 19/19] docs/leverage.md: Added freqtrade_liquidation_price formula to docs --- docs/leverage.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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