Merge pull request #10169 from freqtrade/fix/issue_10166

Improve backtest behavior with adjust_trade_position
This commit is contained in:
Matthias
2024-05-05 09:40:29 +02:00
committed by GitHub
3 changed files with 49 additions and 30 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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