Update backtest sell terminology to exit
This commit is contained in:
parent
3c17409bd7
commit
25c6c5e326
@ -178,7 +178,7 @@ class Backtesting:
|
|||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to Strategy baseclass
|
||||||
strategy.wallets = self.wallets
|
strategy.wallets = self.wallets
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-exit is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
|
||||||
@ -353,24 +353,24 @@ class Backtesting:
|
|||||||
data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else []
|
data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else []
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_close_rate(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
def _get_close_rate(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
"""
|
"""
|
||||||
Get close rate for backtesting result
|
Get close rate for backtesting result
|
||||||
"""
|
"""
|
||||||
# Special handling if high or low hit STOP_LOSS or ROI
|
# Special handling if high or low hit STOP_LOSS or ROI
|
||||||
if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
||||||
return self._get_close_rate_for_stoploss(row, trade, sell, trade_dur)
|
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur)
|
||||||
elif sell.exit_type == (ExitType.ROI):
|
elif exit.exit_type == (ExitType.ROI):
|
||||||
return self._get_close_rate_for_roi(row, trade, sell, trade_dur)
|
return self._get_close_rate_for_roi(row, trade, exit, trade_dur)
|
||||||
else:
|
else:
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
# our stoploss was already lower than candle high,
|
# 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.
|
# exit at open price.
|
||||||
is_short = trade.is_short or False
|
is_short = trade.is_short or False
|
||||||
leverage = trade.leverage or 1.0
|
leverage = trade.leverage or 1.0
|
||||||
side_1 = -1 if is_short else 1
|
side_1 = -1 if is_short else 1
|
||||||
@ -384,7 +384,7 @@ class Backtesting:
|
|||||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||||
# pessimistic price movement, which is moving just enough to arm stoploss and
|
# pessimistic price movement, which is moving just enough to arm stoploss and
|
||||||
# immediately going down to stop price.
|
# immediately going down to stop price.
|
||||||
if sell.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0:
|
if exit.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0:
|
||||||
if (
|
if (
|
||||||
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
|
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
|
||||||
and self.strategy.trailing_only_offset_is_reached
|
and self.strategy.trailing_only_offset_is_reached
|
||||||
@ -403,7 +403,7 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
assert stop_rate < row[HIGH_IDX]
|
assert stop_rate < row[HIGH_IDX]
|
||||||
|
|
||||||
# Limit lower-end to candle low to avoid sells below the low.
|
# Limit lower-end to candle low to avoid exits below the low.
|
||||||
# This still remains "worst case" - but "worst realistic case".
|
# This still remains "worst case" - but "worst realistic case".
|
||||||
if is_short:
|
if is_short:
|
||||||
return min(row[HIGH_IDX], stop_rate)
|
return min(row[HIGH_IDX], stop_rate)
|
||||||
@ -413,7 +413,7 @@ class Backtesting:
|
|||||||
# Set close_rate to stoploss
|
# Set close_rate to stoploss
|
||||||
return trade.stop_loss
|
return trade.stop_loss
|
||||||
|
|
||||||
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
is_short = trade.is_short or False
|
is_short = trade.is_short or False
|
||||||
leverage = trade.leverage or 1.0
|
leverage = trade.leverage or 1.0
|
||||||
@ -438,7 +438,7 @@ class Backtesting:
|
|||||||
and roi_entry % self.timeframe_min == 0
|
and roi_entry % self.timeframe_min == 0
|
||||||
and is_new_roi):
|
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 exit rate
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
|
|
||||||
if (trade_dur == 0 and (
|
if (trade_dur == 0 and (
|
||||||
@ -461,11 +461,11 @@ class Backtesting:
|
|||||||
# 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 wick.
|
# 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 exits 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 exit 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.
|
||||||
return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX])
|
return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX])
|
||||||
|
|
||||||
@ -500,7 +500,7 @@ class Backtesting:
|
|||||||
""" Rate is within candle, therefore filled"""
|
""" Rate is within candle, therefore filled"""
|
||||||
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||||
|
|
||||||
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
def _get_exit_trade_entry_for_candle(self, trade: LocalTrade,
|
||||||
row: Tuple) -> Optional[LocalTrade]:
|
row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
# Check if we need to adjust our current positions
|
# Check if we need to adjust our current positions
|
||||||
@ -512,33 +512,33 @@ class Backtesting:
|
|||||||
if check_adjust_entry:
|
if check_adjust_entry:
|
||||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||||
exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||||
sell = self.strategy.should_exit(
|
exit_ = self.strategy.should_exit(
|
||||||
trade, row[OPEN_IDX], sell_candle_time, # type: ignore
|
trade, row[OPEN_IDX], exit_candle_time, # type: ignore
|
||||||
enter=enter, exit_=exit_,
|
enter=enter, exit_=exit_,
|
||||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||||
)
|
)
|
||||||
|
|
||||||
if sell.exit_flag:
|
if exit_.exit_flag:
|
||||||
trade.close_date = sell_candle_time
|
trade.close_date = exit_candle_time
|
||||||
|
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
try:
|
try:
|
||||||
closerate = self._get_close_rate(row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(row, trade, exit_, trade_dur)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
# call the custom exit price,with default value as previous closerate
|
# call the custom exit price,with default value as previous closerate
|
||||||
current_profit = trade.calc_profit_ratio(closerate)
|
current_profit = trade.calc_profit_ratio(closerate)
|
||||||
order_type = self.strategy.order_types['exit']
|
order_type = self.strategy.order_types['exit']
|
||||||
if sell.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
|
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
|
||||||
# Custom exit pricing only for sell-signals
|
# Custom exit pricing only for exit-signals
|
||||||
if order_type == 'limit':
|
if order_type == 'limit':
|
||||||
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
default_retval=closerate)(
|
default_retval=closerate)(
|
||||||
pair=trade.pair, trade=trade,
|
pair=trade.pair, trade=trade,
|
||||||
current_time=sell_candle_time,
|
current_time=exit_candle_time,
|
||||||
proposed_rate=closerate, current_profit=current_profit)
|
proposed_rate=closerate, current_profit=current_profit)
|
||||||
# We can't place orders lower than current low.
|
# We can't place orders lower than current low.
|
||||||
# freqtrade does not support this in live, and the order would fill immediately
|
# freqtrade does not support this in live, and the order would fill immediately
|
||||||
@ -553,12 +553,12 @@ class Backtesting:
|
|||||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||||
rate=closerate,
|
rate=closerate,
|
||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
sell_reason=sell.exit_reason, # deprecated
|
sell_reason=exit_.exit_reason, # deprecated
|
||||||
exit_reason=sell.exit_reason,
|
exit_reason=exit_.exit_reason,
|
||||||
current_time=sell_candle_time):
|
current_time=exit_candle_time):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trade.exit_reason = sell.exit_reason
|
trade.exit_reason = exit_.exit_reason
|
||||||
|
|
||||||
# Checks and adds an exit tag, after checking that the length of the
|
# Checks and adds an exit tag, after checking that the length of the
|
||||||
# row has the length for an exit tag column
|
# row has the length for an exit tag column
|
||||||
@ -573,8 +573,8 @@ class Backtesting:
|
|||||||
order = Order(
|
order = Order(
|
||||||
id=self.order_id_counter,
|
id=self.order_id_counter,
|
||||||
ft_trade_id=trade.id,
|
ft_trade_id=trade.id,
|
||||||
order_date=sell_candle_time,
|
order_date=exit_candle_time,
|
||||||
order_update_date=sell_candle_time,
|
order_update_date=exit_candle_time,
|
||||||
ft_is_open=True,
|
ft_is_open=True,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
order_id=str(self.order_id_counter),
|
order_id=str(self.order_id_counter),
|
||||||
@ -595,8 +595,8 @@ class Backtesting:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_sell_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
||||||
sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||||
@ -604,20 +604,20 @@ class Backtesting:
|
|||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
is_short=trade.is_short,
|
is_short=trade.is_short,
|
||||||
open_date=trade.open_date_utc,
|
open_date=trade.open_date_utc,
|
||||||
close_date=sell_candle_time,
|
close_date=exit_candle_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.timeframe_detail and trade.pair in self.detail_data:
|
if self.timeframe_detail and trade.pair in self.detail_data:
|
||||||
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
|
exit_candle_end = exit_candle_time + timedelta(minutes=self.timeframe_min)
|
||||||
|
|
||||||
detail_data = self.detail_data[trade.pair]
|
detail_data = self.detail_data[trade.pair]
|
||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
(detail_data['date'] >= sell_candle_time) &
|
(detail_data['date'] >= exit_candle_time) &
|
||||||
(detail_data['date'] < sell_candle_end)
|
(detail_data['date'] < exit_candle_end)
|
||||||
].copy()
|
].copy()
|
||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
return self._get_sell_trade_entry_for_candle(trade, row)
|
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||||
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
||||||
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
||||||
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
||||||
@ -627,14 +627,14 @@ class Backtesting:
|
|||||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
||||||
for det_row in detail_data[headers].values.tolist():
|
for det_row in detail_data[headers].values.tolist():
|
||||||
res = self._get_sell_trade_entry_for_candle(trade, det_row)
|
res = self._get_exit_trade_entry_for_candle(trade, det_row)
|
||||||
if res:
|
if res:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return self._get_sell_trade_entry_for_candle(trade, row)
|
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
||||||
@ -815,11 +815,11 @@ class Backtesting:
|
|||||||
if trade.open_order_id and trade.nr_of_successful_entries == 0:
|
if trade.open_order_id and trade.nr_of_successful_entries == 0:
|
||||||
# Ignore trade if entry-order did not fill yet
|
# Ignore trade if entry-order did not fill yet
|
||||||
continue
|
continue
|
||||||
sell_row = data[pair][-1]
|
exit_row = data[pair][-1]
|
||||||
|
|
||||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
trade.close_date = exit_row[DATE_IDX].to_pydatetime()
|
||||||
trade.exit_reason = ExitType.FORCE_EXIT.value
|
trade.exit_reason = ExitType.FORCE_EXIT.value
|
||||||
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
trade.close(exit_row[OPEN_IDX], show_msg=False)
|
||||||
LocalTrade.close_bt_trade(trade)
|
LocalTrade.close_bt_trade(trade)
|
||||||
# Deepcopy object to have wallets update correctly
|
# Deepcopy object to have wallets update correctly
|
||||||
trade1 = deepcopy(trade)
|
trade1 = deepcopy(trade)
|
||||||
@ -985,18 +985,18 @@ class Backtesting:
|
|||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# 4. Create sell orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
self._get_sell_trade_entry(trade, row) # Place sell order if necessary
|
self._get_exit_trade_entry(trade, row) # Place exit order if necessary
|
||||||
|
|
||||||
# 5. Process sell orders.
|
# 5. Process exit orders.
|
||||||
order = trade.select_order(trade.exit_side, 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
|
||||||
trade.close(order.price, show_msg=False)
|
trade.close(order.price, show_msg=False)
|
||||||
|
|
||||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
# logger.debug(f"{pair} - Backtesting exit {trade}")
|
||||||
open_trade_count -= 1
|
open_trade_count -= 1
|
||||||
open_trades[pair].remove(trade)
|
open_trades[pair].remove(trade)
|
||||||
LocalTrade.close_bt_trade(trade)
|
LocalTrade.close_bt_trade(trade)
|
||||||
|
@ -714,7 +714,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# No data available.
|
# No data available.
|
||||||
res = backtesting._get_sell_trade_entry(trade, row_sell)
|
res = backtesting._get_exit_trade_entry(trade, row_sell)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
assert res.exit_reason == ExitType.ROI.value
|
||||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
||||||
@ -727,13 +727,13 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
||||||
|
|
||||||
res = backtesting._get_sell_trade_entry(trade, row)
|
res = backtesting._get_exit_trade_entry(trade, row)
|
||||||
assert res is None
|
assert res is None
|
||||||
|
|
||||||
# Assign backtest-detail data
|
# Assign backtest-detail data
|
||||||
backtesting.detail_data[pair] = row_detail
|
backtesting.detail_data[pair] = row_detail
|
||||||
|
|
||||||
res = backtesting._get_sell_trade_entry(trade, row_sell)
|
res = backtesting._get_exit_trade_entry(trade, row_sell)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
assert res.exit_reason == ExitType.ROI.value
|
||||||
# Sell at minute 3 (not available above!)
|
# Sell at minute 3 (not available above!)
|
||||||
|
Loading…
Reference in New Issue
Block a user