Merge pull request #12127 from freqtrade/backtest-filename

Backtest filename
This commit is contained in:
Matthias
2025-08-18 06:47:12 +02:00
committed by GitHub
18 changed files with 177 additions and 97 deletions

View File

@@ -46,29 +46,32 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
More options are available by running with the `-h` option.
### Using export-filename
### Using backtest-filename
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go
back to a previous backtest output, you need to supply the `--export-filename` option.
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
output file. This allows you to keep historical versions of backtest results and re-analyse
them at a later date:
By default, `backtesting-analysis` processes the most recent backtest results in the `user_data/backtest_results` directory.
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
``` bash
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=user_data/backtest-results/backtest-result-2025-03-05_20-38-34.zip
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
```
You should see some output similar to below in the logs with the name of the timestamped
filename that was exported:
```
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json"
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
```
You can then use that filename in `backtesting-analysis`:
```
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json
freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
```
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
``` bash
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
```
### Tuning the buy tags and sell tags to display

View File

@@ -108,7 +108,7 @@ Only use this if you're sure you'll not want to plot or analyze your results fur
Exporting trades to file specifying a custom directory
```bash
freqtrade backtesting --strategy backtesting --export trades --export-filename=user_data/custom-backtest-results
freqtrade backtesting --strategy backtesting --export trades --backtest-directory=user_data/custom-backtest-results
```
---

View File

@@ -3,6 +3,7 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
[-V] [-c PATH] [-d PATH]
[--userdir PATH]
[--backtest-filename PATH]
[--backtest-directory PATH]
[--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]]
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
@@ -16,7 +17,13 @@ options:
-h, --help show this help message and exit
--backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-filename=user_data/backtest_results/`
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]
grouping output - 0: simple wins/losses by enter tag,
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by

View File

@@ -2,6 +2,7 @@
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH]
[--backtest-filename PATH]
[--backtest-directory PATH]
[--show-pair-list]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
@@ -9,7 +10,13 @@ options:
-h, --help show this help message and exit
--backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-filename=user_data/backtest_results/`
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--show-pair-list Show backtesting pairlist sorted by profit.
--breakdown {day,week,month,year} [{day,week,month,year} ...]
Show backtesting breakdown per [day, week, month,

View File

@@ -15,6 +15,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export {none,trades,signals}]
[--backtest-filename PATH]
[--backtest-directory PATH]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
[--cache {none,day,week,month}]
[--freqai-backtest-live-models] [--notes TEXT]
@@ -63,7 +64,13 @@ options:
Export backtest results (default: trades).
--backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-filename=user_data/backtest_results/`
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--breakdown {day,week,month,year} [{day,week,month,year} ...]
Show backtesting breakdown per [day, week, month,
year].

View File

@@ -16,6 +16,7 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--no-color] [--logfile FILE]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export {none,trades,signals}]
[--backtest-filename PATH]
[--backtest-directory PATH]
[--freqai-backtest-live-models]
[--minimum-trade-amount INT]
[--targeted-trade-amount INT]
@@ -62,7 +63,13 @@ options:
Export backtest results (default: trades).
--backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-filename=user_data/backtest_results/`
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--freqai-backtest-live-models
Run backtest with ready models.
--minimum-trade-amount INT

View File

@@ -40,7 +40,10 @@ options:
Export backtest results (default: trades).
--backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-filename=user_data/backtest_results/`
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--timerange TIMERANGE
Specify what timerange of data to use.
-i TIMEFRAME, --timeframe TIMEFRAME

View File

@@ -21,7 +21,10 @@ options:
Export backtest results (default: trades).
--backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-filename=user_data/backtest_results/`
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for

View File

@@ -54,6 +54,7 @@ ARGS_BACKTEST = [
"strategy_list",
"export",
"exportfilename",
"exportdirectory",
"backtest_breakdown",
"backtest_cache",
"freqai_backtest_live_models",
@@ -94,7 +95,12 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column"]
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
ARGS_BACKTEST_SHOW = [
"exportfilename",
"exportdirectory",
"backtest_show_pair_list",
"backtest_breakdown",
]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
@@ -233,6 +239,7 @@ ARGS_HYPEROPT_SHOW = [
ARGS_ANALYZE_ENTRIES_EXITS = [
"exportfilename",
"exportdirectory",
"analysis_groups",
"enter_reason_list",
"exit_reason_list",

View File

@@ -199,21 +199,29 @@ AVAILABLE_CLI_OPTIONS = {
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
nargs="+",
),
"export": Arg(
"--export",
help="Export backtest results (default: trades).",
choices=constants.EXPORT_OPTIONS,
),
"backtest_notes": Arg(
"--notes",
help="Add notes to the backtest results.",
metavar="TEXT",
),
"export": Arg(
"--export",
help="Export backtest results (default: trades).",
choices=constants.EXPORT_OPTIONS,
),
"exportdirectory": Arg(
"--backtest-directory",
"--export-directory",
help="Directory to use for backtest results. "
"Example: `--export-directory=user_data/backtest_results/`. ",
metavar="PATH",
),
"exportfilename": Arg(
"--backtest-filename",
"--export-filename",
help="Use this filename for backtest results."
"Example: `--backtest-filename=user_data/backtest_results/`",
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
metavar="PATH",
),
"disableparamexport": Arg(

View File

@@ -72,7 +72,7 @@ def start_backtesting_show(args: dict[str, Any]) -> None:
from freqtrade.data.btanalysis import load_backtest_stats
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
results = load_backtest_stats(config["exportfilename"])
results = load_backtest_stats(config["exportdirectory"], config["exportfilename"])
show_backtest_results(config, results)
show_sorted_pairlist(config, results)

View File

@@ -209,13 +209,28 @@ class Configuration:
config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
logger.info("Using data directory: %s ...", config.get("datadir"))
self._args_to_config(
config, argname="exportdirectory", logstring="Using {} as backtest directory ..."
)
if self.args.get("exportfilename"):
self._args_to_config(
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
)
config["exportfilename"] = Path(config["exportfilename"])
else:
config["exportfilename"] = config["user_data_dir"] / "backtest_results"
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
logger.warning(
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
"use `--backtest-directory` instead."
)
if config.get("exportdirectory") is None:
# Fallback - assign export-directory directly.
config["exportdirectory"] = config["exportfilename"]
if not config.get("exportdirectory"):
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
if not config.get("exportfilename"):
config["exportfilename"] = None
config["exportdirectory"] = Path(config["exportdirectory"])
if self.args.get("show_sensitive"):
logger.warning(

View File

@@ -155,33 +155,55 @@ def load_backtest_metadata(filename: Path | str) -> dict[str, Any]:
raise OperationalException("Unexpected error while loading backtest metadata.") from e
def load_backtest_stats(filename: Path | str) -> BacktestResultType:
def _normalize_filename(file_or_directory: Path | str, filename: Path | str | None) -> Path:
"""
Normalize the filename by ensuring it is a Path object.
:param file_or_directory: The directory or file to normalize.
:param filename: The filename to normalize.
:return: A Path object representing the normalized filename.
"""
if isinstance(file_or_directory, str):
file_or_directory = Path(file_or_directory)
if file_or_directory.is_dir():
if not filename:
filename = get_latest_backtest_filename(file_or_directory)
if Path(filename).is_file():
fn = Path(filename)
else:
fn = file_or_directory / filename
else:
fn = file_or_directory
return fn
def load_backtest_stats(
file_or_directory: Path | str, filename: Path | str | None = None
) -> BacktestResultType:
"""
Load backtest statistics file.
:param filename: pathlib.Path object, or string pointing to the file.
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
or absolute/relative path to the backtest results file.
:param filename: Optional filename to load from (if different from the main filename).
Only valid when loading from a directory.
:return: a dictionary containing the resulting file.
"""
if isinstance(filename, str):
filename = Path(filename)
if filename.is_dir():
filename = filename / get_latest_backtest_filename(filename)
if not filename.is_file():
raise ValueError(f"File {filename} does not exist.")
logger.info(f"Loading backtest result from {filename}")
fn = _normalize_filename(file_or_directory, filename)
if filename.suffix == ".zip":
if not fn.is_file():
raise ValueError(f"File or directory {fn} does not exist.")
logger.info(f"Loading backtest result from {fn}")
if fn.suffix == ".zip":
data = json_load(
StringIO(
load_file_from_zip(filename, filename.with_suffix(".json").name).decode("utf-8")
)
StringIO(load_file_from_zip(fn, fn.with_suffix(".json").name).decode("utf-8"))
)
else:
with filename.open() as file:
with fn.open() as file:
data = json_load(file)
# Legacy list format does not contain metadata.
if isinstance(data, dict):
data["metadata"] = load_backtest_metadata(filename)
data["metadata"] = load_backtest_metadata(fn)
return data
@@ -362,16 +384,21 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
return df
def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.DataFrame:
def load_backtest_data(
file_or_directory: Path | str, strategy: str | None = None, filename: Path | str | None = None
) -> pd.DataFrame:
"""
Load backtest data file.
:param filename: pathlib.Path object, or string pointing to a file or directory
Load backtest data file, returns a dataframe with the individual trades.
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
or absolute/relative path to the backtest results file.
:param strategy: Strategy to load - mainly relevant for multi-strategy backtests
Can also serve as protection to load the correct result.
:param filename: Optional filename to load from (if different from the main filename).
Only valid when loading from a directory.
:return: a dataframe with the analysis results
:raise: ValueError if loading goes wrong.
"""
data = load_backtest_stats(filename)
data = load_backtest_stats(file_or_directory, filename)
if not isinstance(data, list):
# new, nested format
if "strategy" not in data:
@@ -430,20 +457,23 @@ def load_file_from_zip(zip_path: Path, filename: str) -> bytes:
raise ValueError(f"Bad zip file: {zip_path}.") from None
def load_backtest_analysis_data(backtest_dir: Path, name: Literal["signals", "rejected", "exited"]):
def load_backtest_analysis_data(
file_or_directory: Path,
name: Literal["signals", "rejected", "exited"],
filename: Path | str | None = None,
):
"""
Load backtest analysis data either from a pickle file or from within a zip file
:param backtest_dir: Directory containing backtest results
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
or absolute/relative path to the backtest results file.
:param name: Name of the analysis data to load (signals, rejected, exited)
:param filename: Optional filename to load from (if different from the main filename).
Only valid when loading from a directory.
:return: Analysis data
"""
import joblib
if backtest_dir.is_dir():
lbf = Path(get_latest_backtest_filename(backtest_dir))
zip_path = backtest_dir / lbf
else:
zip_path = backtest_dir
zip_path = _normalize_filename(file_or_directory, filename)
if zip_path.suffix == ".zip":
# Load from zip file
@@ -458,10 +488,10 @@ def load_backtest_analysis_data(backtest_dir: Path, name: Literal["signals", "re
else:
# Load from separate pickle file
if backtest_dir.is_dir():
scpf = Path(backtest_dir, f"{zip_path.stem}_{name}.pkl")
if file_or_directory.is_dir():
scpf = Path(file_or_directory, f"{zip_path.stem}_{name}.pkl")
else:
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
scpf = Path(file_or_directory.parent / f"{file_or_directory.stem}_{name}.pkl")
try:
with scpf.open("rb") as scp:

View File

@@ -330,7 +330,7 @@ def process_entry_exit_reasons(config: Config):
do_rejected = config.get("analysis_rejected", False)
to_csv = config.get("analysis_to_csv", False)
csv_path = Path(
config.get("analysis_csv_path", config["exportfilename"]), # type: ignore[arg-type]
config.get("analysis_csv_path", config["exportdirectory"]), # type: ignore[arg-type]
)
if entry_only is True and exit_only is True:
@@ -344,21 +344,29 @@ def process_entry_exit_reasons(config: Config):
None if config.get("timerange") is None else str(config.get("timerange"))
)
try:
backtest_stats = load_backtest_stats(config["exportfilename"])
backtest_stats = load_backtest_stats(
config["exportdirectory"], config["exportfilename"]
)
except ValueError as e:
raise ConfigurationError(e) from e
for strategy_name, results in backtest_stats["strategy"].items():
trades = load_backtest_data(config["exportfilename"], strategy_name)
trades = load_backtest_data(
config["exportdirectory"], strategy_name, config["exportfilename"]
)
if trades is not None and not trades.empty:
signal_candles = load_backtest_analysis_data(config["exportfilename"], "signals")
exit_signals = load_backtest_analysis_data(config["exportfilename"], "exited")
signal_candles = load_backtest_analysis_data(
config["exportdirectory"], "signals", config["exportfilename"]
)
exit_signals = load_backtest_analysis_data(
config["exportdirectory"], "exited", config["exportfilename"]
)
rej_df = None
if do_rejected:
rejected_signals_dict = load_backtest_analysis_data(
config["exportfilename"], "rejected"
config["exportdirectory"], "rejected", config["exportfilename"]
)
rej_df = prepare_results(
rejected_signals_dict,

View File

@@ -64,7 +64,7 @@ def store_backtest_results(
:param market_change_data: Dataframe containing market change data
:param analysis_results: Dictionary containing analysis results
"""
recordfilename: Path = config["exportfilename"]
recordfilename: Path = config["exportdirectory"]
zip_filename = _generate_filename(recordfilename, dtappendix, ".zip")
base_filename = _generate_filename(recordfilename, dtappendix, "")
json_filename = _generate_filename(recordfilename, dtappendix, ".json")

View File

@@ -1862,8 +1862,10 @@ def test_backtesting_show(mocker, testdatadir, capsys):
sbr = mocker.patch("freqtrade.optimize.optimize_reports.show_backtest_results")
args = [
"backtesting-show",
"--export-directory",
f"{testdatadir / 'backtest_results'}",
"--export-filename",
f"{testdatadir / 'backtest_results/backtest-result.json'}",
"backtest-result.json",
"--show-pair-list",
]
pargs = get_args(args)

View File

@@ -236,7 +236,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
filename_last = tmp_path / LAST_BT_RESULT_FN
_backup_file(filename_last, copy_file=True)
assert not filename.is_file()
default_conf["exportfilename"] = filename
default_conf["exportdirectory"] = filename
store_backtest_results(default_conf, stats, "2022_01_01_15_05_13")
@@ -263,7 +263,7 @@ def test_store_backtest_results(testdatadir, mocker):
zip_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.ZipFile")
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_results(
{"exportfilename": testdatadir, "original_config": {}}, data, "2022_01_01_15_05_13"
{"exportdirectory": testdatadir, "original_config": {}}, data, "2022_01_01_15_05_13"
)
assert dump_mock.call_count == 2
@@ -275,7 +275,7 @@ def test_store_backtest_results(testdatadir, mocker):
zip_mock.reset_mock()
filename = testdatadir / "testresult.json"
store_backtest_results(
{"exportfilename": filename, "original_config": {}}, data, "2022_01_01_15_05_13"
{"exportdirectory": filename, "original_config": {}}, data, "2022_01_01_15_05_13"
)
assert dump_mock.call_count == 2
assert zip_mock.call_count == 1
@@ -287,7 +287,7 @@ def test_store_backtest_results(testdatadir, mocker):
def test_store_backtest_results_real(tmp_path, caplog):
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
config = {
"exportfilename": tmp_path,
"exportdirectory": tmp_path,
"original_config": {},
}
store_backtest_results(
@@ -356,7 +356,7 @@ def test_write_read_backtest_candles(tmp_path):
bt_results = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
mock_conf = {
"exportfilename": tmp_path,
"exportdirectory": tmp_path,
"export": "signals",
"runmode": "backtest",
"original_config": {},
@@ -393,33 +393,6 @@ def test_write_read_backtest_candles(tmp_path):
_clean_test_file(stored_file)
# test file exporting
filename = tmp_path / "testresult"
mock_conf["exportfilename"] = filename
store_backtest_results(mock_conf, bt_results, sample_date, analysis_results=data)
stored_file = tmp_path / f"testresult-{sample_date}.zip"
signals_pkl = f"testresult-{sample_date}_signals.pkl"
rejected_pkl = f"testresult-{sample_date}_rejected.pkl"
exited_pkl = f"testresult-{sample_date}_exited.pkl"
assert not (tmp_path / signals_pkl).is_file()
assert stored_file.is_file()
with ZipFile(stored_file, "r") as zipf:
assert signals_pkl in zipf.namelist()
assert rejected_pkl in zipf.namelist()
assert exited_pkl in zipf.namelist()
with zipf.open(signals_pkl) as scp:
pickled_signal_candles2 = joblib.load(scp)
assert pickled_signal_candles2.keys() == candle_dict.keys()
assert pickled_signal_candles2["DefStrat"].keys() == pickled_signal_candles2["DefStrat"].keys()
assert pickled_signal_candles2["DefStrat"]["UNITTEST/BTC"].equals(
pickled_signal_candles2["DefStrat"]["UNITTEST/BTC"]
)
_clean_test_file(stored_file)
def test_generate_pair_metrics():
results = pd.DataFrame(

View File

@@ -2802,8 +2802,8 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmp_path):
ftbot.config["export"] = "trades"
ftbot.config["backtest_cache"] = "day"
ftbot.config["user_data_dir"] = tmp_path
ftbot.config["exportfilename"] = tmp_path / "backtest_results"
ftbot.config["exportfilename"].mkdir()
ftbot.config["exportdirectory"] = tmp_path / "backtest_results"
ftbot.config["exportdirectory"].mkdir()
# start backtesting
data = {