diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ee85ac711..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: @@ -588,9 +590,8 @@ class Backtesting: 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() + # 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 @@ -636,6 +637,25 @@ class Backtesting: return True return False + 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: + 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, @@ -748,17 +768,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 + 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( + 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 def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False): @@ -946,6 +967,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) @@ -1178,8 +1200,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) @@ -1196,18 +1216,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, 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 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