diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8cbdd0dea..26fd3f454 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -605,8 +605,6 @@ class Backtesting: trade_dur: int, ) -> float: is_short = trade.is_short or False - leverage = trade.leverage or 1.0 - side_1 = -1 if is_short else 1 roi_entry, roi = self.strategy.min_roi_reached_entry( trade, # type: ignore[arg-type] trade_dur, @@ -619,10 +617,7 @@ class Backtesting: # - we'll use open instead of close return row[OPEN_IDX] - # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) - roi_rate = trade.open_rate * roi / leverage - open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open) - close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1) + close_rate = trade.calc_close_rate_for_roi(roi) if is_short: is_new_roi = row[OPEN_IDX] < close_rate else: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 85f072747..e6107eac6 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1208,6 +1208,35 @@ class LocalTrade: return float(f"{profit_ratio:.8f}") + def calc_close_rate_for_roi(self, target_roi: float) -> float: + """ + Calculate the required close price to reach a target ROI. + Must match the logic used in `calc_profit_ratio()`. + + :param target_roi: The desired return on investment (as a decimal, e.g., 0.05 for 5%) + :return: Close price (rate) required to achieve the target ROI + """ + leverage = float(self.leverage or 1.0) + deleveraged_roi = float(target_roi) / leverage + + open_value = self._calc_open_trade_value(self.amount, self.open_rate) + + # The ROI formula uses close_value(rate), which depends on trading mode: + # - SPOT: linear in rate, adjusted by close fee + # - MARGIN: same, but long subtracts interest, short increases amount + # - FUTURES: adds/subtracts funding to/from close value + # All cases are affine in rate: + # close_value(rate) = a * rate + b + # We extract a and b by probing close_value at rate = 0 and 1. + value_at_0 = self.calc_close_trade_value(0.0) + value_at_1 = self.calc_close_trade_value(1.0) + alpha = value_at_1 - value_at_0 + beta = value_at_0 + + s = -1.0 if self.is_short else 1.0 + adj = 1.0 + (deleveraged_roi / s) + return (adj * open_value - beta) / alpha + def recalc_trade_from_orders(self, *, is_closing: bool = False): ZERO = FtPrecise(0.0) current_amount = FtPrecise(0.0) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 6e779137d..d75b13ced 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2893,3 +2893,49 @@ def test_recalc_trade_from_orders_dca(data) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert not trade.has_open_orders + + +@pytest.mark.parametrize( + "is_short,lev,trading_mode", + [ + (False, 1, spot), + (False, 1, margin), + (False, 10, margin), + (False, 1, futures), + (False, 10, futures), + (True, 1, margin), + (True, 10, margin), + (True, 1, futures), + (True, 10, futures), + ], +) +@pytest.mark.usefixtures("init_persistence") +def test_close_rate_for_roi(fee, is_short, lev, trading_mode): + """ + Ensure calc_close_rate_for_roi is consistent with calc_profit_ratio. + """ + open_dt = datetime.fromisoformat("2022-01-01 00:00:00") + trade_duration = timedelta(days=10) + trade = Trade( + id=2, + pair="ADA/USDT", + stake_amount=60.0, + open_rate=2.0, + amount=30.0, + is_open=True, + open_date=open_dt, + close_date=open_dt + trade_duration, # to trigger interest calculation in margin mode + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange="binance", + is_short=is_short, + leverage=lev, + trading_mode=trading_mode, + interest_rate=0.0005, + funding_fees=0.1234, + ) + for roi in [0.1337, 0.5, -0.1, 0.25]: + close_rate = trade.calc_close_rate_for_roi(roi) + assert roi == trade.calc_profit_ratio(close_rate), ( + f"Failed for ROI {roi}, close_rate {close_rate}" + )