From 4790aaaae1eaa85657674d91b48621539af77711 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 26 Nov 2022 16:58:56 +0000 Subject: [PATCH] Implement cli options for backtesting-analysis date filtering --- docs/advanced-backtesting.md | 15 ++++ docs/utils.md | 8 ++ freqtrade/commands/analyze_commands.py | 8 +- freqtrade/commands/arguments.py | 3 +- freqtrade/commands/cli_options.py | 10 +++ freqtrade/configuration/configuration.py | 6 ++ freqtrade/data/entryexitanalysis.py | 95 ++++++++++++++---------- tests/data/test_entryexitanalysis.py | 9 +++ 8 files changed, 107 insertions(+), 47 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 5c2500f18..78e692f84 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -100,3 +100,18 @@ freqtrade backtesting-analysis -c --analysis-groups 0 2 --enter-re The indicators have to be present in your strategy's main DataFrame (either for your main timeframe or for informative timeframes) otherwise they will simply be ignored in the script output. + +### Filtering the trade output by date + +To show only trades between dates within your backtested timerange, supply the following option(s) in YYYYMMDD format: + +``` +--analysis-date-start : Start date to filter output trades, inclusive. e.g. 20220101 +--analysis-date-end : End date to filter output trades, exclusive. e.g. 20220131 +``` + +For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January: + +```bash +freqtrade backtesting-analysis -c --analysis-date-start 20220101 --analysis-date-end 20220201 +``` diff --git a/docs/utils.md b/docs/utils.md index 3d8a3bd03..e88a13a9a 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -722,6 +722,8 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V] [--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]] [--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]] [--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]] + [--analysis-date-start YYYYMMDD] + [--analysis-date-end YYYYMMDD] optional arguments: -h, --help show this help message and exit @@ -744,6 +746,12 @@ optional arguments: --indicator-list INDICATOR_LIST [INDICATOR_LIST ...] Comma separated list of indicators to analyse. e.g. 'close,rsi,bb_lowerband,profit_abs' + --analysis-date-start YYYYMMDD + Start date to filter trades for analysis (inclusive). e.g. + 20220101 + --analysis-date-end YYYYMMDD + End date to filter trades for analysis (exclusive). e.g. + 20220131 Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py index b6b790788..20afa7ffd 100755 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -60,10 +60,4 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None: logger.info('Starting freqtrade in analysis mode') - process_entry_exit_reasons(config['exportfilename'], - config['exchange']['pair_whitelist'], - config['analysis_groups'], - config['enter_reason_list'], - config['exit_reason_list'], - config['indicator_list'] - ) + process_entry_exit_reasons(config) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 79ab9dafa..159b18439 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -106,7 +106,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop "disableparamexport", "backtest_breakdown"] ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list", - "exit_reason_list", "indicator_list"] + "exit_reason_list", "indicator_list", + "analysis_date_start", "analysis_date_end"] NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-freqaimodels", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 91ac16365..0592b0e53 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -658,6 +658,16 @@ AVAILABLE_CLI_OPTIONS = { nargs='+', default=[], ), + "analysis_date_start": Arg( + "--analysis-date-start", + help=("Start date to filter trades for analysis (inclusive). " + "e.g. '20220101'"), + ), + "analysis_date_end": Arg( + "--analysis-date-end", + help=("End date to filter trades for analysis (exclusive). " + "e.g. '20220131'"), + ), "freqaimodel": Arg( '--freqaimodel', help='Specify a custom freqaimodels.', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4929c023d..4e8abf48e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -462,6 +462,12 @@ class Configuration: self._args_to_config(config, argname='indicator_list', logstring='Analysis indicator list: {}') + self._args_to_config(config, argname='analysis_date_start', + logstring='Analysis filter start date: {}') + + self._args_to_config(config, argname='analysis_date_end', + logstring='Analysis filter end date: {}') + def _process_runmode(self, config: Config) -> None: self._args_to_config(config, argname='dry_run', diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 10969431d..77f14d0c6 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,11 +1,12 @@ import logging +from datetime import datetime from pathlib import Path -from typing import List, Optional import joblib import pandas as pd from tabulate import tabulate +from freqtrade.constants import Config from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) from freqtrade.exceptions import OperationalException @@ -153,55 +154,64 @@ def _do_group_table_output(bigdf, glist): def _select_rows_within_dates(df, date_start=None, date_end=None): + dtfmt = "%Y%m%d" + try: + bool(datetime.strptime(date_start, dtfmt)) + bool(datetime.strptime(date_end, dtfmt)) + except ValueError: + logger.error("Invalid start and/or end date provided. Use YYYYMMDD.") + return None + except TypeError: + return df + if (date_start is not None): df = df.loc[(df['date'] >= date_start)] if (date_end is not None): df = df.loc[(df['date'] < date_end)] - return df -def _select_rows_by_entry_exit_tags(df, enter_reason_list, exit_reason_list): +def _select_rows_by_tags(df, enter_reason_list, exit_reason_list): if enter_reason_list and "all" not in enter_reason_list: df = df.loc[(df['enter_reason'].isin(enter_reason_list))] if exit_reason_list and "all" not in exit_reason_list: df = df.loc[(df['exit_reason'].isin(exit_reason_list))] - return df -def _print_results(analysed_trades, stratname, analysis_groups, - enter_reason_list, exit_reason_list, - indicator_list, columns=None, - date_start=None, date_end=None): - if columns is None: - columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason'] - - bigdf = pd.DataFrame() +def prepare_results(analysed_trades, stratname, + enter_reason_list, exit_reason_list, + date_start=None, date_end=None): + res_df = pd.DataFrame() for pair, trades in analysed_trades[stratname].items(): - bigdf = pd.concat([bigdf, trades], ignore_index=True) + res_df = pd.concat([res_df, trades], ignore_index=True) - bigdf = _select_rows_within_dates(bigdf, date_start, date_end) + res_df = _select_rows_within_dates(res_df, date_start, date_end) - if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns): + if res_df is not None and res_df.shape[0] > 0 and ('enter_reason' in res_df.columns): + res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list) + + return res_df + + +def print_results(res_df, analysis_groups, indicator_list): + if res_df.shape[0] > 0: if analysis_groups: - _do_group_table_output(bigdf, analysis_groups) - - bigdf = _select_rows_by_entry_exit_tags(bigdf, enter_reason_list, exit_reason_list) + _do_group_table_output(res_df, analysis_groups) if "all" in indicator_list: - print(bigdf) + print(res_df) elif indicator_list is not None: available_inds = [] for ind in indicator_list: - if ind in bigdf: + if ind in res_df: available_inds.append(ind) ilist = ["pair", "enter_reason", "exit_reason"] + available_inds - _print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False) + _print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False) else: - print("\\_ No trades to show") + print("\\No trades to show") def _print_table(df, sortcols=None, show_index=False): @@ -220,27 +230,34 @@ def _print_table(df, sortcols=None, show_index=False): ) -def process_entry_exit_reasons(backtest_dir: Path, - pairlist: List[str], - analysis_groups: Optional[List[str]] = ["0", "1", "2"], - enter_reason_list: Optional[List[str]] = ["all"], - exit_reason_list: Optional[List[str]] = ["all"], - indicator_list: Optional[List[str]] = []): +def process_entry_exit_reasons(config: Config): try: - backtest_stats = load_backtest_stats(backtest_dir) + analysis_groups = config.get('analysis_groups', []) + enter_reason_list = config.get('enter_reason_list', ["all"]) + exit_reason_list = config.get('exit_reason_list', ["all"]) + indicator_list = config.get('indicator_list', []) + analysis_date_start = config.get('analysis_date_start', None) + analysis_date_end = config.get('analysis_date_end', None) + + backtest_stats = load_backtest_stats(config['exportfilename']) + for strategy_name, results in backtest_stats['strategy'].items(): - trades = load_backtest_data(backtest_dir, strategy_name) + trades = load_backtest_data(config['exportfilename'], strategy_name) if not trades.empty: - signal_candles = _load_signal_candles(backtest_dir) - analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name, - trades, signal_candles) - _print_results(analysed_trades_dict, - strategy_name, - analysis_groups, - enter_reason_list, - exit_reason_list, - indicator_list) + signal_candles = _load_signal_candles(config['exportfilename']) + analysed_trades_dict = _process_candles_and_indicators( + config['exchange']['pair_whitelist'], strategy_name, + trades, signal_candles) + + res_df = prepare_results(analysed_trades_dict, strategy_name, + enter_reason_list, exit_reason_list, + date_start=analysis_date_start, + date_end=analysis_date_end) + + print_results(res_df, + analysis_groups, + indicator_list) except ValueError as e: raise OperationalException(e) from e diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 588220465..8daca1a67 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -189,3 +189,12 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert '0.5' in captured.out assert '1' in captured.out assert '2.5' in captured.out + + # test date filtering + args = get_args(base_args + + ['--analysis-date-start', "20180129", + '--analysis-date-end', "20180130"] + ) + start_analysis_entries_exits(args) + captured = capsys.readouterr() + assert 'enter_tag_long_b' not in captured.out