Merge pull request #12315 from x-mass/develop

fix: align _get_close_rate_for_roi with calc_profit_ratio logic in backtesting
This commit is contained in:
Matthias
2026-02-01 17:23:13 +01:00
committed by GitHub
3 changed files with 76 additions and 6 deletions

View File

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

View File

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

View File

@@ -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}"
)