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.
* 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.
* 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
!!! Note

View File

@ -423,7 +423,7 @@ class AwesomeStrategy(IStrategy):
!!! 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.
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

View File

@ -554,7 +554,8 @@ class Backtesting:
if remaining < min_stake:
# Remaining stake is too low to be sold.
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:
order = pos_trade.orders[-1]
if self._get_order_filled(order.price, row):
@ -589,14 +590,15 @@ class Backtesting:
return t
return None
def _get_exit_for_signal(self, trade: LocalTrade, row: Tuple,
exit_: ExitCheckTuple) -> Optional[LocalTrade]:
def _get_exit_for_signal(
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
amount: Optional[float] = None) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
if exit_.exit_flag:
trade.close_date = exit_candle_time
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)
try:
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
current_profit = trade.calc_profit_ratio(close_rate)
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
# row has the length for an exit tag column
if (
@ -633,12 +636,13 @@ class Backtesting:
# Confirm trade 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)
and not strategy_safe_wrapper(
self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair,
trade=trade, # type: ignore[arg-type]
order_type=order_type,
amount=trade.amount,
amount=amount_,
rate=close_rate,
time_in_force=time_in_force,
sell_reason=exit_reason, # deprecated
@ -648,7 +652,7 @@ class Backtesting:
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
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,