Merge pull request #6467 from samgermain/backtest-liq

Liquidation price in backtesting
This commit is contained in:
Matthias 2022-03-03 06:50:32 +01:00 committed by GitHub
commit f558d4b132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 277 additions and 190 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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"),