Merge pull request #6467 from samgermain/backtest-liq
Liquidation price in backtesting
This commit is contained in:
commit
f558d4b132
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"),
|
||||
|
Loading…
Reference in New Issue
Block a user