Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bae2f98e5f | |||
| e88e3617c7 | |||
| 7cd97d9cfd | |||
| 12a9f0e1b1 | |||
| 6bb78edd96 | |||
| 98b56a49c8 | |||
| 791fbe8054 | |||
| cedfb2a51f | |||
| 93b6c52aad | |||
| 0ef85e161e | |||
| 26aed91bbf | |||
| 3c2ddda203 | |||
| beec9123d4 | |||
| ea6d23bd2d |
+4
-6
@@ -16,7 +16,8 @@ RUN mkdir /freqtrade \
|
||||
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||
&& pip install --upgrade pip
|
||||
|
||||
WORKDIR /freqtrade
|
||||
|
||||
@@ -24,14 +25,11 @@ WORKDIR /freqtrade
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip wheel
|
||||
|
||||
&& apt-get clean
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<3.0" \
|
||||
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
RUN pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
|
||||
# Copy dependencies to runtime-image
|
||||
FROM base AS runtime-image
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.14-slim-bookworm AS base
|
||||
FROM python:3.13.11-slim-trixie AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG=C.UTF-8
|
||||
@@ -13,7 +13,7 @@ RUN mkdir /freqtrade \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install sudo libatlas3-base libopenblas-dev curl sqlite3 libutf8proc-dev libsnappy-dev \
|
||||
&& apt-get clean \
|
||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||
@@ -24,12 +24,12 @@ WORKDIR /freqtrade
|
||||
# Install dependencies
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
COPY build_helpers/*.whl /tmp/
|
||||
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2026.1-dev"
|
||||
__version__ = "2026.2-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -2896,8 +2896,11 @@ class Exchange:
|
||||
}
|
||||
pairs_to_download = [p for p in pairs if p not in candles]
|
||||
if pairs_to_download:
|
||||
candles = self.refresh_latest_ohlcv(pairs_to_download, since_ms=since_ms, cache=False)
|
||||
for c, val in candles.items():
|
||||
candles_new = self.refresh_latest_ohlcv(
|
||||
pairs_to_download, since_ms=since_ms, cache=False
|
||||
)
|
||||
for c, val in candles_new.items():
|
||||
candles[c] = val
|
||||
self._expiring_candle_cache[(c[1], since_ms)][c] = val
|
||||
return candles
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from freqtrade_client.ft_rest_client import FtRestClient
|
||||
|
||||
|
||||
__version__ = "2026.1-dev"
|
||||
__version__ = "2026.2-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -2794,8 +2794,10 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
("LTC/BTC", "1h", CandleType.SPOT),
|
||||
]
|
||||
|
||||
ohlcv_data = {p: ohlcv for p in pairs}
|
||||
ohlcv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data)
|
||||
def ohlcv_side_effect(requested_pairs, *args, **kwargs):
|
||||
return {p: ohlcv for p in requested_pairs}
|
||||
|
||||
ohlcv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", side_effect=ohlcv_side_effect)
|
||||
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
@@ -2813,6 +2815,14 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
ohlcv_mock.reset_mock()
|
||||
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||
assert ohlcv_mock.call_count == 0
|
||||
assert len(res) == 5
|
||||
|
||||
# # re-run with one additional pair
|
||||
res = exchange.refresh_ohlcv_with_cache(
|
||||
pairs + [("NEW/PAIR", "1d", CandleType.SPOT)], start.timestamp()
|
||||
)
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(res) == 6
|
||||
|
||||
# Expire 5m cache
|
||||
time_machine.move_to(start + timedelta(minutes=6), tick=False)
|
||||
@@ -2821,6 +2831,7 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(ohlcv_mock.call_args_list[0][0][0]) == 1
|
||||
assert len(res) == 5
|
||||
|
||||
# Expire 5m and 1h cache
|
||||
time_machine.move_to(start + timedelta(hours=2), tick=False)
|
||||
@@ -2829,6 +2840,7 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(ohlcv_mock.call_args_list[0][0][0]) == 2
|
||||
assert len(res) == 5
|
||||
|
||||
# Expire all caches
|
||||
time_machine.move_to(start + timedelta(days=1, hours=2), tick=False)
|
||||
@@ -2838,6 +2850,7 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(ohlcv_mock.call_args_list[0][0][0]) == 5
|
||||
assert ohlcv_mock.call_args_list[0][0][0] == pairs
|
||||
assert len(res) == 5
|
||||
|
||||
|
||||
def test_refresh_latest_ohlcv_funding_rate(mocker, default_conf_usdt, caplog) -> None:
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user