Merge pull request #5108 from rokups/rk/pessimistic-trailing-stoploss

Implement most pessimistic handling of trailing stoploss.
This commit is contained in:
Matthias 2021-06-17 18:41:00 +01:00 committed by GitHub
commit 8bb464bd64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 74 additions and 8 deletions

View File

@ -444,6 +444,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
- Low happens before high for stoploss, protecting capital first - Low happens before high for stoploss, protecting capital first
- Trailing stoploss - Trailing stoploss
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
- High happens first - adjusting stoploss - High happens first - adjusting stoploss
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies

View File

@ -225,6 +225,22 @@ class Backtesting:
# sell at open price. # sell at open price.
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
# Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and
# immediately going down to stop price.
if (sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0
and self.strategy.trailing_stop_positive):
if self.strategy.trailing_only_offset_is_reached:
# Worst case: price reaches stop_positive_offset and dives down.
stop_rate = (sell_row[OPEN_IDX] *
(1 + abs(self.strategy.trailing_stop_positive_offset) -
abs(self.strategy.trailing_stop_positive)))
else:
# Worst case: price ticks tiny bit above open and dives down.
stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive))
assert stop_rate < sell_row[HIGH_IDX]
return 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): elif sell.sell_type == (SellType.ROI):

View File

@ -524,15 +524,14 @@ class IStrategy(ABC, HyperStrategyMixin):
:param force_stoploss: Externally provided stoploss :param force_stoploss: Externally provided stoploss
:return: True if trade should be sold, False otherwise :return: True if trade should be sold, False otherwise
""" """
# Set current rate to low for backtesting sell current_rate = rate
current_rate = low or rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
trade.adjust_min_max_rates(high or current_rate) trade.adjust_min_max_rates(high or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit, current_time=date, current_profit=current_profit,
force_stoploss=force_stoploss, high=high) force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting sell # Set current rate to high for backtesting sell
current_rate = high or rate current_rate = high or rate
@ -599,18 +598,21 @@ class IStrategy(ABC, HyperStrategyMixin):
def stop_loss_reached(self, current_rate: float, trade: Trade, def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float, current_time: datetime, current_profit: float,
force_stoploss: float, high: float = None) -> SellCheckTuple: force_stoploss: float, low: float = None,
high: float = None) -> SellCheckTuple:
""" """
Based on current profit of the trade and configured (trailing) stoploss, Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not decides to sell or not
:param current_profit: current profit as ratio :param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
""" """
stop_loss_value = force_stoploss if force_stoploss else self.stoploss stop_loss_value = force_stoploss if force_stoploss else self.stoploss
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if self.use_custom_stoploss: if self.use_custom_stoploss and trade.stop_loss < (low or current_rate):
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
)(pair=trade.pair, trade=trade, )(pair=trade.pair, trade=trade,
current_time=current_time, current_time=current_time,
@ -623,7 +625,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
logger.warning("CustomStoploss function did not return valid stoploss") logger.warning("CustomStoploss function did not return valid stoploss")
if self.trailing_stop: if self.trailing_stop and trade.stop_loss < (low or current_rate):
# trailing stoploss handling # trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset sl_offset = self.trailing_stop_positive_offset
@ -643,7 +645,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# evaluate if the stoploss was hit if stoploss is not on exchange # evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling. # regular stoploss handling.
if ((trade.stop_loss >= current_rate) and if ((trade.stop_loss >= (low or current_rate)) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS sell_type = SellType.STOP_LOSS
@ -652,7 +654,7 @@ class IStrategy(ABC, HyperStrategyMixin):
if trade.initial_stop_loss != trade.stop_loss: if trade.initial_stop_loss != trade.stop_loss:
sell_type = SellType.TRAILING_STOP_LOSS sell_type = SellType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
f"stoploss is {trade.stop_loss:.6f}, " f"stoploss is {trade.stop_loss:.6f}, "
f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}") f"trade opened at {trade.open_rate:.6f}")

View File

@ -457,6 +457,50 @@ tc28 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 29: trailing_stop should be triggered by low of next candle, without adjusting stoploss using
# high of stoploss candle.
# stop-loss: 10%, ROI: 10% (should not apply)
tc29 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5050, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Triggers trailing-stoploss
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.02, trailing_stop=True,
trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
)
# Test 30: trailing_stop should be triggered immediately on trade open candle.
# stop-loss: 10%, ROI: 10% (should not apply)
tc30 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True,
trailing_stop_positive=0.01,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
)
# Test 31: trailing_stop should be triggered immediately on trade open candle.
# stop-loss: 10%, ROI: 10% (should not apply)
tc31 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.01, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
)
TESTS = [ TESTS = [
tc0, tc0,
tc1, tc1,
@ -487,6 +531,9 @@ TESTS = [
tc26, tc26,
tc27, tc27,
tc28, tc28,
tc29,
tc30,
tc31,
] ]