From 62a3ed6f8da5537d616ba4ce0a5504d63684a7f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 08:39:13 +0200 Subject: [PATCH 1/7] partial exit order should not close immediately closes #10166 --- freqtrade/optimize/backtesting.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ee85ac711..8670a6b7c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -587,10 +587,6 @@ class Backtesting: exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT, order_tag) pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount) if pos_trade is not None: - order = pos_trade.orders[-1] - if self._try_close_open_order(order, trade, current_time, row): - trade.recalc_trade_from_orders() - self.wallets.update() return pos_trade return trade @@ -748,18 +744,18 @@ class Backtesting: if self.strategy.position_adjustment_enable: trade = self._get_adjust_trade_entry_for_candle(trade, row, current_time) - 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] - exits = self.strategy.should_exit( - trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore - enter=enter, exit_=exit_sig, - low=row[LOW_IDX], high=row[HIGH_IDX] - ) - for exit_ in exits: - t = self._get_exit_for_signal(trade, row, exit_, current_time) - if t: - return t - return None + if not trade.has_open_orders: + 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] + exits = self.strategy.should_exit( + trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore + enter=enter, exit_=exit_sig, + low=row[LOW_IDX], high=row[HIGH_IDX] + ) + for exit_ in exits: + t = self._get_exit_for_signal(trade, row, exit_, current_time) + if t: + return t def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False): """ From e5b79eee5a2a06dddfd1ec19cb9de4f802473811 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 09:00:46 +0200 Subject: [PATCH 2/7] Extract _process_exit_order to separate function --- freqtrade/optimize/backtesting.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8670a6b7c..3dbf11408 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -632,6 +632,22 @@ class Backtesting: return True return False + def _process_exit_order( + self, order: Order, trade: LocalTrade, current_time: datetime, row: Tuple, pair: str + ): + if self._try_close_open_order(order, trade, current_time, row): + sub_trade = order.safe_amount_after_fee != trade.amount + if sub_trade: + trade.recalc_trade_from_orders() + else: + trade.close_date = current_time + trade.close(order.ft_price, show_msg=False) + + # logger.debug(f"{pair} - Backtesting exit {trade}") + LocalTrade.close_bt_trade(trade) + self.wallets.update() + self.run_protections(pair, current_time, trade.trade_direction) + def _get_exit_for_signal( self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple, current_time: datetime, @@ -1192,18 +1208,8 @@ class Backtesting: # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) - if order and self._try_close_open_order(order, trade, current_time, row): - sub_trade = order.safe_amount_after_fee != trade.amount - if sub_trade: - trade.recalc_trade_from_orders() - else: - trade.close_date = current_time - trade.close(order.ft_price, show_msg=False) - - # logger.debug(f"{pair} - Backtesting exit {trade}") - LocalTrade.close_bt_trade(trade) - self.wallets.update() - self.run_protections(pair, current_time, trade.trade_direction) + if order: + self._process_exit_order(order, trade, current_time, row, pair) return open_trade_count_start def backtest(self, processed: Dict, From 67636abb30d8e2ec7b59ff7c82313b46336426ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 09:01:05 +0200 Subject: [PATCH 3/7] Fix #10166 with fewer side-effects --- freqtrade/optimize/backtesting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3dbf11408..085af137e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -587,6 +587,9 @@ class Backtesting: exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT, order_tag) pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount) if pos_trade is not None: + order = pos_trade.orders[-1] + # If the order was filled and for the full trade amount, we need to close the trade. + self._process_exit_order(order, pos_trade, current_time, row, trade.pair) return pos_trade return trade @@ -760,7 +763,7 @@ class Backtesting: if self.strategy.position_adjustment_enable: trade = self._get_adjust_trade_entry_for_candle(trade, row, current_time) - if not trade.has_open_orders: + if trade.is_open: 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] exits = self.strategy.should_exit( From c81c07c24acd73c8404c421ea48952d9363dfd12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 09:07:56 +0200 Subject: [PATCH 4/7] Add docstring for process_exit_order --- freqtrade/optimize/backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 085af137e..1f5d96bc4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -638,6 +638,9 @@ class Backtesting: def _process_exit_order( self, order: Order, trade: LocalTrade, current_time: datetime, row: Tuple, pair: str ): + """ + Takes an exit order and processes it, potentially closing the trade. + """ if self._try_close_open_order(order, trade, current_time, row): sub_trade = order.safe_amount_after_fee != trade.amount if sub_trade: From ee7be1cd5a847cededca761a239d8caa533d4344 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 09:14:56 +0200 Subject: [PATCH 5/7] move "add_bt_trade" call for entries into enter_trade function --- freqtrade/optimize/backtesting.py | 3 +-- tests/optimize/test_backtesting.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1f5d96bc4..e16ea8606 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -964,6 +964,7 @@ class Backtesting: contract_size=contract_size, orders=[], ) + LocalTrade.add_bt_trade(trade) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) @@ -1196,8 +1197,6 @@ class Backtesting: # This emulates previous behavior - not sure if this is correct # Prevents entering if the trade-slot was freed in this candle open_trade_count_start += 1 - # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") - LocalTrade.add_bt_trade(trade) self.wallets.update() else: self._collate_rejected(pair, row) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 64dfd1613..4cd56ba76 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -501,34 +501,38 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: # Fake 2 trades, so there's not enough amount for the next trade left. LocalTrade.trades_open.append(trade) - LocalTrade.trades_open.append(trade) backtesting.wallets.update() trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None LocalTrade.trades_open.pop() trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is not None + LocalTrade.trades_open.pop() backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5 backtesting.wallets.update() trade = backtesting._enter_trade(pair, row=row, direction='long') + LocalTrade.trades_open.pop() assert trade assert trade.stake_amount == 123.5 # In case of error - use proposed stake backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 trade = backtesting._enter_trade(pair, row=row, direction='long') + LocalTrade.trades_open.pop() assert trade assert trade.stake_amount == 495 assert trade.is_short is False trade = backtesting._enter_trade(pair, row=row, direction='short') + LocalTrade.trades_open.pop() assert trade assert trade.stake_amount == 495 assert trade.is_short is True mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=300.0) trade = backtesting._enter_trade(pair, row=row, direction='long') + LocalTrade.trades_open.pop() assert trade assert trade.stake_amount == 300.0 From ab93fd3be4d9391f7e60e917636cc671b9443807 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 09:15:15 +0200 Subject: [PATCH 6/7] Enhance trade to verify #10166 --- freqtrade/optimize/backtesting.py | 1 + tests/optimize/test_backtesting_adjust_position.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e16ea8606..59432bd97 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -778,6 +778,7 @@ class Backtesting: t = self._get_exit_for_signal(trade, row, exit_, current_time) if t: return t + return None def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False): """ diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 983e4b47f..cf30489e1 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -213,3 +213,8 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 assert pytest.approx(trade.liquidation_price) == liq_price + + # Adjust to close trade + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-trade.stake_amount) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row_exit, current_time) + assert trade.is_open is False From 866f059d6ac15f603b4149a6131ab5662e857d29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2024 11:25:07 +0200 Subject: [PATCH 7/7] Use FtPrecise to avoid rounding errors --- freqtrade/optimize/backtesting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 59432bd97..131a88b47 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -42,6 +42,7 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.types import BacktestResultType, get_BacktestResultType_default +from freqtrade.util import FtPrecise from freqtrade.util.migrations import migrate_data from freqtrade.wallets import Wallets @@ -575,7 +576,8 @@ class Backtesting: if stake_amount is not None and stake_amount < 0.0: amount = amount_to_contract_precision( - abs(stake_amount * trade.amount / trade.stake_amount), + abs(float(FtPrecise(stake_amount) * FtPrecise(trade.amount) + / FtPrecise(trade.stake_amount))), trade.amount_precision, self.precision_mode, trade.contract_size) if amount == 0.0: