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
9 changed files with 277 additions and 190 deletions

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)