From 391817243cab8ba944933e19c26280caebf1baf0 Mon Sep 17 00:00:00 2001 From: froggleston Date: Fri, 25 Nov 2022 16:12:15 +0000 Subject: [PATCH 1/4] Tidy up complex functions --- freqtrade/data/entryexitanalysis.py | 31 +++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index b22c3f87e..10969431d 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -152,9 +152,30 @@ def _do_group_table_output(bigdf, glist): logger.warning("Invalid group mask specified.") +def _select_rows_within_dates(df, date_start=None, date_end=None): + 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): + 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): + 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'] @@ -162,15 +183,13 @@ def _print_results(analysed_trades, stratname, analysis_groups, for pair, trades in analysed_trades[stratname].items(): bigdf = pd.concat([bigdf, trades], ignore_index=True) + bigdf = _select_rows_within_dates(bigdf, date_start, date_end) + if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns): if analysis_groups: _do_group_table_output(bigdf, analysis_groups) - if enter_reason_list and "all" not in enter_reason_list: - bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))] - - if exit_reason_list and "all" not in exit_reason_list: - bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))] + bigdf = _select_rows_by_entry_exit_tags(bigdf, enter_reason_list, exit_reason_list) if "all" in indicator_list: print(bigdf) From 4790aaaae1eaa85657674d91b48621539af77711 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 26 Nov 2022 16:58:56 +0000 Subject: [PATCH 2/4] 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 From 1a3f88c7b93e10b65869cef1adb6e023f650d916 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sun, 27 Nov 2022 11:30:13 +0000 Subject: [PATCH 3/4] Replace separate start/end date option with usual timerange option --- docs/advanced-backtesting.md | 7 ++--- freqtrade/commands/arguments.py | 3 +- freqtrade/commands/cli_options.py | 10 ------- freqtrade/configuration/configuration.py | 7 ++--- freqtrade/data/entryexitanalysis.py | 36 +++++++++--------------- tests/data/test_entryexitanalysis.py | 6 ++-- 6 files changed, 21 insertions(+), 48 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 78e692f84..ae3eb2e4e 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -103,15 +103,14 @@ 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: +To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[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 +--timerange : Timerange to filter output trades, start date inclusive, end date exclusive. e.g. 20220101-20221231 ``` 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 +freqtrade backtesting-analysis -c --timerange 20220101-20220201 ``` diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 159b18439..b53a1022d 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -106,8 +106,7 @@ 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", - "analysis_date_start", "analysis_date_end"] + "exit_reason_list", "indicator_list", "timerange"] 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 0592b0e53..91ac16365 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -658,16 +658,6 @@ 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 4e8abf48e..664610f33 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -462,11 +462,8 @@ 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: {}') + self._args_to_config(config, argname='timerange', + logstring='Filter trades by timerange: {}') def _process_runmode(self, config: Config) -> None: diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 77f14d0c6..565a279b1 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,11 +1,11 @@ import logging -from datetime import datetime from pathlib import Path import joblib import pandas as pd from tabulate import tabulate +from freqtrade.configuration import TimeRange from freqtrade.constants import Config from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) @@ -153,22 +153,12 @@ def _do_group_table_output(bigdf, glist): logger.warning("Invalid group mask specified.") -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)] +def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'): + if timerange: + if timerange.starttype == 'date': + df = df.loc[(df[df_date_col] >= timerange.startdt)] + if timerange.stoptype == 'date': + df = df.loc[(df[df_date_col] < timerange.stopdt)] return df @@ -183,12 +173,12 @@ def _select_rows_by_tags(df, enter_reason_list, exit_reason_list): def prepare_results(analysed_trades, stratname, enter_reason_list, exit_reason_list, - date_start=None, date_end=None): + timerange=None): res_df = pd.DataFrame() for pair, trades in analysed_trades[stratname].items(): res_df = pd.concat([res_df, trades], ignore_index=True) - res_df = _select_rows_within_dates(res_df, date_start, date_end) + res_df = _select_rows_within_dates(res_df, timerange) 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) @@ -236,8 +226,9 @@ def process_entry_exit_reasons(config: Config): 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) + + timerange = TimeRange.parse_timerange(None if config.get( + 'timerange') is None else str(config.get('timerange'))) backtest_stats = load_backtest_stats(config['exportfilename']) @@ -252,8 +243,7 @@ def process_entry_exit_reasons(config: Config): 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) + timerange=timerange) print_results(res_df, analysis_groups, diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 8daca1a67..e33ed4955 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -191,10 +191,8 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert '2.5' in captured.out # test date filtering - args = get_args(base_args + - ['--analysis-date-start', "20180129", - '--analysis-date-end', "20180130"] - ) + args = get_args(base_args + ['--timerange', "20180129-20180130"]) start_analysis_entries_exits(args) captured = capsys.readouterr() + assert 'enter_tag_long_a' in captured.out assert 'enter_tag_long_b' not in captured.out From 05a7fca2424c2c10b85f4d5e44f8ba5fa26fdb4c Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Mon, 28 Nov 2022 12:12:45 +0000 Subject: [PATCH 4/4] Fix utils docs for backtesting-analysis --- docs/utils.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index e88a13a9a..e717a0f9c 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -722,8 +722,7 @@ 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] + [--timerange YYYYMMDD-[YYYYMMDD]] optional arguments: -h, --help show this help message and exit @@ -746,12 +745,10 @@ 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 + --timerange YYYYMMDD-[YYYYMMDD] + Timerange to filter trades for analysis, + start inclusive, end exclusive. e.g. + 20220101-20220201 Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).