Merge branch 'develop' into pr/mrpabloyeah/11561

This commit is contained in:
Matthias
2025-04-02 07:05:36 +02:00
27 changed files with 1592 additions and 94 deletions

View File

@@ -73,17 +73,17 @@ jobs:
python build_helpers/freqtrade_client_version_align.py
- name: Tests
if: (!(runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04'))
if: (!(runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04'))
run: |
pytest --random-order
- name: Tests with Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
run: |
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
@@ -139,6 +139,7 @@ jobs:
ruff format --check
- name: Mypy
if: matrix.os == 'ubuntu-24.04'
run: |
mypy freqtrade scripts tests
@@ -264,6 +265,7 @@ jobs:
ruff format --check
- name: Mypy
if: matrix.os == 'macos-15'
run: |
mypy freqtrade scripts

View File

@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pycqa/flake8
rev: "7.1.2"
rev: "7.2.0"
hooks:
- id: flake8
additional_dependencies: [Flake8-pyproject]
@@ -16,10 +16,10 @@ repos:
additional_dependencies:
- types-cachetools==5.5.0.20240820
- types-filelock==3.2.7
- types-requests==2.32.0.20250306
- types-requests==2.32.0.20250328
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20241206
- SQLAlchemy==2.0.39
- SQLAlchemy==2.0.40
# stages: [push]
- repo: https://github.com/pycqa/isort

View File

@@ -435,6 +435,20 @@ To save time, by default backtest will reuse a cached result from within the las
To further analyze your backtest results, freqtrade will export the trades to file by default.
You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section.
### Backtest output file
The output file freqtrade produces is a zip file containing the following files:
- The backtest report in json format
- the market change data in feather format
- a copy of the strategy file
- a copy of the strategy parameters (if a parameter file was used)
- a sanitized copy of the config file
This will ensure results are reproducible - under the assumption that the same data is available.
Only the strategy file and the config file are included in the zip file, eventual dependencies are not included.
## Assumptions made by backtesting
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:

View File

@@ -363,6 +363,10 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
### Historic Hyperliquid data
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.

View File

@@ -258,6 +258,8 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file.
This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
### Structure
#### Model

View File

@@ -1,6 +1,6 @@
markdown==3.7
mkdocs==1.6.1
mkdocs-material==9.6.9
mkdocs-material==9.6.10
mdx_truly_sane_lists==1.3
pymdown-extensions==10.14.3
jinja2==3.1.6

View File

@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2025.3-dev"
__version__ = "2025.4-dev"
if "dev" in __version__:
from pathlib import Path

View File

@@ -118,7 +118,7 @@ def _calc_drawdown_series(
) -> pd.DataFrame:
max_drawdown_df = pd.DataFrame()
max_drawdown_df["cumulative"] = profit_results[value_col].cumsum()
max_drawdown_df["high_value"] = max_drawdown_df["cumulative"].cummax()
max_drawdown_df["high_value"] = np.maximum(0, max_drawdown_df["cumulative"].cummax())
max_drawdown_df["drawdown"] = max_drawdown_df["cumulative"] - max_drawdown_df["high_value"]
max_drawdown_df["date"] = profit_results.loc[:, date_col]
if starting_balance:

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@ class FtHas(TypedDict, total=False):
funding_fee_timeframe: str
funding_fee_candle_limit: int
floor_leverage: bool
uses_leverage_tiers: bool
needs_trading_fees: bool
order_props_in_contracts: list[Literal["amount", "cost", "filled", "remaining"]]

View File

@@ -35,6 +35,7 @@ class Hyperliquid(Exchange):
"stop_price_prop": "stopPrice",
"funding_fee_timeframe": "1h",
"funding_fee_candle_limit": 500,
"uses_leverage_tiers": False,
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [

View File

@@ -69,7 +69,7 @@ class Kraken(Exchange):
consolidated: CcxtBalances = {}
for currency, balance in balances.items():
base_currency = currency[:-2] if currency.endswith(".F") else currency
base_currency = self._api.commonCurrencies.get(base_currency, base_currency)
if base_currency in consolidated:
consolidated[base_currency]["free"] += balance["free"]
consolidated[base_currency]["used"] += balance["used"]

View File

@@ -1,4 +1,5 @@
from typing import Any
from copy import deepcopy
from typing import Any, cast
from typing_extensions import TypedDict
@@ -15,11 +16,16 @@ class BacktestResultType(TypedDict):
def get_BacktestResultType_default() -> BacktestResultType:
return {
"metadata": {},
"strategy": {},
"strategy_comparison": [],
}
return cast(
BacktestResultType,
deepcopy(
{
"metadata": {},
"strategy": {},
"strategy_comparison": [],
}
),
)
class BacktestHistoryEntryType(BacktestMetadataType):

View File

@@ -360,8 +360,9 @@ class Backtesting:
)
# Combine data to avoid combining the data per trade.
unavailable_pairs = []
uses_leverage_tiers = self.exchange.get_option("uses_leverage_tiers", True)
for pair in self.pairlists.whitelist:
if pair not in self.exchange._leverage_tiers:
if uses_leverage_tiers and pair not in self.exchange._leverage_tiers:
unavailable_pairs.append(pair)
continue
@@ -1792,6 +1793,7 @@ class Backtesting:
dt_appendix,
market_change_data=combined_res,
analysis_results=self.analysis_results,
strategy_files={s.get_strategy_name(): s.__file__ for s in self.strategylist},
)
# Results may be mixed up now. Sort them so they follow --strategy-list order.

View File

@@ -6,6 +6,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
from pandas import DataFrame
from freqtrade.configuration import sanitize_config
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.enums.runmode import RunMode
from freqtrade.ft_types import BacktestResultType
@@ -52,6 +53,7 @@ def store_backtest_results(
*,
market_change_data: DataFrame | None = None,
analysis_results: dict[str, dict[str, DataFrame]] | None = None,
strategy_files: dict[str, str] | None = None,
) -> Path:
"""
Stores backtest results and analysis data in a zip file, with metadata stored separately
@@ -85,6 +87,32 @@ def store_backtest_results(
dump_json_to_file(stats_buf, stats_copy)
zipf.writestr(json_filename.name, stats_buf.getvalue())
config_buf = StringIO()
dump_json_to_file(config_buf, sanitize_config(config["original_config"]))
zipf.writestr(f"{base_filename.stem}_config.json", config_buf.getvalue())
for strategy_name, strategy_file in (strategy_files or {}).items():
# Store the strategy file and its parameters
strategy_buf = BytesIO()
strategy_path = Path(strategy_file)
if not strategy_path.is_file():
logger.warning(f"Strategy file '{strategy_path}' does not exist. Skipping.")
continue
with strategy_path.open("rb") as strategy_file_obj:
strategy_buf.write(strategy_file_obj.read())
strategy_buf.seek(0)
zipf.writestr(f"{base_filename.stem}_{strategy_name}.py", strategy_buf.getvalue())
strategy_params = strategy_path.with_suffix(".json")
if strategy_params.is_file():
strategy_params_buf = BytesIO()
with strategy_params.open("rb") as strategy_params_obj:
strategy_params_buf.write(strategy_params_obj.read())
strategy_params_buf.seek(0)
zipf.writestr(
f"{base_filename.stem}_{strategy_name}.json",
strategy_params_buf.getvalue(),
)
# Add market change data if present
if market_change_data is not None:
market_change_name = f"{base_filename.stem}_market_change.feather"

View File

@@ -18,7 +18,7 @@ from freqtrade.data.metrics import (
calculate_sortino,
calculate_sqn,
)
from freqtrade.ft_types import BacktestResultType
from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
@@ -644,11 +644,7 @@ def generate_backtest_stats(
:param max_date: Backtest end date
:return: Dictionary containing results per strategy and a strategy summary.
"""
result: BacktestResultType = {
"metadata": {},
"strategy": {},
"strategy_comparison": [],
}
result: BacktestResultType = get_BacktestResultType_default()
market_change = calculate_market_change(btdata, "close")
metadata = {}
pairlist = list(btdata.keys())

View File

@@ -108,6 +108,9 @@ def __run_backtest_bg(btconfig: Config):
ApiBG.bt["bt"].results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
market_change_data=combined_res,
strategy_files={
s.get_strategy_name(): s.__file__ for s in ApiBG.bt["bt"].strategylist
},
)
ApiBG.bt["bt"].results["metadata"][strategy_name]["filename"] = str(fn.stem)
ApiBG.bt["bt"].results["metadata"][strategy_name]["strategy"] = strategy_name

View File

@@ -132,6 +132,7 @@ class IStrategy(ABC, HyperStrategyMixin):
stake_currency: str
# container variable for strategy source code
__source__: str = ""
__file__: str = ""
# Definition of plot_config. See plotting documentation for more details.
plot_config: dict = {}

View File

@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.3-dev"
__version__ = "2025.4-dev"
if "dev" in __version__:
from pathlib import Path

View File

@@ -11,7 +11,7 @@ ruff==0.11.2
mypy==1.15.0
pre-commit==4.2.0
pytest==8.3.5
pytest-asyncio==0.25.3
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-random-order==1.1.1
@@ -27,6 +27,6 @@ nbconvert==7.16.6
# mypy types
types-cachetools==5.5.0.20240820
types-filelock==3.2.7
types-requests==2.32.0.20250306
types-requests==2.32.0.20250328
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20241206

View File

@@ -2,7 +2,6 @@
-r requirements-freqai.txt
# Required for freqai-rl
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
torch==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos

View File

@@ -4,14 +4,14 @@ bottleneck==1.4.2
numexpr==2.10.2
pandas-ta==0.3.14b
ccxt==4.4.69
ccxt==4.4.71
cryptography==44.0.2
aiohttp==3.9.5
SQLAlchemy==2.0.39
SQLAlchemy==2.0.40
python-telegram-bot==22.0
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.12.1
humanize==4.12.2
cachetools==5.5.2
requests==2.32.3
urllib3==2.3.0
@@ -22,7 +22,7 @@ tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.4.2
rich==13.9.4
rich==14.0.0
pyarrow==19.0.1; platform_machine != 'armv7l'
# find first, C search in arrays
@@ -31,14 +31,14 @@ py_find_1st==1.1.7
# Load ticker files 30% faster
python-rapidjson==1.20
# Properly format api responses
orjson==3.10.15
orjson==3.10.16
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.115.12
pydantic==2.10.6
pydantic==2.11.1
uvicorn==0.34.0
pyjwt==2.10.1
aiofiles==24.1.0
@@ -49,7 +49,7 @@ questionary==2.1.0
prompt-toolkit==3.0.50
# Extensions to datetime library
python-dateutil==2.9.0.post0
pytz==2025.1
pytz==2025.2
#Futures
schedule==1.2.2

View File

@@ -652,6 +652,7 @@ def get_default_conf(testdatadir):
"trading_mode": "spot",
"margin_mode": "",
"candle_type_def": CandleType.SPOT,
"original_config": {},
}
return configuration

View File

@@ -569,7 +569,7 @@ def test_calculate_max_drawdown2():
df1.loc[:, "profit"] = df1["profit"] * -1
# No winning trade ...
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.043965
assert drawdown.drawdown_abs == 0.055545
@pytest.mark.parametrize(

View File

@@ -71,7 +71,7 @@ def test_get_balances_prod_kraken(default_conf, mocker):
"4TH": balance_item.copy(),
"EUR": balance_item.copy(),
"BTC": {"free": 0.0, "total": 0.0, "used": 0.0},
"XBT.F": balance_item.copy(),
"BTC.F": balance_item.copy(),
"timestamp": 123123,
}
)

View File

@@ -156,7 +156,7 @@ EXCHANGES = {
"ADA.F": {"free": 2.0, "total": 2.0, "used": 0.0},
"BTC": {"free": 0.0006, "total": 0.0006, "used": 0.0},
# XBT.F should be mapped to BTC.F
"XBT.F": {"free": 0.001, "total": 0.001, "used": 0.0},
"BTC.F": {"free": 0.001, "total": 0.001, "used": 0.0},
},
},
},

View File

@@ -1,5 +1,6 @@
import json
import re
import shutil
from datetime import timedelta
from pathlib import Path
from shutil import copyfile
@@ -41,7 +42,7 @@ from freqtrade.optimize.optimize_reports.optimize_reports import (
from freqtrade.resolvers.strategy_resolver import StrategyResolver
from freqtrade.util import dt_ts
from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc
from tests.conftest import CURRENT_TEST_STRATEGY
from tests.conftest import CURRENT_TEST_STRATEGY, log_has_re
from tests.data.test_history import _clean_test_file
@@ -263,8 +264,9 @@ def test_store_backtest_results(testdatadir, mocker):
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_json")
zip_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.ZipFile")
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_results({"exportfilename": testdatadir}, data, "2022_01_01_15_05_13")
store_backtest_results(
{"exportfilename": testdatadir, "original_config": {}}, data, "2022_01_01_15_05_13"
)
assert dump_mock.call_count == 2
assert zip_mock.call_count == 1
@@ -274,7 +276,9 @@ def test_store_backtest_results(testdatadir, mocker):
dump_mock.reset_mock()
zip_mock.reset_mock()
filename = testdatadir / "testresult.json"
store_backtest_results({"exportfilename": filename}, data, "2022_01_01_15_05_13")
store_backtest_results(
{"exportfilename": filename, "original_config": {}}, data, "2022_01_01_15_05_13"
)
assert dump_mock.call_count == 2
assert zip_mock.call_count == 1
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@@ -282,9 +286,16 @@ def test_store_backtest_results(testdatadir, mocker):
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / "testresult"))
def test_store_backtest_results_real(tmp_path):
def test_store_backtest_results_real(tmp_path, caplog):
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_results({"exportfilename": tmp_path}, data, "2022_01_01_15_05_13")
config = {
"exportfilename": tmp_path,
"original_config": {},
}
store_backtest_results(
config, data, "2022_01_01_15_05_13", strategy_files={"DefStrat": "NoFile"}
)
assert log_has_re(r"Strategy file .* does not exist\. Skipping\.", caplog)
zip_file = tmp_path / "backtest-result-2022_01_01_15_05_13.zip"
assert zip_file.is_file()
@@ -297,8 +308,19 @@ def test_store_backtest_results_real(tmp_path):
fn = get_latest_backtest_filename(tmp_path)
assert fn == "backtest-result-2022_01_01_15_05_13.zip"
strategy_test_dir = Path(__file__).parent.parent / "strategy" / "strats"
shutil.copy(strategy_test_dir / "strategy_test_v3.py", tmp_path)
params_file = tmp_path / "strategy_test_v3.json"
with params_file.open("w") as f:
f.write("""{"strategy_name": "TurtleStrategyX5","params":{}}""")
store_backtest_results(
{"exportfilename": tmp_path}, data, "2024_01_01_15_05_25", market_change_data=pd.DataFrame()
config,
data,
"2024_01_01_15_05_25",
market_change_data=pd.DataFrame(),
strategy_files={"DefStrat": str(tmp_path / "strategy_test_v3.py")},
)
zip_file = tmp_path / "backtest-result-2024_01_01_15_05_25.zip"
assert zip_file.is_file()
@@ -308,6 +330,22 @@ def test_store_backtest_results_real(tmp_path):
with ZipFile(zip_file, "r") as zipf:
assert "backtest-result-2024_01_01_15_05_25.json" in zipf.namelist()
assert "backtest-result-2024_01_01_15_05_25_market_change.feather" in zipf.namelist()
assert "backtest-result-2024_01_01_15_05_25_config.json" in zipf.namelist()
# strategy file is copied to the zip file
assert "backtest-result-2024_01_01_15_05_25_DefStrat.py" in zipf.namelist()
# compare the content of the strategy file
with zipf.open("backtest-result-2024_01_01_15_05_25_DefStrat.py") as strategy_file:
strategy_content = strategy_file.read()
with (strategy_test_dir / "strategy_test_v3.py").open("rb") as original_file:
original_content = original_file.read()
assert strategy_content == original_content
assert "backtest-result-2024_01_01_15_05_25_DefStrat.py" in zipf.namelist()
with zipf.open("backtest-result-2024_01_01_15_05_25_DefStrat.json") as pf:
params_content = pf.read()
with params_file.open("rb") as original_file:
original_content = original_file.read()
assert params_content == original_content
assert (tmp_path / LAST_BT_RESULT_FN).is_file()
# Last file reference should be updated
@@ -323,6 +361,7 @@ def test_write_read_backtest_candles(tmp_path):
"exportfilename": tmp_path,
"export": "signals",
"runmode": "backtest",
"original_config": {},
}
# test directory exporting
sample_date = "2022_01_01_15_05_13"