Merge pull request #6537 from adrianceding/fs_fix

Add BT's leverage and short calculation
This commit is contained in:
Matthias 2022-03-16 19:25:19 +01:00 committed by GitHub
commit 84e9dc5001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 132 additions and 84 deletions

View File

@ -359,10 +359,25 @@ class Backtesting:
""" """
# Special handling if high or low hit STOP_LOSS or ROI # Special handling if high or low hit STOP_LOSS or ROI
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
if trade.stop_loss > sell_row[HIGH_IDX]: return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur)
# our stoploss was already higher than candle high, elif sell.sell_type == (SellType.ROI):
return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur)
else:
return sell_row[OPEN_IDX]
def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
trade_dur: int) -> float:
# our stoploss was already lower than candle high,
# possibly due to a cancelled trade exit. # possibly due to a cancelled trade exit.
# sell at open price. # sell at open price.
is_short = trade.is_short or False
leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1
if is_short:
if trade.stop_loss < sell_row[LOW_IDX]:
return sell_row[OPEN_IDX]
else:
if trade.stop_loss > sell_row[HIGH_IDX]:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
# Special case: trailing triggers within same candle as trade opened. Assume most # Special case: trailing triggers within same candle as trade opened. Assume most
@ -377,19 +392,32 @@ class Backtesting:
): ):
# Worst case: price reaches stop_positive_offset and dives down. # Worst case: price reaches stop_positive_offset and dives down.
stop_rate = (sell_row[OPEN_IDX] * stop_rate = (sell_row[OPEN_IDX] *
(1 + abs(self.strategy.trailing_stop_positive_offset) - (1 + side_1 * abs(self.strategy.trailing_stop_positive_offset) -
abs(self.strategy.trailing_stop_positive))) side_1 * abs(self.strategy.trailing_stop_positive / leverage)))
else: else:
# Worst case: price ticks tiny bit above open and dives down. # Worst case: price ticks tiny bit above open and dives down.
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct)) stop_rate = sell_row[OPEN_IDX] * (1 -
side_1 * abs(trade.stop_loss_pct / leverage))
if is_short:
assert stop_rate > sell_row[LOW_IDX]
else:
assert stop_rate < sell_row[HIGH_IDX] assert stop_rate < sell_row[HIGH_IDX]
# Limit lower-end to candle low to avoid sells below the low. # Limit lower-end to candle low to avoid sells below the low.
# This still remains "worst case" - but "worst realistic case". # This still remains "worst case" - but "worst realistic case".
if is_short:
return min(sell_row[HIGH_IDX], stop_rate)
else:
return max(sell_row[LOW_IDX], stop_rate) return max(sell_row[LOW_IDX], stop_rate)
# Set close_rate to stoploss # Set close_rate to stoploss
return trade.stop_loss return trade.stop_loss
elif sell.sell_type == (SellType.ROI):
def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
trade_dur: int) -> float:
is_short = trade.is_short or False
leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
if roi is not None and roi_entry is not None: if roi is not None and roi_entry is not None:
if roi == -1 and roi_entry % self.timeframe_min == 0: if roi == -1 and roi_entry % self.timeframe_min == 0:
@ -398,29 +426,44 @@ class Backtesting:
# - we'll use open instead of close # - we'll use open instead of close
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1) # - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
close_rate = - (trade.open_rate * roi + trade.open_rate * roi_rate = trade.open_rate * roi / leverage
(1 + trade.fee_open)) / (trade.fee_close - 1) open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
close_rate = -side_1 * (roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1)
if is_short:
is_new_roi = sell_row[OPEN_IDX] < close_rate
else:
is_new_roi = sell_row[OPEN_IDX] > close_rate
if (trade_dur > 0 and trade_dur == roi_entry if (trade_dur > 0 and trade_dur == roi_entry
and roi_entry % self.timeframe_min == 0 and roi_entry % self.timeframe_min == 0
and sell_row[OPEN_IDX] > close_rate): and is_new_roi):
# new ROI entry came into effect. # new ROI entry came into effect.
# use Open rate if open_rate > calculated sell rate # use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
if ( if (trade_dur == 0 and (
trade_dur == 0 (
# Red candle (for longs), TODO: green candle (for shorts) is_short
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle # Red candle (for longs)
and sell_row[OPEN_IDX] < sell_row[CLOSE_IDX] # Red candle
and trade.open_rate > sell_row[OPEN_IDX] # trade-open above open_rate
and close_rate < sell_row[CLOSE_IDX] # closes below close
)
or
(
not is_short
# green candle (for shorts)
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # green candle
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate
and close_rate > sell_row[CLOSE_IDX] and close_rate > sell_row[CLOSE_IDX] # closes above close
): )
)):
# ROI on opening candles with custom pricing can only # ROI on opening candles with custom pricing can only
# trigger if the entry was at Open or lower. # trigger if the entry was at Open or lower wick.
# details: https: // github.com/freqtrade/freqtrade/issues/6261 # details: https: // github.com/freqtrade/freqtrade/issues/6261
# If open_rate is < open, only allow sells below the close on red candles. # If open_rate is < open, only allow sells below the close on red candles.
raise ValueError("Opening candle ROI on red candles.") raise ValueError("Opening candle ROI on red candles.")
# Use the maximum between close_rate and low as we # Use the maximum between close_rate and low as we
# cannot sell outside of a candle. # cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that. # Applies when a new ROI setting comes in place and the whole candle is above that.
@ -429,8 +472,6 @@ class Backtesting:
else: else:
# This should not be reached... # This should not be reached...
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
else:
return sell_row[OPEN_IDX]
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> LocalTrade: ) -> LocalTrade:
@ -534,8 +575,8 @@ class Backtesting:
ft_pair=trade.pair, ft_pair=trade.pair,
order_id=str(self.order_id_counter), order_id=str(self.order_id_counter),
symbol=trade.pair, symbol=trade.pair,
ft_order_side="sell", ft_order_side=trade.exit_side,
side="sell", side=trade.exit_side,
order_type=order_type, order_type=order_type,
status="open", status="open",
price=closerate, price=closerate,
@ -607,6 +648,9 @@ class Backtesting:
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
# We can't place orders higher than current high (otherwise it'd be a stop limit buy) # We can't place orders higher than current high (otherwise it'd be a stop limit buy)
# which freqtrade does not support in live. # which freqtrade does not support in live.
if direction == "short":
propose_rate = max(propose_rate, row[LOW_IDX])
else:
propose_rate = min(propose_rate, row[HIGH_IDX]) propose_rate = min(propose_rate, row[HIGH_IDX])
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
@ -712,8 +756,8 @@ class Backtesting:
ft_pair=trade.pair, ft_pair=trade.pair,
order_id=str(self.order_id_counter), order_id=str(self.order_id_counter),
symbol=trade.pair, symbol=trade.pair,
ft_order_side="buy", ft_order_side=trade.enter_side,
side="buy", side=trade.enter_side,
order_type=order_type, order_type=order_type,
status="open", status="open",
order_date=current_time, order_date=current_time,
@ -795,17 +839,17 @@ class Backtesting:
timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time)
if timedout: if timedout:
if order.side == 'buy': if order.side == trade.enter_side:
self.timedout_entry_orders += 1 self.timedout_entry_orders += 1
if trade.nr_of_successful_entries == 0: if trade.nr_of_successful_entries == 0:
# Remove trade due to buy timeout expiration. # Remove trade due to entry timeout expiration.
return True return True
else: else:
# Close additional buy order # Close additional buy order
del trade.orders[trade.orders.index(order)] del trade.orders[trade.orders.index(order)]
if order.side == 'sell': if order.side == trade.exit_side:
self.timedout_exit_orders += 1 self.timedout_exit_orders += 1
# Close sell order and retry selling on next signal. # Close exit order and retry exiting on next signal.
del trade.orders[trade.orders.index(order)] del trade.orders[trade.orders.index(order)]
return False return False
@ -901,8 +945,8 @@ class Backtesting:
open_trades[pair].append(trade) open_trades[pair].append(trade)
for trade in list(open_trades[pair]): for trade in list(open_trades[pair]):
# 2. Process buy orders. # 2. Process entry orders.
order = trade.select_order('buy', is_open=True) order = trade.select_order(trade.enter_side, is_open=True)
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time) order.close_bt_order(current_time)
trade.open_order_id = None trade.open_order_id = None
@ -914,7 +958,7 @@ class Backtesting:
self._get_sell_trade_entry(trade, row) # Place sell order if necessary self._get_sell_trade_entry(trade, row) # Place sell order if necessary
# 4. Process sell orders. # 4. Process sell orders.
order = trade.select_order('sell', is_open=True) order = trade.select_order(trade.exit_side, is_open=True)
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
trade.open_order_id = None trade.open_order_id = None
trade.close_date = current_time trade.close_date = current_time

View File

@ -582,13 +582,17 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
pair = 'UNITTEST/USDT:USDT' pair = 'UNITTEST/USDT:USDT'
row = [ row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
1, # Buy
0.001, # Open 0.001, # Open
0.0011, # Close
0, # Sell
0.00099, # Low
0.0012, # High 0.0012, # High
'', # Buy Signal Name 0.00099, # Low
0.0011, # Close
1, # enter_long
0, # exit_long
1, # enter_short
0, # exit_hsort
'', # Long Signal Name
'', # Short Signal Name
'', # Exit Signal Name
] ]
backtesting.strategy.leverage = MagicMock(return_value=5.0) backtesting.strategy.leverage = MagicMock(return_value=5.0)