support price callback for partial exits in bt

This will align results to how live works.
closes #7292
This commit is contained in:
Matthias 2022-08-27 08:50:09 +02:00
parent 9204f01312
commit 2b70c3d0c0
3 changed files with 23 additions and 19 deletions

View File

@ -70,7 +70,7 @@ This loop will be repeated again and again until the bot is stopped.
* Determine stake size by calling the `custom_stake_amount()` callback. * Determine stake size by calling the `custom_stake_amount()` callback.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points. * Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
* For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). * For exits based on exit-signal, custom-exit and partial exits: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Generate backtest report output * Generate backtest report output
!!! Note !!! Note

View File

@ -423,7 +423,7 @@ class AwesomeStrategy(IStrategy):
!!! Warning "Backtesting" !!! Warning "Backtesting"
Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range. Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle. Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
`custom_exit_price()` is only called for sells of type exit_signal and Custom exit. All other exit-types will use regular backtesting prices. `custom_exit_price()` is only called for sells of type exit_signal, Custom exit and partial exits. All other exit-types will use regular backtesting prices.
## Custom order timeout rules ## Custom order timeout rules

View File

@ -554,7 +554,8 @@ class Backtesting:
if remaining < min_stake: if remaining < min_stake:
# Remaining stake is too low to be sold. # Remaining stake is too low to be sold.
return trade return trade
pos_trade = self._exit_trade(trade, row, current_rate, amount) exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
if pos_trade is not None: if pos_trade is not None:
order = pos_trade.orders[-1] order = pos_trade.orders[-1]
if self._get_order_filled(order.price, row): if self._get_order_filled(order.price, row):
@ -589,14 +590,15 @@ class Backtesting:
return t return t
return None return None
def _get_exit_for_signal(self, trade: LocalTrade, row: Tuple, def _get_exit_for_signal(
exit_: ExitCheckTuple) -> Optional[LocalTrade]: self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
amount: Optional[float] = None) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
if exit_.exit_flag: if exit_.exit_flag:
trade.close_date = exit_candle_time trade.close_date = exit_candle_time
exit_reason = exit_.exit_reason exit_reason = exit_.exit_reason
amount_ = amount if amount is not None else trade.amount
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:
close_rate = self._get_close_rate(row, trade, exit_, trade_dur) close_rate = self._get_close_rate(row, trade, exit_, trade_dur)
@ -605,7 +607,8 @@ class Backtesting:
# call the custom exit price,with default value as previous close_rate # call the custom exit price,with default value as previous close_rate
current_profit = trade.calc_profit_ratio(close_rate) current_profit = trade.calc_profit_ratio(close_rate)
order_type = self.strategy.order_types['exit'] order_type = self.strategy.order_types['exit']
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT,
ExitType.PARTIAL_EXIT):
# 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
if ( if (
@ -633,22 +636,23 @@ class Backtesting:
# Confirm trade exit: # Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['exit'] time_in_force = self.strategy.order_time_in_force['exit']
if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( if (exit_.exit_type not in (ExitType.LIQUIDATION, ExitType.PARTIAL_EXIT)
self.strategy.confirm_trade_exit, default_retval=True)( and not strategy_safe_wrapper(
pair=trade.pair, self.strategy.confirm_trade_exit, default_retval=True)(
trade=trade, # type: ignore[arg-type] pair=trade.pair,
order_type=order_type, trade=trade, # type: ignore[arg-type]
amount=trade.amount, order_type=order_type,
rate=close_rate, amount=amount_,
time_in_force=time_in_force, rate=close_rate,
sell_reason=exit_reason, # deprecated time_in_force=time_in_force,
exit_reason=exit_reason, sell_reason=exit_reason, # deprecated
current_time=exit_candle_time)): exit_reason=exit_reason,
current_time=exit_candle_time)):
return None return None
trade.exit_reason = exit_reason trade.exit_reason = exit_reason
return self._exit_trade(trade, row, close_rate, trade.amount) return self._exit_trade(trade, row, close_rate, amount_)
return None return None
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,