mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-15 04:33:57 +00:00
Merge pull request #10782 from freqtrade/feat/bt_reverse
Some checks are pending
Build Documentation / Deploy Docs through mike (push) Waiting to run
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:
commit
67f26fa1ac
|
@ -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.
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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]
|
||||||
):
|
):
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user