Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions freqtrade/optimize/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,6 @@ def _get_close_rate_for_roi(
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,
Expand All @@ -619,10 +617,7 @@ def _get_close_rate_for_roi(
# - 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:
Expand Down
29 changes: 29 additions & 0 deletions freqtrade/persistence/trade_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,35 @@ def calc_profit_ratio(

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)
Expand Down
46 changes: 46 additions & 0 deletions tests/persistence/test_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
Loading