Merge pull request #6870 from freqtrade/should_exit_list
Should exit list
This commit is contained in:
commit
23e089061b
@ -530,8 +530,9 @@ Since backtesting lacks some detailed information about what happens within a ca
|
|||||||
- Exit-reason does not explain if a trade was positive or negative, just what triggered the exit (this can look odd if negative ROI values are used)
|
- Exit-reason does not explain if a trade was positive or negative, just what triggered the exit (this can look odd if negative ROI values are used)
|
||||||
- Evaluation sequence (if multiple signals happen on the same candle)
|
- Evaluation sequence (if multiple signals happen on the same candle)
|
||||||
- Exit-signal
|
- Exit-signal
|
||||||
- ROI (if not stoploss)
|
|
||||||
- Stoploss
|
- Stoploss
|
||||||
|
- ROI
|
||||||
|
- Trailing stoploss
|
||||||
|
|
||||||
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
||||||
Also, keep in mind that past results don't guarantee future success.
|
Also, keep in mind that past results don't guarantee future success.
|
||||||
|
@ -563,6 +563,14 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
||||||
|
|
||||||
|
`confirm_trade_exit()` may be called multiple times within one iteration for the same trade if different exit-reasons apply.
|
||||||
|
The exit-reasons (if applicable) will be in the following sequence:
|
||||||
|
|
||||||
|
* `exit_signal` / `custom_exit`
|
||||||
|
* `stop_loss`
|
||||||
|
* `roi`
|
||||||
|
* `trailing_stop_loss`
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
@ -605,6 +613,9 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
`confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits.
|
||||||
|
|
||||||
## Adjust trade position
|
## Adjust trade position
|
||||||
|
|
||||||
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
|
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
|
||||||
|
@ -15,3 +15,9 @@ class ExitCheckTuple:
|
|||||||
@property
|
@property
|
||||||
def exit_flag(self):
|
def exit_flag(self):
|
||||||
return self.exit_type != ExitType.NONE
|
return self.exit_type != ExitType.NONE
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.exit_type == other.exit_type and self.exit_reason == other.exit_reason
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ExitCheckTuple({self.exit_type}, {self.exit_reason})"
|
||||||
|
@ -1106,7 +1106,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
Check and execute trade exit
|
Check and execute trade exit
|
||||||
"""
|
"""
|
||||||
should_exit: ExitCheckTuple = self.strategy.should_exit(
|
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
||||||
trade,
|
trade,
|
||||||
exit_rate,
|
exit_rate,
|
||||||
datetime.now(timezone.utc),
|
datetime.now(timezone.utc),
|
||||||
@ -1114,12 +1114,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
exit_=exit_,
|
exit_=exit_,
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
for should_exit in exits:
|
||||||
if should_exit.exit_flag:
|
if should_exit.exit_flag:
|
||||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
|
||||||
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||||
return True
|
if exited:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def manage_open_orders(self) -> None:
|
def manage_open_orders(self) -> None:
|
||||||
@ -1406,7 +1407,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
:param limit: limit rate for the sell order
|
:param limit: limit rate for the sell order
|
||||||
:param exit_check: CheckTuple with signal and reason
|
:param exit_check: CheckTuple with signal and reason
|
||||||
:return: True if it succeeds (supported) False (not supported)
|
:return: True if it succeeds False
|
||||||
"""
|
"""
|
||||||
trade.funding_fees = self.exchange.get_funding_fees(
|
trade.funding_fees = self.exchange.get_funding_fees(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
@ -1453,7 +1454,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
time_in_force=time_in_force, exit_reason=exit_reason,
|
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||||
sell_reason=exit_reason, # sellreason -> compatibility
|
sell_reason=exit_reason, # sellreason -> compatibility
|
||||||
current_time=datetime.now(timezone.utc)):
|
current_time=datetime.now(timezone.utc)):
|
||||||
logger.info(f"User requested abortion of exiting {trade.pair}")
|
logger.info(f"User requested abortion of {trade.pair} exit.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -531,15 +531,23 @@ 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)
|
||||||
|
|
||||||
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_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||||
exit_ = self.strategy.should_exit(
|
exits = self.strategy.should_exit(
|
||||||
trade, row[OPEN_IDX], exit_candle_time, # type: ignore
|
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
|
||||||
enter=enter, exit_=exit_sig,
|
enter=enter, exit_=exit_sig,
|
||||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||||
)
|
)
|
||||||
|
for exit_ in exits:
|
||||||
|
t = self._get_exit_for_signal(trade, row, exit_)
|
||||||
|
if t:
|
||||||
|
return t
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_exit_for_signal(self, trade: LocalTrade, row: Tuple,
|
||||||
|
exit_: ExitCheckTuple) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -878,16 +878,16 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
||||||
enter: bool, exit_: bool,
|
enter: bool, exit_: bool,
|
||||||
low: float = None, high: float = None,
|
low: float = None, high: float = None,
|
||||||
force_stoploss: float = 0) -> ExitCheckTuple:
|
force_stoploss: float = 0) -> List[ExitCheckTuple]:
|
||||||
"""
|
"""
|
||||||
This function evaluates if one of the conditions required to trigger an exit order
|
This function evaluates if one of the conditions required to trigger an exit order
|
||||||
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
||||||
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
||||||
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
||||||
:param force_stoploss: Externally provided stoploss
|
:param force_stoploss: Externally provided stoploss
|
||||||
:return: True if trade should be exited, False otherwise
|
:return: List of exit reasons - or empty list.
|
||||||
"""
|
"""
|
||||||
|
exits: List[ExitCheckTuple] = []
|
||||||
current_rate = rate
|
current_rate = rate
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
|
||||||
@ -938,24 +938,29 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
logger.debug(f"{trade.pair} - Sell signal received. "
|
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||||
f"exit_type=ExitType.{exit_signal.name}" +
|
f"exit_type=ExitType.{exit_signal.name}" +
|
||||||
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||||
return ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason)
|
exits.append(ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason))
|
||||||
|
|
||||||
# Sequence:
|
# Sequence:
|
||||||
# Exit-signal
|
# Exit-signal
|
||||||
# ROI (if not stoploss)
|
|
||||||
# Stoploss
|
# Stoploss
|
||||||
if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
|
# ROI
|
||||||
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
# Trailing stoploss
|
||||||
return ExitCheckTuple(exit_type=ExitType.ROI)
|
|
||||||
|
|
||||||
if stoplossflag.exit_flag:
|
if stoplossflag.exit_type == ExitType.STOP_LOSS:
|
||||||
|
|
||||||
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
||||||
return stoplossflag
|
exits.append(stoplossflag)
|
||||||
|
|
||||||
# This one is noisy, commented out...
|
if roi_reached:
|
||||||
# logger.debug(f"{trade.pair} - No exit signal.")
|
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
||||||
return ExitCheckTuple(exit_type=ExitType.NONE)
|
exits.append(ExitCheckTuple(exit_type=ExitType.ROI))
|
||||||
|
|
||||||
|
if stoplossflag.exit_type == ExitType.TRAILING_STOP_LOSS:
|
||||||
|
|
||||||
|
logger.debug(f"{trade.pair} - Trailing stoploss hit.")
|
||||||
|
exits.append(stoplossflag)
|
||||||
|
|
||||||
|
return exits
|
||||||
|
|
||||||
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,
|
||||||
|
@ -495,37 +495,113 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
|
|||||||
enter=False, exit_=False,
|
enter=False, exit_=False,
|
||||||
low=None, high=None)
|
low=None, high=None)
|
||||||
|
|
||||||
assert res.exit_flag is False
|
assert res == []
|
||||||
assert res.exit_type == ExitType.NONE
|
|
||||||
|
|
||||||
strategy.custom_exit = MagicMock(return_value=True)
|
strategy.custom_exit = MagicMock(return_value=True)
|
||||||
res = strategy.should_exit(trade, 1, now,
|
res = strategy.should_exit(trade, 1, now,
|
||||||
enter=False, exit_=False,
|
enter=False, exit_=False,
|
||||||
low=None, high=None)
|
low=None, high=None)
|
||||||
assert res.exit_flag is True
|
assert res[0].exit_flag is True
|
||||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
assert res[0].exit_type == ExitType.CUSTOM_EXIT
|
||||||
assert res.exit_reason == 'custom_exit'
|
assert res[0].exit_reason == 'custom_exit'
|
||||||
|
|
||||||
strategy.custom_exit = MagicMock(return_value='hello world')
|
strategy.custom_exit = MagicMock(return_value='hello world')
|
||||||
|
|
||||||
res = strategy.should_exit(trade, 1, now,
|
res = strategy.should_exit(trade, 1, now,
|
||||||
enter=False, exit_=False,
|
enter=False, exit_=False,
|
||||||
low=None, high=None)
|
low=None, high=None)
|
||||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
assert res[0].exit_type == ExitType.CUSTOM_EXIT
|
||||||
assert res.exit_flag is True
|
assert res[0].exit_flag is True
|
||||||
assert res.exit_reason == 'hello world'
|
assert res[0].exit_reason == 'hello world'
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
strategy.custom_exit = MagicMock(return_value='h' * 100)
|
strategy.custom_exit = MagicMock(return_value='h' * 100)
|
||||||
res = strategy.should_exit(trade, 1, now,
|
res = strategy.should_exit(trade, 1, now,
|
||||||
enter=False, exit_=False,
|
enter=False, exit_=False,
|
||||||
low=None, high=None)
|
low=None, high=None)
|
||||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
assert res[0].exit_type == ExitType.CUSTOM_EXIT
|
||||||
assert res.exit_flag is True
|
assert res[0].exit_flag is True
|
||||||
assert res.exit_reason == 'h' * 64
|
assert res[0].exit_reason == 'h' * 64
|
||||||
assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog)
|
assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_sell(default_conf, fee) -> None:
|
||||||
|
|
||||||
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.01,
|
||||||
|
amount=1,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='binance',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
now = arrow.utcnow().datetime
|
||||||
|
res = strategy.should_exit(trade, 1, now,
|
||||||
|
enter=False, exit_=False,
|
||||||
|
low=None, high=None)
|
||||||
|
|
||||||
|
assert res == []
|
||||||
|
strategy.min_roi_reached = MagicMock(return_value=True)
|
||||||
|
|
||||||
|
res = strategy.should_exit(trade, 1, now,
|
||||||
|
enter=False, exit_=False,
|
||||||
|
low=None, high=None)
|
||||||
|
assert len(res) == 1
|
||||||
|
assert res == [ExitCheckTuple(exit_type=ExitType.ROI)]
|
||||||
|
|
||||||
|
strategy.min_roi_reached = MagicMock(return_value=True)
|
||||||
|
strategy.stop_loss_reached = MagicMock(
|
||||||
|
return_value=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
||||||
|
|
||||||
|
res = strategy.should_exit(trade, 1, now,
|
||||||
|
enter=False, exit_=False,
|
||||||
|
low=None, high=None)
|
||||||
|
assert len(res) == 2
|
||||||
|
assert res == [
|
||||||
|
ExitCheckTuple(exit_type=ExitType.STOP_LOSS),
|
||||||
|
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||||
|
]
|
||||||
|
|
||||||
|
strategy.custom_exit = MagicMock(return_value='hello world')
|
||||||
|
# custom-exit and exit-signal is first
|
||||||
|
res = strategy.should_exit(trade, 1, now,
|
||||||
|
enter=False, exit_=False,
|
||||||
|
low=None, high=None)
|
||||||
|
assert len(res) == 3
|
||||||
|
assert res == [
|
||||||
|
ExitCheckTuple(exit_type=ExitType.CUSTOM_EXIT, exit_reason='hello world'),
|
||||||
|
ExitCheckTuple(exit_type=ExitType.STOP_LOSS),
|
||||||
|
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||||
|
]
|
||||||
|
|
||||||
|
strategy.stop_loss_reached = MagicMock(
|
||||||
|
return_value=ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS))
|
||||||
|
# Regular exit signal
|
||||||
|
res = strategy.should_exit(trade, 1, now,
|
||||||
|
enter=False, exit_=True,
|
||||||
|
low=None, high=None)
|
||||||
|
assert len(res) == 3
|
||||||
|
assert res == [
|
||||||
|
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
|
||||||
|
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||||
|
ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Regular exit signal, no ROI
|
||||||
|
strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
res = strategy.should_exit(trade, 1, now,
|
||||||
|
enter=False, exit_=True,
|
||||||
|
low=None, high=None)
|
||||||
|
assert len(res) == 2
|
||||||
|
assert res == [
|
||||||
|
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
|
||||||
|
ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('side', TRADE_SIDES)
|
@pytest.mark.parametrize('side', TRADE_SIDES)
|
||||||
def test_leverage_callback(default_conf, side) -> None:
|
def test_leverage_callback(default_conf, side) -> None:
|
||||||
default_conf['strategy'] = 'StrategyTestV2'
|
default_conf['strategy'] = 'StrategyTestV2'
|
||||||
|
@ -52,8 +52,8 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
|
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
|
||||||
# Sell 3rd trade (not called for the first trade)
|
# Sell 3rd trade (not called for the first trade)
|
||||||
should_sell_mock = MagicMock(side_effect=[
|
should_sell_mock = MagicMock(side_effect=[
|
||||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
[],
|
||||||
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]
|
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]]
|
||||||
)
|
)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||||
@ -160,11 +160,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
|
|||||||
_notify_exit=MagicMock(),
|
_notify_exit=MagicMock(),
|
||||||
)
|
)
|
||||||
should_sell_mock = MagicMock(side_effect=[
|
should_sell_mock = MagicMock(side_effect=[
|
||||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
[],
|
||||||
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
|
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)],
|
||||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
[],
|
||||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
[],
|
||||||
ExitCheckTuple(exit_type=ExitType.NONE)]
|
[]]
|
||||||
)
|
)
|
||||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
|
mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user