Merge pull request #10782 from freqtrade/feat/bt_reverse
Some checks are pending
Build Documentation / Deploy Docs through mike (push) Waiting to run

Enable future positions to reverse their position
This commit is contained in:
Matthias 2024-11-01 09:29:35 +01:00 committed by GitHub
commit 67f26fa1ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 107 additions and 7 deletions

View File

@ -558,6 +558,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- Stoploss - Stoploss
- ROI - ROI
- Trailing stoploss - Trailing stoploss
- Position reversals (futures only) happen if an entry signal in the other direction than the closing trade triggers at the candle the existing trade closes.
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.

View File

@ -2284,7 +2284,7 @@ class FreqtradeBot(LoggingMixin):
def handle_protections(self, pair: str, side: LongShort) -> None: def handle_protections(self, pair: str, side: LongShort) -> None:
# Lock pair for one candle to prevent immediate re-entries # Lock pair for one candle to prevent immediate re-entries
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock") self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock", side=side)
prot_trig = self.protections.stop_per_pair(pair, side=side) prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig: if prot_trig:
msg: RPCProtectionMsg = { msg: RPCProtectionMsg = {

View File

@ -1335,11 +1335,38 @@ class Backtesting:
trade_dir: Optional[LongShort], trade_dir: Optional[LongShort],
can_enter: bool, can_enter: bool,
) -> None: ) -> None:
"""
Conditionally call backtest_loop_inner a 2nd time if shorting is enabled,
a position closed and a new signal in the other direction is available.
"""
if not self._can_short or trade_dir is None:
# No need to reverse position if shorting is disabled or there's no new signal
self.backtest_loop_inner(row, pair, current_time, trade_dir, can_enter)
else:
for _ in (0, 1):
a = self.backtest_loop_inner(row, pair, current_time, trade_dir, can_enter)
if not a or a == trade_dir:
# the trade didn't close or position change is in the same direction
break
def backtest_loop_inner(
self,
row: tuple,
pair: str,
current_time: datetime,
trade_dir: Optional[LongShort],
can_enter: bool,
) -> Optional[LongShort]:
""" """
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
Backtesting processing for one candle/pair. Backtesting processing for one candle/pair.
""" """
exiting_dir: Optional[LongShort] = None
if not self._position_stacking and len(LocalTrade.bt_trades_open_pp[pair]) > 0:
# position_stacking not supported for now.
exiting_dir = "short" if LocalTrade.bt_trades_open_pp[pair][0].is_short else "long"
for t in list(LocalTrade.bt_trades_open_pp[pair]): for t in list(LocalTrade.bt_trades_open_pp[pair]):
# 1. Manage currently open orders of active trades # 1. Manage currently open orders of active trades
if self.manage_open_orders(t, current_time, row): if self.manage_open_orders(t, current_time, row):
@ -1380,6 +1407,10 @@ class Backtesting:
if order: if order:
self._process_exit_order(order, trade, current_time, row, pair) self._process_exit_order(order, trade, current_time, row, pair)
if exiting_dir and len(LocalTrade.bt_trades_open_pp[pair]) == 0:
return exiting_dir
return None
def time_pair_generator( def time_pair_generator(
self, start_date: datetime, end_date: datetime, increment: timedelta, pairs: list[str] self, start_date: datetime, end_date: datetime, increment: timedelta, pairs: list[str]
): ):

View File

@ -3483,16 +3483,17 @@ def test_locked_pairs(
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS), exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS),
) )
trade.close(ticker_usdt_sell_down()["bid"]) trade.close(ticker_usdt_sell_down()["bid"])
assert freqtrade.strategy.is_pair_locked(trade.pair, side="*") assert not freqtrade.strategy.is_pair_locked(trade.pair, side="*")
# Both sides are locked # Both sides are locked
assert freqtrade.strategy.is_pair_locked(trade.pair, side="long") assert freqtrade.strategy.is_pair_locked(trade.pair, side="long") != is_short
assert freqtrade.strategy.is_pair_locked(trade.pair, side="short") assert freqtrade.strategy.is_pair_locked(trade.pair, side="short") == is_short
# reinit - should buy other pair. # reinit - should buy other pair.
caplog.clear() caplog.clear()
freqtrade.enter_positions() freqtrade.enter_positions()
direction = "short" if is_short else "long"
assert log_has_re(rf"Pair {trade.pair} \* is locked.*", caplog) assert log_has_re(rf"Pair {trade.pair} {direction} is locked.*", caplog)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])

View File

@ -1121,6 +1121,70 @@ tc53 = BTContainer(
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=True)], trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=True)],
) )
# Test 54: Switch position from long to short
tc54 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0],
[1, 5000, 5000, 4951, 5000, 6172, 0, 0, 0, 0],
[2, 4910, 5150, 4910, 5100, 6172, 0, 0, 1, 0], # Enter short signal being ignored
[3, 5100, 5100, 4950, 4950, 6172, 0, 1, 1, 0], # exit - re-enter short
[4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 1],
[5, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0],
],
stop_loss=-0.10,
roi={"0": 0.10},
profit_perc=0.00,
use_exit_signal=True,
trades=[
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False),
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=4, close_tick=5, is_short=True),
],
)
# Test 55: Switch position from short to long
tc55 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
[1, 5000, 5000, 4951, 5000, 6172, 1, 0, 0, 0], # Enter long signal being ignored
[2, 4910, 5150, 4910, 5100, 6172, 1, 0, 0, 1], # Exit - reenter long
[3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 1, 0, 0],
[5, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0],
],
stop_loss=-0.10,
roi={"0": 0.10},
profit_perc=-0.04,
use_exit_signal=True,
trades=[
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=3, is_short=True),
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=3, close_tick=5, is_short=False),
],
)
# Test 56: Switch position from long to short
tc56 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0],
[1, 5000, 5000, 4951, 5000, 6172, 0, 0, 0, 0],
[2, 4910, 5150, 4910, 5100, 6172, 0, 0, 1, 0], # exit on stoploss - re-enter short
[3, 5100, 5100, 4888, 4950, 6172, 0, 0, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 1],
[5, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0],
],
stop_loss=-0.02,
roi={"0": 0.10},
profit_perc=-0.0,
use_exit_signal=True,
trades=[
BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3, is_short=False),
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=3, close_tick=5, is_short=True),
],
)
TESTS = [ TESTS = [
tc0, tc0,
tc1, tc1,
@ -1176,6 +1240,9 @@ TESTS = [
tc51, tc51,
tc52, tc52,
tc53, tc53,
tc54,
tc55,
tc56,
] ]

View File

@ -1899,7 +1899,7 @@ def test_backtest_multi_pair_long_short_switch(
if use_detail: if use_detail:
# Backtest loop is called once per candle per pair # Backtest loop is called once per candle per pair
assert bl_spy.call_count == 1071 assert bl_spy.call_count == 1523
else: else:
assert bl_spy.call_count == 479 assert bl_spy.call_count == 479
@ -1909,7 +1909,7 @@ def test_backtest_multi_pair_long_short_switch(
assert len(evaluate_result_multi(results["results"], "5m", 1)) == 0 assert len(evaluate_result_multi(results["results"], "5m", 1)) == 0
# Expect 26 results initially # Expect 26 results initially
assert len(results["results"]) == 30 assert len(results["results"]) == 53
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):