From 5a4e99b413f84f818662cc3012819db76aec47c1 Mon Sep 17 00:00:00 2001 From: froggleston Date: Mon, 5 Dec 2022 15:34:31 +0000 Subject: [PATCH 01/56] Add support for collating and analysing rejected trades in backtest --- docs/advanced-backtesting.md | 37 ++++++- docs/utils.md | 23 +++-- freqtrade/commands/arguments.py | 3 +- freqtrade/commands/cli_options.py | 17 +++- freqtrade/configuration/configuration.py | 9 ++ freqtrade/data/entryexitanalysis.py | 118 +++++++++++++++++------ freqtrade/optimize/backtesting.py | 64 +++++++++--- freqtrade/optimize/optimize_reports.py | 28 ++++-- tests/data/test_entryexitanalysis.py | 12 ++- 9 files changed, 254 insertions(+), 57 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index ae3eb2e4e..779844cfe 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -29,7 +29,7 @@ If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` `user_data/backtest_results` folder. To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command -with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`): +with `--analysis-groups` option provided with space-separated arguments: ``` bash freqtrade backtesting-analysis -c --analysis-groups 0 1 2 3 4 @@ -39,6 +39,7 @@ This command will read from the last backtesting results. The `--analysis-groups used to specify the various tabular outputs showing the profit fo each group or trade, ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4): +* 0: overall winrate and profit summary by enter_tag * 1: profit summaries grouped by enter_tag * 2: profit summaries grouped by enter_tag and exit_tag * 3: profit summaries grouped by pair and enter_tag @@ -114,3 +115,37 @@ For example, if your backtest timerange was `20220101-20221231` but you only wan ```bash freqtrade backtesting-analysis -c --timerange 20220101-20220201 ``` + +### Printing out rejected trades + +Use the `--rejected` option to print out rejected trades. + +```bash +freqtrade backtesting-analysis -c --rejected +``` + +### Writing tables to CSV + +Some of the tabular outputs can become large, so printing them out to the terminal is not preferable. +Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files. + +```bash +freqtrade backtesting-analysis -c --analysis-to-csv +``` + +By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g. + +```bash +freqtrade backtesting-analysis -c --analysis-to-csv --rejected --analysis-groups 0 1 +``` + +This will write to `user_data/backtest_results`: +* rejected.csv +* group_0.csv +* group_1.csv + +To override where the files will be written, also specify the `--analysis-csv-path` option. + +```bash +freqtrade backtesting-analysis -c --analysis-to-csv --analysis-csv-path another/data/path/ +``` diff --git a/docs/utils.md b/docs/utils.md index e717a0f9c..24639e81e 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -723,6 +723,9 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V] [--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]] [--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]] [--timerange YYYYMMDD-[YYYYMMDD]] + [--rejected] + [--analysis-to-csv] + [--analysis-csv-path PATH] optional arguments: -h, --help show this help message and exit @@ -736,19 +739,27 @@ optional arguments: pair and enter_tag, 4: by pair, enter_ and exit_tag (this can get quite large) --enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...] - Comma separated list of entry signals to analyse. - Default: all. e.g. 'entry_tag_a,entry_tag_b' + Space separated list of entry signals to analyse. + Default: all. e.g. 'entry_tag_a entry_tag_b' --exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...] - Comma separated list of exit signals to analyse. + Space separated list of exit signals to analyse. Default: all. e.g. - 'exit_tag_a,roi,stop_loss,trailing_stop_loss' + 'exit_tag_a roi stop_loss trailing_stop_loss' --indicator-list INDICATOR_LIST [INDICATOR_LIST ...] - Comma separated list of indicators to analyse. e.g. - 'close,rsi,bb_lowerband,profit_abs' + Space separated list of indicators to analyse. e.g. + 'close rsi bb_lowerband profit_abs' --timerange YYYYMMDD-[YYYYMMDD] Timerange to filter trades for analysis, start inclusive, end exclusive. e.g. 20220101-20220201 + --rejected + Print out rejected trades table + --analysis-to-csv + Write out tables to individual CSVs, by default to + 'user_data/backtest_results' unless '--analysis-csv-path' is given. + --analysis-csv-path [PATH] + Optional path where individual CSVs will be written. If not used, + CSVs will be written to 'user_data/backtest_results'. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b53a1022d..b53aec1fb 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", "timerange"] + "exit_reason_list", "indicator_list", "timerange", + "analysis_rejected", "analysis_to_csv", "analysis_csv_path"] 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..6b019b0d6 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -634,7 +634,7 @@ AVAILABLE_CLI_OPTIONS = { "3: by pair and enter_tag, " "4: by pair, enter_ and exit_tag (this can get quite large)"), nargs='+', - default=['0', '1', '2'], + default=[], choices=['0', '1', '2', '3', '4'], ), "enter_reason_list": Arg( @@ -658,6 +658,21 @@ AVAILABLE_CLI_OPTIONS = { nargs='+', default=[], ), + "analysis_rejected": Arg( + '--rejected', + help='Analyse rejected trades', + action='store_true', + ), + "analysis_to_csv": Arg( + '--analysis-to-csv', + help='Save selected analysis tables to individual CSVs', + action='store_true', + ), + "analysis_csv_path": Arg( + '--analysis-csv-path', + help=("Specify a path to save the analysis CSVs " + "if --analysis-to-csv is enabled. Default: user_data/basktesting_results/"), + ), "freqaimodel": Arg( '--freqaimodel', help='Specify a custom freqaimodels.', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 664610f33..6698008cc 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -465,6 +465,15 @@ class Configuration: self._args_to_config(config, argname='timerange', logstring='Filter trades by timerange: {}') + self._args_to_config(config, argname='analysis_rejected', + logstring='Analyse rejected trades: {}') + + self._args_to_config(config, argname='analysis_to_csv', + logstring='Store analysis tables to CSV: {}') + + self._args_to_config(config, argname='analysis_csv_path', + logstring='Path to store analysis CSVs: {}') + 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 565a279b1..0343da6e6 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -15,22 +15,30 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -def _load_signal_candles(backtest_dir: Path): +def _load_backtest_analysis_data(backtest_dir: Path, name: str): if backtest_dir.is_dir(): scpf = Path(backtest_dir, - Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl" + Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl" ) else: - scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl") + scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl") try: scp = open(scpf, "rb") - signal_candles = joblib.load(scp) - logger.info(f"Loaded signal candles: {str(scpf)}") + rejected_trades = joblib.load(scp) + logger.info(f"Loaded {name} data: {str(scpf)}") except Exception as e: - logger.error("Cannot load signal candles from pickled results: ", e) + logger.error(f"Cannot load {name} data from pickled results: ", e) - return signal_candles + return rejected_trades + + +def _load_rejected_trades(backtest_dir: Path): + return _load_backtest_analysis_data(backtest_dir, "rejected") + + +def _load_signal_candles(backtest_dir: Path): + return _load_backtest_analysis_data(backtest_dir, "signals") def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles): @@ -43,9 +51,9 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand for pair in pairlist: if pair in signal_candles[strategy_name]: analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators( - pair, - trades, - signal_candles[strategy_name][pair]) + pair, + trades, + signal_candles[strategy_name][pair]) except Exception as e: print(f"Cannot process entry/exit reasons for {strategy_name}: ", e) @@ -85,7 +93,7 @@ def _analyze_candles_and_indicators(pair, trades, signal_candles): return pd.DataFrame() -def _do_group_table_output(bigdf, glist): +def _do_group_table_output(bigdf, glist, to_csv=False, csv_path=None): for g in glist: # 0: summary wins/losses grouped by enter tag if g == "0": @@ -116,7 +124,8 @@ def _do_group_table_output(bigdf, glist): sortcols = ['total_num_buys'] - _print_table(new, sortcols, show_index=True) + _print_table(new, sortcols, show_index=True, name="Group 0:", + to_csv=to_csv, csv_path=csv_path) else: agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'], @@ -148,11 +157,23 @@ def _do_group_table_output(bigdf, glist): new['mean_profit_pct'] = new['mean_profit_pct'] * 100 new['total_profit_pct'] = new['total_profit_pct'] * 100 - _print_table(new, sortcols) + _print_table(new, sortcols, name=f"Group {g}:", + to_csv=to_csv, csv_path=csv_path) else: logger.warning("Invalid group mask specified.") +def _do_rejected_trades_output(rejected_trades_df, to_csv=False, csv_path=None): + cols = ['pair', 'date', 'enter_tag'] + sortcols = ['date', 'pair', 'enter_tag'] + _print_table(rejected_trades_df[cols], + sortcols, + show_index=False, + name="Rejected Trades:", + to_csv=to_csv, + csv_path=csv_path) + + def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'): if timerange: if timerange.starttype == 'date': @@ -186,38 +207,65 @@ def prepare_results(analysed_trades, stratname, return res_df -def print_results(res_df, analysis_groups, indicator_list): +def print_results(res_df, analysis_groups, indicator_list, + rejected_trades=None, to_csv=False, csv_path=None): if res_df.shape[0] > 0: if analysis_groups: - _do_group_table_output(res_df, analysis_groups) + _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) + if rejected_trades is not None and not rejected_trades.empty: + _do_rejected_trades_output(rejected_trades, to_csv=to_csv, csv_path=csv_path) + + # NB this can be large for big dataframes! if "all" in indicator_list: - print(res_df) - elif indicator_list is not None: + _print_table(res_df, + show_index=False, + name="Indicators:", + to_csv=to_csv, + csv_path=csv_path) + elif indicator_list is not None and indicator_list: available_inds = [] for ind in indicator_list: if ind in res_df: available_inds.append(ind) ilist = ["pair", "enter_reason", "exit_reason"] + available_inds - _print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False) + _print_table(res_df[ilist], + sortcols=['exit_reason'], + show_index=False, + name="Indicators:", + to_csv=to_csv, + csv_path=csv_path) else: print("\\No trades to show") -def _print_table(df, sortcols=None, show_index=False): +def _print_table(df, sortcols=None, show_index=False, name=None, to_csv=False, csv_path=None): if (sortcols is not None): data = df.sort_values(sortcols) else: data = df - print( - tabulate( - data, - headers='keys', - tablefmt='psql', - showindex=show_index + if to_csv: + if csv_path is not None: + safe_name = Path(csv_path, + name.lower().replace(" ", "_").replace(":", "")) + else: + safe_name = Path("user_data", + "backtest_results", + name.lower().replace(" ", "_").replace(":", "")) + data.to_csv(f"{str(safe_name)}.csv") + else: + if name is not None: + print(name) + + print( + tabulate( + data, + headers='keys', + tablefmt='psql', + showindex=show_index + ) ) - ) def process_entry_exit_reasons(config: Config): @@ -226,6 +274,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', []) + do_rejected = config.get('analysis_rejected', False) + to_csv = config.get('analysis_to_csv', False) + csv_path = config.get('analysis_csv_path', config['exportfilename']) timerange = TimeRange.parse_timerange(None if config.get( 'timerange') is None else str(config.get('timerange'))) @@ -235,8 +286,16 @@ def process_entry_exit_reasons(config: Config): for strategy_name, results in backtest_stats['strategy'].items(): trades = load_backtest_data(config['exportfilename'], strategy_name) - if not trades.empty: + if trades is not None and not trades.empty: signal_candles = _load_signal_candles(config['exportfilename']) + + rej_df = None + if do_rejected: + rejected_trades_dict = _load_rejected_trades(config['exportfilename']) + rej_df = prepare_results(rejected_trades_dict, strategy_name, + enter_reason_list, exit_reason_list, + timerange=timerange) + analysed_trades_dict = _process_candles_and_indicators( config['exchange']['pair_whitelist'], strategy_name, trades, signal_candles) @@ -247,7 +306,10 @@ def process_entry_exit_reasons(config: Config): print_results(res_df, analysis_groups, - indicator_list) + indicator_list, + rejected_trades=rej_df, + to_csv=to_csv, + csv_path=csv_path) except ValueError as e: raise OperationalException(e) from e diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 82aa2b3e9..9e866fda5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -29,6 +29,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, + store_backtest_rejected_trades, store_backtest_signal_candles, store_backtest_stats) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade @@ -83,6 +84,8 @@ class Backtesting: self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} self.processed_dfs: Dict[str, Dict] = {} + self.rejected_dict: Dict[str, List] = {} + self.rejected_df: Dict[str, Dict] = {} self._exchange_name = self.config['exchange']['name'] self.exchange = ExchangeResolver.load_exchange( @@ -1048,6 +1051,18 @@ class Backtesting: return None return row + def _collate_rejected(self, pair, row): + """ + Temporarily store rejected trade information for downstream use in backtesting_analysis + """ + # It could be fun to enable hyperopt mode to write + # a loss function to reduce rejected signals + if (self.config.get('export', 'none') == 'signals' and + self.dataprovider.runmode == RunMode.BACKTEST): + if pair not in self.rejected_dict: + self.rejected_dict[pair] = [] + self.rejected_dict[pair].append([row[DATE_IDX], row[ENTER_TAG_IDX]]) + def backtest_loop( self, row: Tuple, pair: str, current_time: datetime, end_date: datetime, max_open_trades: int, open_trade_count_start: int, is_first: bool = True) -> int: @@ -1073,20 +1088,22 @@ class Backtesting: if ( (self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0) and is_first - and self.trade_slot_available(max_open_trades, open_trade_count_start) and current_time != end_date and trade_dir is not None and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir) ): - trade = self._enter_trade(pair, row, trade_dir) - if trade: - # TODO: hacky workaround to avoid opening > max_open_trades - # This emulates previous behavior - not sure if this is correct - # Prevents entering if the trade-slot was freed in this candle - open_trade_count_start += 1 - # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") - LocalTrade.add_bt_trade(trade) - self.wallets.update() + if (self.trade_slot_available(max_open_trades, open_trade_count_start)): + trade = self._enter_trade(pair, row, trade_dir) + if trade: + # TODO: hacky workaround to avoid opening > max_open_trades + # This emulates previous behavior - not sure if this is correct + # Prevents entering if the trade-slot was freed in this candle + open_trade_count_start += 1 + # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") + LocalTrade.add_bt_trade(trade) + self.wallets.update() + else: + self._collate_rejected(pair, row) for trade in list(LocalTrade.bt_trades_open_pp[pair]): # 3. Process entry orders. @@ -1266,6 +1283,7 @@ class Backtesting: if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): self._generate_trade_signal_candles(preprocessed_tmp, results) + self._generate_rejected_trades(preprocessed_tmp, self.rejected_dict) return min_date, max_date @@ -1282,12 +1300,33 @@ class Backtesting: for t, v in pairresults.open_date.items(): allinds = pairdf.loc[(pairdf['date'] < v)] signal_inds = allinds.iloc[[-1]] - signal_candles_only_df = pd.concat([signal_candles_only_df, signal_inds]) + signal_candles_only_df = pd.concat([ + signal_candles_only_df.infer_objects(), + signal_inds.infer_objects()]) signal_candles_only[pair] = signal_candles_only_df self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only + def _generate_rejected_trades(self, preprocessed_df, rejected_dict): + rejected_candles_only = {} + for pair, trades in rejected_dict.items(): + rejected_trades_only_df = DataFrame() + pairdf = preprocessed_df[pair] + + for t in trades: + data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy() + data_df_row['pair'] = pair + data_df_row['enter_tag'] = t[1] + + rejected_trades_only_df = pd.concat([ + rejected_trades_only_df.infer_objects(), + data_df_row.infer_objects()]) + + rejected_candles_only[pair] = rejected_trades_only_df + + self.rejected_df[self.strategy.get_strategy_name()] = rejected_candles_only + def _get_min_cached_backtest_date(self): min_backtest_date = None backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT) @@ -1353,6 +1392,9 @@ class Backtesting: store_backtest_signal_candles( self.config['exportfilename'], self.processed_dfs, dt_appendix) + store_backtest_rejected_trades( + self.config['exportfilename'], self.rejected_df, dt_appendix) + # Results may be mixed up now. Sort them so they follow --strategy-list order. if 'strategy_list' in self.config and len(self.results) > 0: self.results['strategy_comparison'] = sorted( diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8ad37e7d8..2ac0eed3d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -45,29 +45,41 @@ def store_backtest_stats( file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def store_backtest_signal_candles( - recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path: +def _store_backtest_analysis_data( + recordfilename: Path, data: Dict[str, Dict], + dtappendix: str, name: str) -> Path: """ - Stores backtest trade signal candles + Stores backtest trade candles for analysis :param recordfilename: Path object, which can either be a filename or a directory. Filenames will be appended with a timestamp right before the suffix - while for directories, /backtest-result-_signals.pkl will be used + while for directories, /backtest-result-_.pkl will be used as filename - :param stats: Dict containing the backtesting signal candles + :param candles: Dict containing the backtesting data for analysis :param dtappendix: Datetime to use for the filename + :param name: Name to use for the file, e.g. signals, rejected """ if recordfilename.is_dir(): - filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl') + filename = (recordfilename / f'backtest-result-{dtappendix}_{name}.pkl') else: filename = Path.joinpath( - recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl' + recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_{name}.pkl' ) - file_dump_joblib(filename, candles) + file_dump_joblib(filename, data) return filename +def store_backtest_signal_candles( + recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path: + return _store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals") + + +def store_backtest_rejected_trades( + recordfilename: Path, trades: Dict[str, Dict], dtappendix: str) -> Path: + return _store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected") + + def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index e33ed4955..96ab7b1ca 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -191,8 +191,18 @@ 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 + ['--timerange', "20180129-20180130"]) + args = get_args(base_args + + ['--analysis-groups', "0", "1", "2", + '--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 + + # test rejected - how to mock this? + # args = get_args(base_args + ['--rejected']) + # start_analysis_entries_exits(args) + # captured = capsys.readouterr() + # assert 'Rejected Trades:' in captured.out + # assert False From 854f056eaf83b478244b60da2ab1cef29d6e81e0 Mon Sep 17 00:00:00 2001 From: froggleston Date: Mon, 5 Dec 2022 16:16:36 +0000 Subject: [PATCH 02/56] Fix missing Path constructors --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2ac0eed3d..87d2ce376 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -72,12 +72,12 @@ def _store_backtest_analysis_data( def store_backtest_signal_candles( recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path: - return _store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals") + return _store_backtest_analysis_data(Path(recordfilename), candles, dtappendix, "signals") def store_backtest_rejected_trades( recordfilename: Path, trades: Dict[str, Dict], dtappendix: str) -> Path: - return _store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected") + return _store_backtest_analysis_data(Path(recordfilename), trades, dtappendix, "rejected") def _get_line_floatfmt(stake_currency: str) -> List[str]: From f5359985e843bd0b15782524dec74700ddf9beef Mon Sep 17 00:00:00 2001 From: froggleston Date: Thu, 8 Dec 2022 18:47:09 +0000 Subject: [PATCH 03/56] Make CLI option and docs clearer that we're handling signals not trades --- docs/advanced-backtesting.md | 10 +++++----- freqtrade/commands/cli_options.py | 4 ++-- freqtrade/configuration/configuration.py | 2 +- freqtrade/data/entryexitanalysis.py | 24 ++++++++++++------------ freqtrade/optimize/backtesting.py | 22 +++++++++++----------- freqtrade/optimize/optimize_reports.py | 2 +- tests/data/test_entryexitanalysis.py | 4 ++-- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 779844cfe..de34e5d14 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -116,12 +116,12 @@ For example, if your backtest timerange was `20220101-20221231` but you only wan freqtrade backtesting-analysis -c --timerange 20220101-20220201 ``` -### Printing out rejected trades +### Printing out rejected signals -Use the `--rejected` option to print out rejected trades. +Use the `--rejected-signals` option to print out rejected signals. ```bash -freqtrade backtesting-analysis -c --rejected +freqtrade backtesting-analysis -c --rejected-signals ``` ### Writing tables to CSV @@ -136,11 +136,11 @@ freqtrade backtesting-analysis -c --analysis-to-csv By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g. ```bash -freqtrade backtesting-analysis -c --analysis-to-csv --rejected --analysis-groups 0 1 +freqtrade backtesting-analysis -c --analysis-to-csv --rejected-signals --analysis-groups 0 1 ``` This will write to `user_data/backtest_results`: -* rejected.csv +* rejected_signals.csv * group_0.csv * group_1.csv diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 6b019b0d6..e2f91c3f3 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -659,8 +659,8 @@ AVAILABLE_CLI_OPTIONS = { default=[], ), "analysis_rejected": Arg( - '--rejected', - help='Analyse rejected trades', + '--rejected-signals', + help='Analyse rejected signals', action='store_true', ), "analysis_to_csv": Arg( diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 6698008cc..99332fbb6 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -466,7 +466,7 @@ class Configuration: logstring='Filter trades by timerange: {}') self._args_to_config(config, argname='analysis_rejected', - logstring='Analyse rejected trades: {}') + logstring='Analyse rejected signals: {}') self._args_to_config(config, argname='analysis_to_csv', logstring='Store analysis tables to CSV: {}') diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 0343da6e6..ec076f6dc 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -25,15 +25,15 @@ def _load_backtest_analysis_data(backtest_dir: Path, name: str): try: scp = open(scpf, "rb") - rejected_trades = joblib.load(scp) + loaded_data = joblib.load(scp) logger.info(f"Loaded {name} data: {str(scpf)}") except Exception as e: logger.error(f"Cannot load {name} data from pickled results: ", e) - return rejected_trades + return loaded_data -def _load_rejected_trades(backtest_dir: Path): +def _load_rejected_signals(backtest_dir: Path): return _load_backtest_analysis_data(backtest_dir, "rejected") @@ -163,13 +163,13 @@ def _do_group_table_output(bigdf, glist, to_csv=False, csv_path=None): logger.warning("Invalid group mask specified.") -def _do_rejected_trades_output(rejected_trades_df, to_csv=False, csv_path=None): +def _do_rejected_signals_output(rejected_signals_df, to_csv=False, csv_path=None): cols = ['pair', 'date', 'enter_tag'] sortcols = ['date', 'pair', 'enter_tag'] - _print_table(rejected_trades_df[cols], + _print_table(rejected_signals_df[cols], sortcols, show_index=False, - name="Rejected Trades:", + name="Rejected Signals:", to_csv=to_csv, csv_path=csv_path) @@ -208,13 +208,13 @@ def prepare_results(analysed_trades, stratname, def print_results(res_df, analysis_groups, indicator_list, - rejected_trades=None, to_csv=False, csv_path=None): + rejected_signals=None, to_csv=False, csv_path=None): if res_df.shape[0] > 0: if analysis_groups: _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) - if rejected_trades is not None and not rejected_trades.empty: - _do_rejected_trades_output(rejected_trades, to_csv=to_csv, csv_path=csv_path) + if rejected_signals is not None and not rejected_signals.empty: + _do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path) # NB this can be large for big dataframes! if "all" in indicator_list: @@ -291,8 +291,8 @@ def process_entry_exit_reasons(config: Config): rej_df = None if do_rejected: - rejected_trades_dict = _load_rejected_trades(config['exportfilename']) - rej_df = prepare_results(rejected_trades_dict, strategy_name, + rejected_signals_dict = _load_rejected_signals(config['exportfilename']) + rej_df = prepare_results(rejected_signals_dict, strategy_name, enter_reason_list, exit_reason_list, timerange=timerange) @@ -307,7 +307,7 @@ def process_entry_exit_reasons(config: Config): print_results(res_df, analysis_groups, indicator_list, - rejected_trades=rej_df, + rejected_signals=rej_df, to_csv=to_csv, csv_path=csv_path) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9e866fda5..80981e9ee 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -29,7 +29,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_rejected_trades, + store_backtest_rejected_signals, store_backtest_signal_candles, store_backtest_stats) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade @@ -1053,7 +1053,7 @@ class Backtesting: def _collate_rejected(self, pair, row): """ - Temporarily store rejected trade information for downstream use in backtesting_analysis + Temporarily store rejected signal information for downstream use in backtesting_analysis """ # It could be fun to enable hyperopt mode to write # a loss function to reduce rejected signals @@ -1283,7 +1283,7 @@ class Backtesting: if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): self._generate_trade_signal_candles(preprocessed_tmp, results) - self._generate_rejected_trades(preprocessed_tmp, self.rejected_dict) + self._generate_rejected_signals(preprocessed_tmp, self.rejected_dict) return min_date, max_date @@ -1308,22 +1308,22 @@ class Backtesting: self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only - def _generate_rejected_trades(self, preprocessed_df, rejected_dict): + def _generate_rejected_signals(self, preprocessed_df, rejected_dict): rejected_candles_only = {} - for pair, trades in rejected_dict.items(): - rejected_trades_only_df = DataFrame() + for pair, signals in rejected_dict.items(): + rejected_signals_only_df = DataFrame() pairdf = preprocessed_df[pair] - for t in trades: + for t in signals: data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy() data_df_row['pair'] = pair data_df_row['enter_tag'] = t[1] - rejected_trades_only_df = pd.concat([ - rejected_trades_only_df.infer_objects(), + rejected_signals_only_df = pd.concat([ + rejected_signals_only_df.infer_objects(), data_df_row.infer_objects()]) - rejected_candles_only[pair] = rejected_trades_only_df + rejected_candles_only[pair] = rejected_signals_only_df self.rejected_df[self.strategy.get_strategy_name()] = rejected_candles_only @@ -1392,7 +1392,7 @@ class Backtesting: store_backtest_signal_candles( self.config['exportfilename'], self.processed_dfs, dt_appendix) - store_backtest_rejected_trades( + store_backtest_rejected_signals( self.config['exportfilename'], self.rejected_df, dt_appendix) # Results may be mixed up now. Sort them so they follow --strategy-list order. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 87d2ce376..8038faa4f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -75,7 +75,7 @@ def store_backtest_signal_candles( return _store_backtest_analysis_data(Path(recordfilename), candles, dtappendix, "signals") -def store_backtest_rejected_trades( +def store_backtest_rejected_signals( recordfilename: Path, trades: Dict[str, Dict], dtappendix: str) -> Path: return _store_backtest_analysis_data(Path(recordfilename), trades, dtappendix, "rejected") diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 96ab7b1ca..8b0767873 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -201,8 +201,8 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert 'enter_tag_long_b' not in captured.out # test rejected - how to mock this? - # args = get_args(base_args + ['--rejected']) + # args = get_args(base_args + ['--rejected-signals']) # start_analysis_entries_exits(args) # captured = capsys.readouterr() - # assert 'Rejected Trades:' in captured.out + # assert 'Rejected Signals:' in captured.out # assert False From d03fe1f8eee09a5f4e6aa936bceb50e4dc983c04 Mon Sep 17 00:00:00 2001 From: Richard Jozsa <38407205+richardjozsa@users.noreply.github.com> Date: Thu, 16 Mar 2023 00:53:37 +0100 Subject: [PATCH 04/56] add latest experimental version of gymnasium --- freqtrade/freqai/RL/Base3ActionRLEnv.py | 7 +++++-- freqtrade/freqai/RL/Base4ActionRLEnv.py | 7 +++++-- freqtrade/freqai/RL/Base5ActionRLEnv.py | 6 ++++-- freqtrade/freqai/RL/BaseEnvironment.py | 8 ++++---- freqtrade/freqai/RL/BaseReinforcementLearningModel.py | 2 +- requirements-freqai-rl.txt | 8 +++++--- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqai/RL/Base3ActionRLEnv.py b/freqtrade/freqai/RL/Base3ActionRLEnv.py index 3b5fffc58..83682263b 100644 --- a/freqtrade/freqai/RL/Base3ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base3ActionRLEnv.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from gym import spaces +from gymnasium import spaces from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions @@ -94,9 +94,12 @@ class Base3ActionRLEnv(BaseEnvironment): observation = self._get_observation() + #user can play with time if they want + truncated = False + self._update_history(info) - return observation, step_reward, self._done, info + return observation, step_reward, self._done,truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index 8f45028b1..b26ba988a 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from gym import spaces +from gymnasium import spaces from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions @@ -106,9 +106,12 @@ class Base4ActionRLEnv(BaseEnvironment): observation = self._get_observation() + #user can play with time if they want + truncated = False + self._update_history(info) - return observation, step_reward, self._done, info + return observation, step_reward, self._done,truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py index 22d3cae30..6ce598dfb 100644 --- a/freqtrade/freqai/RL/Base5ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from gym import spaces +from gymnasium import spaces from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions @@ -111,10 +111,12 @@ class Base5ActionRLEnv(BaseEnvironment): ) observation = self._get_observation() + #user can play with time if they want + truncated = False self._update_history(info) - return observation, step_reward, self._done, info + return observation, step_reward, self._done,truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index 7a4467bf7..60b65cc03 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -4,11 +4,11 @@ from abc import abstractmethod from enum import Enum from typing import Optional, Type, Union -import gym +import gymnasium as gym import numpy as np import pandas as pd -from gym import spaces -from gym.utils import seeding +from gymnasium import spaces +from gymnasium.utils import seeding from pandas import DataFrame @@ -195,7 +195,7 @@ class BaseEnvironment(gym.Env): self.close_trade_profit = [] self._total_unrealized_profit = 1 - return self._get_observation() + return self._get_observation(), self.history @abstractmethod def step(self, action: int): diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index e10880f46..e18419d75 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any, Callable, Dict, Optional, Tuple, Type, Union -import gym +import gymnasium as gym import numpy as np import numpy.typing as npt import pandas as pd diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 4de7d8fab..233876425 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -3,8 +3,10 @@ # Required for freqai-rl torch==1.13.1; python_version < '3.11' -stable-baselines3==1.7.0; python_version < '3.11' -sb3-contrib==1.7.0; python_version < '3.11' +#until these branches will be released we can use this +git+https://github.com/Farama-Foundation/Gymnasium@main +git+https://github.com/DLR-RM/stable-baselines3@feat/gymnasium-support +git+https://github.com/Stable-Baselines-Team/stable-baselines3-contrib@feat/gymnasium-support # Gym is forced to this version by stable-baselines3. setuptools==65.5.1 # Should be removed when gym is fixed. -gym==0.21; python_version < '3.11' + From af6fc886f692a22eb2e2369a6a786871e3187702 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 14:56:41 +0100 Subject: [PATCH 05/56] Small refactor for new methods --- freqtrade/optimize/backtesting.py | 11 ++++------- freqtrade/optimize/optimize_reports.py | 13 +++++-------- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 17 ++++++++++------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 80981e9ee..864b6ffad 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -29,8 +29,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_rejected_signals, - store_backtest_signal_candles, + store_backtest_analysis_results, store_backtest_stats) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager @@ -1389,11 +1388,9 @@ class Backtesting: if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): - store_backtest_signal_candles( - self.config['exportfilename'], self.processed_dfs, dt_appendix) - - store_backtest_rejected_signals( - self.config['exportfilename'], self.rejected_df, dt_appendix) + store_backtest_analysis_results( + self.config['exportfilename'], self.processed_dfs, self.rejected_df, + dt_appendix) # Results may be mixed up now. Sort them so they follow --strategy-list order. if 'strategy_list' in self.config and len(self.results) > 0: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8038faa4f..b9599acb4 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -70,14 +70,11 @@ def _store_backtest_analysis_data( return filename -def store_backtest_signal_candles( - recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path: - return _store_backtest_analysis_data(Path(recordfilename), candles, dtappendix, "signals") - - -def store_backtest_rejected_signals( - recordfilename: Path, trades: Dict[str, Dict], dtappendix: str) -> Path: - return _store_backtest_analysis_data(Path(recordfilename), trades, dtappendix, "rejected") +def store_backtest_analysis_results( + recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], + dtappendix: str) -> Path: + _store_backtest_analysis_data(Path(recordfilename), candles, dtappendix, "signals") + _store_backtest_analysis_data(Path(recordfilename), trades, dtappendix, "rejected") def _get_line_floatfmt(stake_currency: str) -> List[str]: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ad6242b0e..9f4df1cf4 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -355,7 +355,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats') - sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_signal_candles') + sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_analysis_results') mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 403075795..763a3bad6 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -21,7 +21,7 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene generate_periodic_breakdown_stats, generate_strategy_comparison, generate_trading_stats, show_sorted_pairlist, - store_backtest_signal_candles, + store_backtest_analysis_results, store_backtest_stats, text_table_bt_results, text_table_exit_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -232,17 +232,17 @@ def test_store_backtest_candles(testdatadir, mocker): candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} # mock directory exporting - store_backtest_signal_candles(testdatadir, candle_dict, '2022_01_01_15_05_13') + store_backtest_analysis_results(testdatadir, candle_dict, {}, '2022_01_01_15_05_13') - assert dump_mock.call_count == 1 + assert dump_mock.call_count == 2 assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) dump_mock.reset_mock() # mock file exporting filename = Path(testdatadir / 'testresult') - store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13') - assert dump_mock.call_count == 1 + store_backtest_analysis_results(filename, candle_dict, {}, '2022_01_01_15_05_13') + assert dump_mock.call_count == 2 assert isinstance(dump_mock.call_args_list[0][0][0], Path) # result will be testdatadir / testresult-_signals.pkl assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) @@ -254,7 +254,9 @@ def test_write_read_backtest_candles(tmpdir): candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} # test directory exporting - stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13') + sample_date = '2022_01_01_15_05_13' + store_backtest_analysis_results(Path(tmpdir), candle_dict, {}, sample_date) + stored_file = Path(tmpdir / f'backtest-result-{sample_date}_signals.pkl') scp = open(stored_file, "rb") pickled_signal_candles = joblib.load(scp) scp.close() @@ -268,7 +270,8 @@ def test_write_read_backtest_candles(tmpdir): # test file exporting filename = Path(tmpdir / 'testresult') - stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13') + store_backtest_analysis_results(filename, candle_dict, {}, sample_date) + stored_file = Path(tmpdir / f'testresult-{sample_date}_signals.pkl') scp = open(stored_file, "rb") pickled_signal_candles = joblib.load(scp) scp.close() From f455e3327c555755fcee93ec981ec22b1be960f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 15:01:37 +0100 Subject: [PATCH 06/56] Simplify method further --- freqtrade/optimize/optimize_reports.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2ac1a785b..c2425deb3 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -73,9 +73,9 @@ def _store_backtest_analysis_data( def store_backtest_analysis_results( recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], - dtappendix: str) -> Path: - _store_backtest_analysis_data(Path(recordfilename), candles, dtappendix, "signals") - _store_backtest_analysis_data(Path(recordfilename), trades, dtappendix, "rejected") + dtappendix: str) -> None: + _store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals") + _store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected") def _get_line_floatfmt(stake_currency: str) -> List[str]: From 66c326b78935bb712a5b01fb81b570e9a7e61684 Mon Sep 17 00:00:00 2001 From: Richard Jozsa <38407205+richardjozsa@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:54:58 +0100 Subject: [PATCH 07/56] Add proper handling of multiple environments --- .../RL/BaseReinforcementLearningModel.py | 4 +--- .../ReinforcementLearner_multiproc.py | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index e18419d75..e36d5ea5d 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -433,7 +433,6 @@ class BaseReinforcementLearningModel(IFreqaiModel): def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, seed: int, train_df: DataFrame, price: DataFrame, - monitor: bool = False, env_info: Dict[str, Any] = {}) -> Callable: """ Utility function for multiprocessed env. @@ -450,8 +449,7 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank, **env_info) - if monitor: - env = Monitor(env) + return env set_random_seed(seed) return _init diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index b3b8c40e6..c215e380b 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -4,7 +4,7 @@ from typing import Any, Dict from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.vec_env import SubprocVecEnv - +from stable_baselines3.common.vec_env import VecMonitor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env @@ -41,22 +41,27 @@ class ReinforcementLearner_multiproc(ReinforcementLearner): env_info = self.pack_env_dict(dk.pair) + eval_freq = len(train_df) // self.max_threads + env_id = "train_env" - self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, + self.train_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, train_df, prices_train, - monitor=True, + env_info=env_info) for i - in range(self.max_threads)]) + in range(self.max_threads)])) eval_env_id = 'eval_env' - self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1, + self.eval_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1, test_df, prices_test, - monitor=True, + env_info=env_info) for i - in range(self.max_threads)]) + in range(self.max_threads)])) + self.eval_callback = EvalCallback(self.eval_env, deterministic=True, - render=False, eval_freq=len(train_df), + render=False, eval_freq=eval_freq, best_model_save_path=str(dk.data_path)) + + # TENSORBOARD CALLBACK DOES NOT RECOMMENDED TO USE WITH MULTIPLE ENVS, IT WILL RETURN FALSE INFORMATIONS, NEVERTHLESS NOT THREAD SAFE WITH SB3!!! actions = self.train_env.env_method("get_actions")[0] self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) From c055f82e9a05a175f8b66142ed0b989b2a390f77 Mon Sep 17 00:00:00 2001 From: Richard Jozsa <38407205+richardjozsa@users.noreply.github.com> Date: Sun, 16 Apr 2023 19:28:36 +0200 Subject: [PATCH 08/56] Pip release follow up --- requirements-freqai-rl.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 233876425..97d8e2c9b 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -4,9 +4,9 @@ # Required for freqai-rl torch==1.13.1; python_version < '3.11' #until these branches will be released we can use this -git+https://github.com/Farama-Foundation/Gymnasium@main -git+https://github.com/DLR-RM/stable-baselines3@feat/gymnasium-support -git+https://github.com/Stable-Baselines-Team/stable-baselines3-contrib@feat/gymnasium-support +gymnasium==0.28.1 +stable_baselines3>=2.0.0a1 +sb3_contrib>=2.0.0a1 # Gym is forced to this version by stable-baselines3. setuptools==65.5.1 # Should be removed when gym is fixed. From 3fb5cd3df6efa96e65d6fa79dafb420641ef06b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 20:27:18 +0200 Subject: [PATCH 09/56] Improve formatting --- freqtrade/freqai/RL/Base3ActionRLEnv.py | 4 ++-- freqtrade/freqai/RL/Base4ActionRLEnv.py | 4 ++-- freqtrade/freqai/RL/Base5ActionRLEnv.py | 4 ++-- .../RL/BaseReinforcementLearningModel.py | 2 +- .../ReinforcementLearner_multiproc.py | 24 +++++++++---------- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/RL/Base3ActionRLEnv.py b/freqtrade/freqai/RL/Base3ActionRLEnv.py index bb773658e..538ca3a6a 100644 --- a/freqtrade/freqai/RL/Base3ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base3ActionRLEnv.py @@ -94,12 +94,12 @@ class Base3ActionRLEnv(BaseEnvironment): observation = self._get_observation() - #user can play with time if they want + # user can play with time if they want truncated = False self._update_history(info) - return observation, step_reward, self._done,truncated, info + return observation, step_reward, self._done, truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index aebed71ab..12f10d4fc 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -96,12 +96,12 @@ class Base4ActionRLEnv(BaseEnvironment): observation = self._get_observation() - #user can play with time if they want + # user can play with time if they want truncated = False self._update_history(info) - return observation, step_reward, self._done,truncated, info + return observation, step_reward, self._done, truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py index d61c1a393..35d04f942 100644 --- a/freqtrade/freqai/RL/Base5ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py @@ -101,12 +101,12 @@ class Base5ActionRLEnv(BaseEnvironment): ) observation = self._get_observation() - #user can play with time if they want + # user can play with time if they want truncated = False self._update_history(info) - return observation, step_reward, self._done,truncated, info + return observation, step_reward, self._done, truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index e36d5ea5d..d3395219a 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -449,7 +449,7 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank, **env_info) - + return env set_random_seed(seed) return _init diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index c215e380b..73f617027 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -3,8 +3,8 @@ from typing import Any, Dict from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback -from stable_baselines3.common.vec_env import SubprocVecEnv -from stable_baselines3.common.vec_env import VecMonitor +from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor + from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env @@ -45,23 +45,21 @@ class ReinforcementLearner_multiproc(ReinforcementLearner): env_id = "train_env" self.train_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, - train_df, prices_train, - - env_info=env_info) for i - in range(self.max_threads)])) + train_df, prices_train, + env_info=env_info) for i + in range(self.max_threads)])) eval_env_id = 'eval_env' self.eval_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1, - test_df, prices_test, - - env_info=env_info) for i - in range(self.max_threads)])) - + test_df, prices_test, + env_info=env_info) for i + in range(self.max_threads)])) + self.eval_callback = EvalCallback(self.eval_env, deterministic=True, render=False, eval_freq=eval_freq, best_model_save_path=str(dk.data_path)) - - # TENSORBOARD CALLBACK DOES NOT RECOMMENDED TO USE WITH MULTIPLE ENVS, IT WILL RETURN FALSE INFORMATIONS, NEVERTHLESS NOT THREAD SAFE WITH SB3!!! + # TENSORBOARD CALLBACK DOES NOT RECOMMENDED TO USE WITH MULTIPLE ENVS, + # IT WILL RETURN FALSE INFORMATIONS, NEVERTHLESS NOT THREAD SAFE WITH SB3!!! actions = self.train_env.env_method("get_actions")[0] self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) From f1e03a68739fd62d36eb7f893ef6b1e233f7dc64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Apr 2023 18:20:25 +0200 Subject: [PATCH 10/56] Update variable to better reflect it's content --- freqtrade/rpc/api_server/api_v1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 8ea70bb69..8aa706e62 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -303,11 +303,11 @@ def get_strategy(strategy: str, config=Depends(get_config)): @router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai']) def list_freqaimodels(config=Depends(get_config)): from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver - strategies = FreqaiModelResolver.search_all_objects( + models = FreqaiModelResolver.search_all_objects( config, False) - strategies = sorted(strategies, key=lambda x: x['name']) + models = sorted(models, key=lambda x: x['name']) - return {'freqaimodels': [x['name'] for x in strategies]} + return {'freqaimodels': [x['name'] for x in models]} @router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) From 0a05099713096d4c21f816d0e03c5bef7d64f3ff Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 21 Apr 2023 22:52:19 +0200 Subject: [PATCH 11/56] fix mypy --- freqtrade/freqai/RL/BaseReinforcementLearningModel.py | 10 +++++----- freqtrade/freqai/RL/TensorboardCallback.py | 8 ++++++-- freqtrade/freqai/freqai_interface.py | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index d3395219a..e2c0f5fda 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -16,13 +16,13 @@ from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.utils import set_random_seed -from stable_baselines3.common.vec_env import SubprocVecEnv +from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv -from freqtrade.freqai.RL.BaseEnvironment import BaseActions, Positions +from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment, Positions from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback from freqtrade.persistence import Trade @@ -46,8 +46,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): 'cpu_count', 1), max(int(self.max_system_threads / 2), 1)) th.set_num_threads(self.max_threads) self.reward_params = self.freqai_info['rl_config']['model_reward_parameters'] - self.train_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env() - self.eval_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env() + self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env() + self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env() self.eval_callback: Optional[EvalCallback] = None self.model_type = self.freqai_info['rl_config']['model_type'] self.rl_config = self.freqai_info['rl_config'] @@ -431,7 +431,7 @@ class BaseReinforcementLearningModel(IFreqaiModel): return 0. -def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, +def make_env(MyRLEnv: Type[BaseEnvironment], env_id: str, rank: int, seed: int, train_df: DataFrame, price: DataFrame, env_info: Dict[str, Any] = {}) -> Callable: """ diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 7f8c76956..c5511cf53 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam +from stable_baselines3.common.vec_env import SubprocVecEnv from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment @@ -16,7 +17,7 @@ class TensorboardCallback(BaseCallback): super().__init__(verbose) self.model: Any = None self.logger = None # type: Any - self.training_env: BaseEnvironment = None # type: ignore + self.training_env: Type[BaseEnvironment] = None # type: ignore self.actions: Type[Enum] = actions def _on_training_start(self) -> None: @@ -44,7 +45,10 @@ class TensorboardCallback(BaseCallback): def _on_step(self) -> bool: local_info = self.locals["infos"][0] - tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] + if isinstance(self.training_env, SubprocVecEnv): + tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] + else: + tensorboard_metrics = self.training_env.tensorboard_metrics for metric in local_info: if metric not in ["episode", "terminal_observation"]: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7eaaeab3e..ebc69452a 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -242,8 +242,8 @@ class IFreqaiModel(ABC): new_trained_timerange, pair, strategy, dk, data_load_timerange ) except Exception as msg: - logger.warning(f"Training {pair} raised exception {msg.__class__.__name__}. " - f"Message: {msg}, skipping.") + logger.exception(f"Training {pair} raised exception {msg.__class__.__name__}. " + f"Message: {msg}, skipping.") self.train_timer('stop', pair) From e29ce218ebd3dee527566a40621015c5a4ac982d Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 26 Apr 2023 10:54:54 +0200 Subject: [PATCH 12/56] fix typing in TensorboardCallback --- freqtrade/freqai/RL/BaseEnvironment.py | 8 ++++++++ freqtrade/freqai/RL/TensorboardCallback.py | 9 +++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index 081d41202..08bb93347 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -127,6 +127,14 @@ class BaseEnvironment(gym.Env): self.history: dict = {} self.trade_history: list = [] + def get_attr(self, attr: str): + """ + Returns the attribute of the environment + :param attr: attribute to return + :return: attribute + """ + return getattr(self, attr) + @abstractmethod def set_action_space(self): """ diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index c5511cf53..12dcd8b9d 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam -from stable_baselines3.common.vec_env import SubprocVecEnv +from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment @@ -17,7 +17,7 @@ class TensorboardCallback(BaseCallback): super().__init__(verbose) self.model: Any = None self.logger = None # type: Any - self.training_env: Type[BaseEnvironment] = None # type: ignore + self.training_env: Union[BaseEnvironment, SubprocVecEnv, VecMonitor] = None self.actions: Type[Enum] = actions def _on_training_start(self) -> None: @@ -45,10 +45,7 @@ class TensorboardCallback(BaseCallback): def _on_step(self) -> bool: local_info = self.locals["infos"][0] - if isinstance(self.training_env, SubprocVecEnv): - tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] - else: - tensorboard_metrics = self.training_env.tensorboard_metrics + tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] for metric in local_info: if metric not in ["episode", "terminal_observation"]: From e86980befacd6dd9249b57aaf43c65beef50f125 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 26 Apr 2023 13:42:10 +0200 Subject: [PATCH 13/56] remove typing from callback init --- freqtrade/freqai/RL/TensorboardCallback.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 12dcd8b9d..282e60b0d 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam -from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor +from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv, VecMonitor from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment @@ -17,7 +17,8 @@ class TensorboardCallback(BaseCallback): super().__init__(verbose) self.model: Any = None self.logger = None # type: Any - self.training_env: Union[BaseEnvironment, SubprocVecEnv, VecMonitor] = None + self.training_env: Union[BaseEnvironment, SubprocVecEnv, + VecMonitor, DummyVecEnv] = DummyVecEnv() self.actions: Type[Enum] = actions def _on_training_start(self) -> None: From c6f3a3bbca4472918c8a60a2784075936621483a Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 26 Apr 2023 14:11:26 +0200 Subject: [PATCH 14/56] avoid typing issues in the tensorboard callback --- freqtrade/freqai/RL/TensorboardCallback.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 282e60b0d..3924f9d2c 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -3,9 +3,8 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam -from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv, VecMonitor -from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment +from freqtrade.freqai.RL.BaseEnvironment import BaseActions class TensorboardCallback(BaseCallback): @@ -17,8 +16,6 @@ class TensorboardCallback(BaseCallback): super().__init__(verbose) self.model: Any = None self.logger = None # type: Any - self.training_env: Union[BaseEnvironment, SubprocVecEnv, - VecMonitor, DummyVecEnv] = DummyVecEnv() self.actions: Type[Enum] = actions def _on_training_start(self) -> None: From 6d3c94a7398718566a91a89ede0c717e3b858b3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Apr 2023 18:08:55 +0200 Subject: [PATCH 15/56] type: ignore the offending tensorflow call --- freqtrade/freqai/RL/TensorboardCallback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 3924f9d2c..784dc848d 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -43,7 +43,7 @@ class TensorboardCallback(BaseCallback): def _on_step(self) -> bool: local_info = self.locals["infos"][0] - tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] + tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] # type: ignore for metric in local_info: if metric not in ["episode", "terminal_observation"]: From 8cf0e4a316747a3c9456c9aa352d71dee1481f47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Apr 2023 19:43:42 +0200 Subject: [PATCH 16/56] Fix mypy typing errors --- freqtrade/freqai/RL/TensorboardCallback.py | 10 ++++++++-- .../freqai/prediction_models/ReinforcementLearner.py | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 784dc848d..61652c9c6 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam +from stable_baselines3.common.vec_env import VecEnv from freqtrade.freqai.RL.BaseEnvironment import BaseActions @@ -12,10 +13,13 @@ class TensorboardCallback(BaseCallback): Custom callback for plotting additional values in tensorboard and episodic summary reports. """ + # Override training_env type to fix type errors + training_env: Union[VecEnv, None] = None + def __init__(self, verbose=1, actions: Type[Enum] = BaseActions): super().__init__(verbose) self.model: Any = None - self.logger = None # type: Any + self.logger: Any = None self.actions: Type[Enum] = actions def _on_training_start(self) -> None: @@ -43,7 +47,9 @@ class TensorboardCallback(BaseCallback): def _on_step(self) -> bool: local_info = self.locals["infos"][0] - tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] # type: ignore + if self.training_env is None: + return True + tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] for metric in local_info: if metric not in ["episode", "terminal_observation"]: diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index 65990da87..a5c2e12b5 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -1,11 +1,12 @@ import logging from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Type import torch as th from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions +from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment from freqtrade.freqai.RL.BaseReinforcementLearningModel import BaseReinforcementLearningModel @@ -84,7 +85,9 @@ class ReinforcementLearner(BaseReinforcementLearningModel): return model - class MyRLEnv(Base5ActionRLEnv): + MyRLEnv: Type[BaseEnvironment] + + class MyRLEnv(Base5ActionRLEnv): # type: ignore[no-redef] """ User can override any function in BaseRLEnv and gym.Env. Here the user sets a custom reward based on profit and trade duration. From 76ae539e619f1b8100f6392abcdb8edd5dafa4cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 14:55:17 +0200 Subject: [PATCH 17/56] Minor edit --- docs/advanced-backtesting.md | 1 + freqtrade/data/entryexitanalysis.py | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 4c81fd5ff..b587c4157 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -141,6 +141,7 @@ freqtrade backtesting-analysis -c --analysis-to-csv --rejected-sig ``` This will write to `user_data/backtest_results`: + * rejected_signals.csv * group_0.csv * group_1.csv diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 2c198c1d4..f7344447d 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import Optional import joblib import pandas as pd @@ -52,9 +53,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand for pair in pairlist: if pair in signal_candles[strategy_name]: analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators( - pair, - trades, - signal_candles[strategy_name][pair]) + pair, trades, signal_candles[strategy_name][pair]) except Exception as e: print(f"Cannot process entry/exit reasons for {strategy_name}: ", e) @@ -214,8 +213,8 @@ def prepare_results(analysed_trades, stratname, return res_df -def print_results(res_df, analysis_groups, indicator_list, - rejected_signals=None, to_csv=False, csv_path=None): +def print_results(res_df: pd.DataFrame, analysis_groups, indicator_list, + rejected_signals=None, to_csv=False, csv_path: Optional[Path] = None): if res_df.shape[0] > 0: if analysis_groups: _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) @@ -246,7 +245,7 @@ def print_results(res_df, analysis_groups, indicator_list, print("\\No trades to show") -def _print_table(df, sortcols=None, show_index=False, name=None, to_csv=False, csv_path=None): +def _print_table(df: pd.DataFrame, sortcols=None, show_index=False, name=None, to_csv=False, csv_path=None): if (sortcols is not None): data = df.sort_values(sortcols) else: From 703ec3ccc4348eccf115e864beac2ff3b8c59ac8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 15:01:47 +0200 Subject: [PATCH 18/56] Fix help text to show correct format --- freqtrade/commands/cli_options.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 916a51b89..f5e6d6926 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -641,22 +641,22 @@ AVAILABLE_CLI_OPTIONS = { ), "enter_reason_list": Arg( "--enter-reason-list", - help=("Comma separated list of entry signals to analyse. Default: all. " - "e.g. 'entry_tag_a,entry_tag_b'"), + help=("Space separated list of entry signals to analyse. Default: all. " + "e.g. 'entry_tag_a entry_tag_b'"), nargs='+', default=['all'], ), "exit_reason_list": Arg( "--exit-reason-list", - help=("Comma separated list of exit signals to analyse. Default: all. " - "e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"), + help=("Space separated list of exit signals to analyse. Default: all. " + "e.g. 'exit_tag_a roi stop_loss trailing_stop_loss'"), nargs='+', default=['all'], ), "indicator_list": Arg( "--indicator-list", - help=("Comma separated list of indicators to analyse. " - "e.g. 'close,rsi,bb_lowerband,profit_abs'"), + help=("Space separated list of indicators to analyse. " + "e.g. 'close rsi bb_lowerband profit_abs'"), nargs='+', default=[], ), From fc2a3c9f172299855a5668ac75f886c9e915943a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 15:17:35 +0200 Subject: [PATCH 19/56] Implement further improvements, improve typehinting --- freqtrade/data/entryexitanalysis.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index f7344447d..a05c34414 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Optional +from typing import List, Optional import joblib import pandas as pd @@ -169,7 +169,8 @@ def _do_group_table_output(bigdf, glist, to_csv=False, csv_path=None): logger.warning("Invalid group mask specified.") -def _do_rejected_signals_output(rejected_signals_df, to_csv=False, csv_path=None): +def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame, + to_csv: bool = False, csv_path=None) -> None: cols = ['pair', 'date', 'enter_tag'] sortcols = ['date', 'pair', 'enter_tag'] _print_table(rejected_signals_df[cols], @@ -213,14 +214,17 @@ def prepare_results(analysed_trades, stratname, return res_df -def print_results(res_df: pd.DataFrame, analysis_groups, indicator_list, +def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str], rejected_signals=None, to_csv=False, csv_path: Optional[Path] = None): if res_df.shape[0] > 0: if analysis_groups: _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) - if rejected_signals is not None and not rejected_signals.empty: - _do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path) + if rejected_signals is not None: + if rejected_signals.empty: + print("There were no rejected signals.") + else: + _do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path) # NB this can be large for big dataframes! if "all" in indicator_list: @@ -245,7 +249,8 @@ def print_results(res_df: pd.DataFrame, analysis_groups, indicator_list, print("\\No trades to show") -def _print_table(df: pd.DataFrame, sortcols=None, show_index=False, name=None, to_csv=False, csv_path=None): +def _print_table(df: pd.DataFrame, sortcols=None, show_index=False, name=None, + to_csv=False, csv_path=None): if (sortcols is not None): data = df.sort_values(sortcols) else: @@ -282,7 +287,7 @@ def process_entry_exit_reasons(config: Config): indicator_list = config.get('indicator_list', []) do_rejected = config.get('analysis_rejected', False) to_csv = config.get('analysis_to_csv', False) - csv_path = config.get('analysis_csv_path', config['exportfilename']) + csv_path = Path(config.get('analysis_csv_path', config['exportfilename'])) timerange = TimeRange.parse_timerange(None if config.get( 'timerange') is None else str(config.get('timerange'))) From e20d9c8f98c759cbb21d60fa2d849f2623ba81bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 15:25:25 +0200 Subject: [PATCH 20/56] Impoved errorhandling, better typesafety --- freqtrade/data/entryexitanalysis.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index a05c34414..0b98a35d3 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -93,7 +93,7 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: return pd.DataFrame() -def _do_group_table_output(bigdf, glist, to_csv=False, csv_path=None): +def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ): for g in glist: # 0: summary wins/losses grouped by enter tag if g == "0": @@ -215,7 +215,7 @@ def prepare_results(analysed_trades, stratname, def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str], - rejected_signals=None, to_csv=False, csv_path: Optional[Path] = None): + csv_path: Path, rejected_signals=None, to_csv=False): if res_df.shape[0] > 0: if analysis_groups: _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) @@ -249,21 +249,15 @@ def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_li print("\\No trades to show") -def _print_table(df: pd.DataFrame, sortcols=None, show_index=False, name=None, - to_csv=False, csv_path=None): +def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, + to_csv=False, csv_path: Path): if (sortcols is not None): data = df.sort_values(sortcols) else: data = df if to_csv: - if csv_path is not None: - safe_name = Path(csv_path, - name.lower().replace(" ", "_").replace(":", "")) - else: - safe_name = Path("user_data", - "backtest_results", - name.lower().replace(" ", "_").replace(":", "")) + safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "")) data.to_csv(f"{str(safe_name)}.csv") else: if name is not None: @@ -288,6 +282,8 @@ 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'])) + if to_csv and not csv_path.is_dir(): + raise OperationalException(f"Specified directory {csv_path} does not exist.") timerange = TimeRange.parse_timerange(None if config.get( 'timerange') is None else str(config.get('timerange'))) From 0753f427b109d4d2ff449781f9ada85a1202743c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 15:29:15 +0200 Subject: [PATCH 21/56] Simplify storage --- freqtrade/data/entryexitanalysis.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 0b98a35d3..e2a986fbc 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -257,8 +257,9 @@ def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None data = df if to_csv: - safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "")) - data.to_csv(f"{str(safe_name)}.csv") + safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "") + ".csv") + data.to_csv(safe_name) + print(f"Saved {name} to {safe_name}") else: if name is not None: print(name) From 6e395ad7c973e6972bbd38af0a50530818669154 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 16:09:09 +0200 Subject: [PATCH 22/56] Refactor methods in backtesting --- freqtrade/data/entryexitanalysis.py | 2 +- freqtrade/optimize/backtesting.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index e2a986fbc..db3a7d3a4 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import List, Optional +from typing import List import joblib import pandas as pd diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5e121e8a2..7388c5e59 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1252,8 +1252,8 @@ class Backtesting: def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame], timerange: TimeRange): self.progress.init_step(BacktestState.ANALYZE, 0) - - logger.info(f"Running backtesting for Strategy {strat.get_strategy_name()}") + strategy_name = strat.get_strategy_name() + logger.info(f"Running backtesting for Strategy {strategy_name}") backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) @@ -1288,20 +1288,23 @@ class Backtesting: ) backtest_end_time = datetime.now(timezone.utc) results.update({ - 'run_id': self.run_ids.get(strat.get_strategy_name(), ''), + 'run_id': self.run_ids.get(strategy_name, ''), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), }) - self.all_results[self.strategy.get_strategy_name()] = results + self.all_results[strategy_name] = results if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): - self._generate_trade_signal_candles(preprocessed_tmp, results) - self._generate_rejected_signals(preprocessed_tmp, self.rejected_dict) + self.processed_dfs[strategy_name] = self._generate_trade_signal_candles( + preprocessed_tmp, results) + self.rejected_df[strategy_name] = self._generate_rejected_signals( + preprocessed_tmp, self.rejected_dict) return min_date, max_date - def _generate_trade_signal_candles(self, preprocessed_df, bt_results): + def _generate_trade_signal_candles(self, preprocessed_df: Dict[str, pd.DataFrame], + bt_results: Dict[str, Any]) -> pd.DataFrame: signal_candles_only = {} for pair in preprocessed_df.keys(): signal_candles_only_df = DataFrame() @@ -1319,10 +1322,10 @@ class Backtesting: signal_inds.infer_objects()]) signal_candles_only[pair] = signal_candles_only_df + return signal_candles_only - self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only - - def _generate_rejected_signals(self, preprocessed_df, rejected_dict): + def _generate_rejected_signals(self, preprocessed_df: Dict[str, DataFrame], + rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]: rejected_candles_only = {} for pair, signals in rejected_dict.items(): rejected_signals_only_df = DataFrame() @@ -1338,8 +1341,7 @@ class Backtesting: data_df_row.infer_objects()]) rejected_candles_only[pair] = rejected_signals_only_df - - self.rejected_df[self.strategy.get_strategy_name()] = rejected_candles_only + return rejected_candles_only def _get_min_cached_backtest_date(self): min_backtest_date = None From 023c155a25346c13ab32854ceda21690b71291ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Apr 2023 16:14:16 +0200 Subject: [PATCH 23/56] Extract signals generation from backtesting class --- freqtrade/optimize/backtesting.py | 49 +++----------------------- freqtrade/optimize/optimize_reports.py | 44 ++++++++++++++++++++++- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7388c5e59..622fe4444 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,7 +9,6 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -import pandas as pd from numpy import nan from pandas import DataFrame @@ -28,7 +27,9 @@ from freqtrade.exchange import (amount_to_contract_precision, price_to_precision from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.bt_progress import BTProgress -from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_rejected_signals, + generate_trade_signal_candles, + show_backtest_results, store_backtest_analysis_results, store_backtest_stats) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade @@ -1296,53 +1297,13 @@ class Backtesting: if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): - self.processed_dfs[strategy_name] = self._generate_trade_signal_candles( + self.processed_dfs[strategy_name] = generate_trade_signal_candles( preprocessed_tmp, results) - self.rejected_df[strategy_name] = self._generate_rejected_signals( + self.rejected_df[strategy_name] = generate_rejected_signals( preprocessed_tmp, self.rejected_dict) return min_date, max_date - def _generate_trade_signal_candles(self, preprocessed_df: Dict[str, pd.DataFrame], - bt_results: Dict[str, Any]) -> pd.DataFrame: - signal_candles_only = {} - for pair in preprocessed_df.keys(): - signal_candles_only_df = DataFrame() - - pairdf = preprocessed_df[pair] - resdf = bt_results['results'] - pairresults = resdf.loc[(resdf["pair"] == pair)] - - if pairdf.shape[0] > 0: - for t, v in pairresults.open_date.items(): - allinds = pairdf.loc[(pairdf['date'] < v)] - signal_inds = allinds.iloc[[-1]] - signal_candles_only_df = pd.concat([ - signal_candles_only_df.infer_objects(), - signal_inds.infer_objects()]) - - signal_candles_only[pair] = signal_candles_only_df - return signal_candles_only - - def _generate_rejected_signals(self, preprocessed_df: Dict[str, DataFrame], - rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]: - rejected_candles_only = {} - for pair, signals in rejected_dict.items(): - rejected_signals_only_df = DataFrame() - pairdf = preprocessed_df[pair] - - for t in signals: - data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy() - data_df_row['pair'] = pair - data_df_row['enter_tag'] = t[1] - - rejected_signals_only_df = pd.concat([ - rejected_signals_only_df.infer_objects(), - data_df_row.infer_objects()]) - - rejected_candles_only[pair] = rejected_signals_only_df - return rejected_candles_only - def _get_min_cached_backtest_date(self): min_backtest_date = None backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index dc496e16f..e60047a79 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -from pandas import DataFrame, to_datetime +from pandas import DataFrame, concat, to_datetime from tabulate import tabulate from freqtrade.constants import (BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, @@ -78,6 +78,48 @@ def store_backtest_analysis_results( _store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected") +def generate_trade_signal_candles(preprocessed_df: Dict[str, DataFrame], + bt_results: Dict[str, Any]) -> DataFrame: + signal_candles_only = {} + for pair in preprocessed_df.keys(): + signal_candles_only_df = DataFrame() + + pairdf = preprocessed_df[pair] + resdf = bt_results['results'] + pairresults = resdf.loc[(resdf["pair"] == pair)] + + if pairdf.shape[0] > 0: + for t, v in pairresults.open_date.items(): + allinds = pairdf.loc[(pairdf['date'] < v)] + signal_inds = allinds.iloc[[-1]] + signal_candles_only_df = concat([ + signal_candles_only_df.infer_objects(), + signal_inds.infer_objects()]) + + signal_candles_only[pair] = signal_candles_only_df + return signal_candles_only + + +def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame], + rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]: + rejected_candles_only = {} + for pair, signals in rejected_dict.items(): + rejected_signals_only_df = DataFrame() + pairdf = preprocessed_df[pair] + + for t in signals: + data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy() + data_df_row['pair'] = pair + data_df_row['enter_tag'] = t[1] + + rejected_signals_only_df = concat([ + rejected_signals_only_df.infer_objects(), + data_df_row.infer_objects()]) + + rejected_candles_only[pair] = rejected_signals_only_df + return rejected_candles_only + + def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) From 9dd077d69ec8a3c7bf920f09a62c6b1522ce578f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:57:08 +0000 Subject: [PATCH 24/56] Bump rich from 13.3.4 to 13.3.5 Bumps [rich](https://github.com/Textualize/rich) from 13.3.4 to 13.3.5. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.3.4...v13.3.5) --- updated-dependencies: - dependency-name: rich dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c646aec6..7ace78e17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 joblib==1.2.0 -rich==13.3.4 +rich==13.3.5 pyarrow==11.0.0; platform_machine != 'armv7l' # find first, C search in arrays From d928792eb4f49b801822cc679a375e3af3aadc63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:57:14 +0000 Subject: [PATCH 25/56] Bump requests from 2.28.2 to 2.29.0 Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.29.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.29.0) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c646aec6..0c0be35b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ python-telegram-bot==20.2 httpx>=0.23.3 arrow==1.2.3 cachetools==4.2.2 -requests==2.28.2 +requests==2.29.0 urllib3==1.26.15 jsonschema==4.17.3 TA-Lib==0.4.26 From 7dda3f5803d48cdb989a836d8532f199fc6f3630 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:57:24 +0000 Subject: [PATCH 26/56] Bump orjson from 3.8.10 to 3.8.11 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.10 to 3.8.11. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.8.10...3.8.11) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c646aec6..631b4bcfd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.8.10 +orjson==3.8.11 # Notify systemd sdnotify==0.3.2 From 7f9a6ffc53a6987c5ba4bb776071f0bee76531d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:57:51 +0000 Subject: [PATCH 27/56] Bump ruff from 0.0.262 to 0.0.263 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.262 to 0.0.263. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.262...v0.0.263) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ea75bb8f2..294f5404f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.262 +ruff==0.0.263 mypy==1.2.0 pre-commit==3.2.2 pytest==7.3.1 From 063ddd62e68cdb89f77545838b413ff9752e3c5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:58:00 +0000 Subject: [PATCH 28/56] Bump uvicorn from 0.21.1 to 0.22.0 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.21.1 to 0.22.0. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.21.1...0.22.0) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c646aec6..daf4b2ad1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ sdnotify==0.3.2 # API Server fastapi==0.95.1 pydantic==1.10.7 -uvicorn==0.21.1 +uvicorn==0.22.0 pyjwt==2.6.0 aiofiles==23.1.0 psutil==5.9.5 From d023e759209090df444341cf974e18fead611524 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:58:44 +0000 Subject: [PATCH 29/56] Bump ccxt from 3.0.75 to 3.0.84 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.75 to 3.0.84. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.75...3.0.84) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c646aec6..ea9f196e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.3 pandas==2.0.1 pandas-ta==0.3.14b -ccxt==3.0.75 +ccxt==3.0.84 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.10 From 1d73c7c27de5e592b751e4bc792ee1c79002f729 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:59:08 +0000 Subject: [PATCH 30/56] Bump mkdocs-material from 9.1.7 to 9.1.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.7 to 9.1.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.7...9.1.8) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 91b0e993b..40f7d9b1c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.7 +mkdocs-material==9.1.8 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 jinja2==3.1.2 From fe9f2d005e37a75953d5c020984598203ac815b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 03:59:14 +0000 Subject: [PATCH 31/56] Bump types-requests from 2.28.11.17 to 2.29.0.0 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.17 to 2.29.0.0. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ea75bb8f2..679dbf66a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,6 +25,6 @@ nbconvert==7.3.1 # mypy types types-cachetools==5.3.0.5 types-filelock==3.2.7 -types-requests==2.28.11.17 +types-requests==2.29.0.0 types-tabulate==0.9.0.2 types-python-dateutil==2.8.19.12 From a31ceb51a0d1da080d36197d7ce10b16578f8bfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 15:54:14 +0000 Subject: [PATCH 32/56] Bump sqlalchemy from 2.0.10 to 2.0.12 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.10 to 2.0.12. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a011331a7..f0b7be07e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==3.0.84 cryptography==40.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.10 +SQLAlchemy==2.0.12 python-telegram-bot==20.2 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.23.3 From 103f27cfd09f5c5caec128e98fd4f1128287e47d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 May 2023 17:54:21 +0200 Subject: [PATCH 33/56] Bump sqlalchemy pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0031300cd..342e82932 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.17 - types-tabulate==0.9.0.2 - types-python-dateutil==2.8.19.12 - - SQLAlchemy==2.0.10 + - SQLAlchemy==2.0.12 # stages: [push] - repo: https://github.com/pycqa/isort From 5e32182c72ae89b9fbc998898570770b19d34584 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 May 2023 19:34:21 +0200 Subject: [PATCH 34/56] Bump types requests --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 342e82932..9142ebb06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.3.0.5 - types-filelock==3.2.7 - - types-requests==2.28.11.17 + - types-requests==2.29.0.0 - types-tabulate==0.9.0.2 - types-python-dateutil==2.8.19.12 - SQLAlchemy==2.0.12 From 3f58c19976a044b93c5ec6753097a0c241d44986 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 17:48:30 +0000 Subject: [PATCH 35/56] Bump ccxt from 3.0.84 to 3.0.85 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.84 to 3.0.85. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.84...3.0.85) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f0b7be07e..1e0047249 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.3 pandas==2.0.1 pandas-ta==0.3.14b -ccxt==3.0.84 +ccxt==3.0.85 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.12 From 238581ee7a53ed9d241d74d261e916b02cd06117 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 07:08:47 +0200 Subject: [PATCH 36/56] Remove <3.11 pin for tqdm --- requirements-freqai-rl.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 45ccc40cc..7c3e875aa 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -10,4 +10,4 @@ sb3_contrib>=2.0.0a4 # Gym is forced to this version by stable-baselines3. setuptools==65.5.1 # Should be removed when gym is fixed. # Progress bar for stable-baselines3 and sb3-contrib -tqdm==4.65.0; python_version < '3.11' +tqdm==4.65.0 From 1c2dd884e9a181bf19bc494863a69f2b34a47ff8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 07:12:46 +0200 Subject: [PATCH 37/56] Remove dependency workarounds in place for gym --- .github/workflows/ci.yml | 6 +++--- Dockerfile | 2 +- build_helpers/install_windows.ps1 | 2 +- docs/installation.md | 6 ------ pyproject.toml | 2 +- requirements-freqai-rl.txt | 2 -- setup.sh | 3 +-- 7 files changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e4487ac8..52c772bd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: - name: Installation - *nix if: runner.os == 'Linux' run: | - python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 + python -m pip install --upgrade pip wheel export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -163,7 +163,7 @@ jobs: rm /usr/local/bin/python3.11-config || true brew install hdf5 c-blosc - python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 + python -m pip install --upgrade pip wheel export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -352,7 +352,7 @@ jobs: - name: Installation - *nix if: runner.os == 'Linux' run: | - python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 + python -m pip install --upgrade pip wheel export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include diff --git a/Dockerfile b/Dockerfile index ee8b3f0a8..d3890a25b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ 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==23.0.1 wheel==0.38.4 + && pip install --upgrade pip wheel # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 3e7df5dfc..2fc21d317 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,7 +1,7 @@ # Downloads don't work automatically, since the URL is regenerated via javascript. # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib -python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 +python -m pip install --upgrade pip wheel $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" diff --git a/docs/installation.md b/docs/installation.md index 11de20e83..718931d72 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -30,12 +30,6 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito !!! Warning "Up-to-date clock" The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. -!!! Error "Running setup.py install for gym did not run successfully." - If you get an error related with gym we suggest you to downgrade setuptools it to version 65.5.0 you can do it with the following command: - ```bash - pip install setuptools==65.5.0 - ``` - ------ ## Requirements diff --git a/pyproject.toml b/pyproject.toml index 28de6a1d8..17f91c7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 46.4.0", "wheel"] +requires = ["setuptools >= 64.0.0", "wheel"] build-backend = "setuptools.build_meta" [tool.black] diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 7c3e875aa..76de5cdab 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -7,7 +7,5 @@ torch==1.13.1; python_version < '3.11' gymnasium==0.28.1 stable_baselines3==2.0.0a5 sb3_contrib>=2.0.0a4 -# Gym is forced to this version by stable-baselines3. -setuptools==65.5.1 # Should be removed when gym is fixed. # Progress bar for stable-baselines3 and sb3-contrib tqdm==4.65.0 diff --git a/setup.sh b/setup.sh index d46569a53..cb982b368 100755 --- a/setup.sh +++ b/setup.sh @@ -49,8 +49,7 @@ function updateenv() { source .env/bin/activate SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." - # Setuptools 65.5.0 is the last version that can install gym==0.21.0 - ${PYTHON} -m pip install --upgrade pip==23.0.1 wheel==0.38.4 setuptools==65.5.1 + ${PYTHON} -m pip install --upgrade pip wheel setuptools REQUIREMENTS_HYPEROPT="" REQUIREMENTS_PLOT="" REQUIREMENTS_FREQAI="" From 75daa44c5a0631b8895d3c54c95b8e7e1152a456 Mon Sep 17 00:00:00 2001 From: Alexander Terekhov Date: Tue, 2 May 2023 09:02:52 +0300 Subject: [PATCH 38/56] Add missing core dependencies --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 048dc066d..5da858f3d 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,15 @@ setup( 'aiofiles', 'schedule', 'websockets', - 'janus' + 'janus', + 'ast-comments', + 'aiohttp', + 'blosc', + 'cryptography', + 'httpx', + 'python-dateutil', + 'tables' + ], extras_require={ 'dev': all_extra, From 8f5fb4e32bff5642cbe32b05144c3bae5d2387ec Mon Sep 17 00:00:00 2001 From: Alexander Terekhov Date: Tue, 2 May 2023 09:09:38 +0300 Subject: [PATCH 39/56] Add missing dev dependencies --- setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setup.py b/setup.py index 5da858f3d..ff59e7a66 100644 --- a/setup.py +++ b/setup.py @@ -32,11 +32,20 @@ hdf5 = [ develop = [ 'coveralls', 'mypy', + 'ruff', + 'pre-commit', 'pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', 'pytest-random-order', + 'isort', + 'time-machine', + 'types-cachetools', + 'types-filelock', + 'types-requests', + 'types-tabulate', + 'types-python-dateutil' ] jupyter = [ From e3f983729f84a59ed5fa9442de8bed4a99d1ff02 Mon Sep 17 00:00:00 2001 From: Alexander Terekhov Date: Tue, 2 May 2023 09:11:31 +0300 Subject: [PATCH 40/56] Update freqai dependencies --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ff59e7a66..9c717df8b 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,11 @@ hyperopt = [ freqai = [ 'scikit-learn', + 'joblib', 'catboost; platform_machine != "aarch64"', 'lightgbm', - 'xgboost' + 'xgboost', + 'tensorboard' ] freqai_rl = [ From 220f8c6b5f84407a7ebd5e4b221b3b8d309b0043 Mon Sep 17 00:00:00 2001 From: Alexander Terekhov Date: Tue, 2 May 2023 09:16:12 +0300 Subject: [PATCH 41/56] Add missing freqai-rl dependencies --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9c717df8b..512e112a8 100644 --- a/setup.py +++ b/setup.py @@ -21,9 +21,11 @@ freqai = [ freqai_rl = [ 'torch', + 'gymnasium', 'stable-baselines3', - 'gym==0.21', - 'sb3-contrib' + 'sb3-contrib', + 'setuptools', + 'tqdm' ] hdf5 = [ From d8a9c9422af42e770881ab1de3eb3be62359c09d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 18:17:21 +0200 Subject: [PATCH 42/56] Update missing "requirements" install in documentation --- docs/installation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation.md b/docs/installation.md index 718931d72..a06968dba 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -236,6 +236,7 @@ source .env/bin/activate ```bash python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` From a935f1e4de7d800c972aca4ab6f5c5701fba4158 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 19:27:01 +0200 Subject: [PATCH 43/56] Remove no longer necessary dependency from setup.py --- setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.py b/setup.py index 512e112a8..38e1ec4bb 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ freqai_rl = [ 'gymnasium', 'stable-baselines3', 'sb3-contrib', - 'setuptools', 'tqdm' ] @@ -107,12 +106,9 @@ setup( 'janus', 'ast-comments', 'aiohttp', - 'blosc', 'cryptography', 'httpx', 'python-dateutil', - 'tables' - ], extras_require={ 'dev': all_extra, From fb5fac164d4db23f5183bab6714b1286782ecf27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 19:28:09 +0200 Subject: [PATCH 44/56] add Packaging dependency explicitly --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 1e0047249..e3e8a5938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,3 +60,4 @@ websockets==11.0.2 janus==1.0.0 ast-comments==1.0.1 +packaging==23.1 diff --git a/setup.py b/setup.py index 38e1ec4bb..0421e307a 100644 --- a/setup.py +++ b/setup.py @@ -109,6 +109,7 @@ setup( 'cryptography', 'httpx', 'python-dateutil', + 'packaging', ], extras_require={ 'dev': all_extra, From f419d7870db901bfb60e844f5a0a241d48111004 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 19:51:33 +0200 Subject: [PATCH 45/56] Add freqaimodel to pair history endpoint closes #8566 --- freqtrade/rpc/api_server/api_v1.py | 5 ++++- freqtrade/rpc/rpc.py | 7 ++++--- scripts/rest_client.py | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 5ee5e36c4..279336c07 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -247,14 +247,17 @@ def pair_candles( @router.get('/pair_history', response_model=PairHistory, tags=['candle data']) def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, + freqaimodel: Optional[str] = None, config=Depends(get_config), exchange=Depends(get_exchange)): # The initial call to this endpoint can be slow, as it may need to initialize # the exchange class. config = deepcopy(config) config.update({ 'strategy': strategy, + 'timerange': timerange, + 'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'), }) - return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange, exchange) + return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 35e08cbc0..0bf4811b1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1216,8 +1216,8 @@ class RPC: @staticmethod def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, - timerange: str, exchange) -> Dict[str, Any]: - timerange_parsed = TimeRange.parse_timerange(timerange) + exchange) -> Dict[str, Any]: + timerange_parsed = TimeRange.parse_timerange(config.get('timerange')) _data = load_data( datadir=config["datadir"], @@ -1228,7 +1228,8 @@ class RPC: candle_type=config.get('candle_type_def', CandleType.SPOT) ) if pair not in _data: - raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.") + raise RPCException( + f"No data for {pair}, {timeframe} in {config.get('timerange')} found.") from freqtrade.data.dataprovider import DataProvider from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 196542780..ccffe7f5f 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -348,12 +348,13 @@ class FtRestClient(): params['limit'] = limit return self._get("pair_candles", params=params) - def pair_history(self, pair, timeframe, strategy, timerange=None): + def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=None): """Return historic, analyzed dataframe :param pair: Pair to get data for :param timeframe: Only pairs with this timeframe available. :param strategy: Strategy to analyze and get values for + :param freqaimodel: FreqAI model to use for analysis :param timerange: Timerange to get data for (same format than --timerange endpoints) :return: json object """ @@ -361,6 +362,7 @@ class FtRestClient(): "pair": pair, "timeframe": timeframe, "strategy": strategy, + "freqaimodel": freqaimodel, "timerange": timerange if timerange else '', }) From 13974d2508bfc4c6101ffc2c7d2f3a84dc3bb73e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 21:36:36 +0200 Subject: [PATCH 46/56] Reduce error severity when maintenance-ratio fails --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9a303426a..5273030ab 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2900,8 +2900,8 @@ class Exchange: if nominal_value >= tier['minNotional']: return (tier['maintenanceMarginRate'], tier['maintAmt']) - raise OperationalException("nominal value can not be lower than 0") + raise ExchangeError("nominal value can not be lower than 0") # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it # describes the min amt for a tier, and the lowest tier will always go down to 0 else: - raise OperationalException(f"Cannot get maintenance ratio using {self.name}") + raise ExchangeError(f"Cannot get maintenance ratio using {self.name}") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b0760944a..399442b08 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4932,7 +4932,7 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage exchange._leverage_tiers = leverage_tiers with pytest.raises( - OperationalException, + DependencyException, match='nominal value can not be lower than 0', ): exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', -1) From 0d1d25e8680d2f171e340f349a3f42e10c9cc6d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 May 2023 21:40:44 +0200 Subject: [PATCH 47/56] Improve error-handling --- freqtrade/freqtradebot.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 89f0ac55d..d8a5c0c29 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1824,16 +1824,18 @@ class FreqtradeBot(LoggingMixin): # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() - trade.set_liquidation_price(self.exchange.get_liquidation_price( - pair=trade.pair, - open_rate=trade.open_rate, - is_short=trade.is_short, - amount=trade.amount, - stake_amount=trade.stake_amount, - leverage=trade.leverage, - wallet_balance=trade.stake_amount, - )) - + try: + trade.set_liquidation_price(self.exchange.get_liquidation_price( + pair=trade.pair, + open_rate=trade.open_rate, + is_short=trade.is_short, + amount=trade.amount, + stake_amount=trade.stake_amount, + leverage=trade.leverage, + wallet_balance=trade.stake_amount, + )) + except DependencyException: + logger.warning('Unable to calculate liquidation price') # Updating wallets when order is closed self.wallets.update() Trade.commit() From 1cc5b6126d525c420cbb1cd6f8b3ca319f6f44af Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 May 2023 06:48:02 +0200 Subject: [PATCH 48/56] Bump pre-commit ruff version --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9142ebb06..13216e495 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.255' + rev: 'v0.0.263' hooks: - id: ruff From 976cc1ab15f8d361132974454d02f47a4af5ec23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 May 2023 06:48:17 +0200 Subject: [PATCH 49/56] Extract order_obj existence check to separate function --- freqtrade/freqtradebot.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d8a5c0c29..423b3ec04 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1722,9 +1722,7 @@ class FreqtradeBot(LoggingMixin): trade.exit_order_status = reason order = trade.select_order_by_order_id(order_id) - if not order: - raise DependencyException( - f"Order_obj not found for {order_id}. This should not have happened.") + self.order_obj_or_raise(order_id, order) profit_rate: float = trade.safe_close_rate profit_trade = trade.calc_profit(rate=profit_rate) @@ -1765,6 +1763,11 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def order_obj_or_raise(self, order_id: str, order_obj: Order): + if not order_obj: + raise DependencyException( + f"Order_obj not found for {order_id}. This should not have happened.") + # # Common update trade state methods # @@ -1804,9 +1807,7 @@ class FreqtradeBot(LoggingMixin): return True order_obj = trade.select_order_by_order_id(order_id) - if not order_obj: - raise DependencyException( - f"Order_obj not found for {order_id}. This should not have happened.") + self.order_obj_or_raise(order_id, order_obj) self.handle_order_fee(trade, order_obj, order) From 0adac268ee94bb6941fb46c2cf95c4173deb4ec6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 May 2023 07:01:57 +0200 Subject: [PATCH 50/56] Add test for #8591 --- tests/rpc/test_rpc_telegram.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4b4c2b028..fff3f956e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -825,6 +825,9 @@ async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_shor assert 'Exit Reason' in msg_mock.call_args_list[-1][0][0] assert 'ROI' in msg_mock.call_args_list[-1][0][0] assert 'Avg. Duration' in msg_mock.call_args_list[-1][0][0] + # Duration is not only N/A + assert '0:19:00' in msg_mock.call_args_list[-1][0][0] + assert 'N/A' in msg_mock.call_args_list[-1][0][0] msg_mock.reset_mock() From 80930d72a64af37716e2b7c672dfec4431be12f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 May 2023 07:03:14 +0200 Subject: [PATCH 51/56] Dont loop trades twice closes #8591 --- freqtrade/rpc/rpc.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0bf4811b1..818ef16ff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -420,16 +420,15 @@ class RPC: else: return 'draws' trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False) - # Sell reason + # Duration + dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []} + # Exit reason exit_reasons = {} for trade in trades: if trade.exit_reason not in exit_reasons: exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0} exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1 - # Duration - dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []} - for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) From 775ea1c8c657f8c6b41ad6604cdfd75b101e4f2c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 May 2023 06:25:02 +0000 Subject: [PATCH 52/56] Improve type safety --- freqtrade/freqtradebot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 423b3ec04..5e1e7d5f8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1721,8 +1721,8 @@ class FreqtradeBot(LoggingMixin): else: trade.exit_order_status = reason - order = trade.select_order_by_order_id(order_id) - self.order_obj_or_raise(order_id, order) + order_or_none = trade.select_order_by_order_id(order_id) + order = self.order_obj_or_raise(order_id, order_or_none) profit_rate: float = trade.safe_close_rate profit_trade = trade.calc_profit(rate=profit_rate) @@ -1763,10 +1763,11 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def order_obj_or_raise(self, order_id: str, order_obj: Order): + def order_obj_or_raise(self, order_id: str, order_obj: Optional[Order]) -> Order: if not order_obj: raise DependencyException( f"Order_obj not found for {order_id}. This should not have happened.") + return order_obj # # Common update trade state methods @@ -1806,8 +1807,8 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timedout. return True - order_obj = trade.select_order_by_order_id(order_id) - self.order_obj_or_raise(order_id, order_obj) + order_obj_or_none = trade.select_order_by_order_id(order_id) + order_obj = self.order_obj_or_raise(order_id, order_obj_or_none) self.handle_order_fee(trade, order_obj, order) From 24804f066c02dc4b504b9e98473c4c56bff176ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 May 2023 20:15:26 +0200 Subject: [PATCH 53/56] Update test comment, uncomment last test section --- tests/data/test_entryexitanalysis.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 17c1ff3d9..367ad8394 100644 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -209,9 +209,8 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert 'enter_tag_long_a' in captured.out assert 'enter_tag_long_b' not in captured.out - # test rejected - how to mock this? - # args = get_args(base_args + ['--rejected-signals']) - # start_analysis_entries_exits(args) - # captured = capsys.readouterr() - # assert 'Rejected Signals:' in captured.out - # assert False + # Due to the backtest mock, there's no rejected signals generated. + args = get_args(base_args + ['--rejected-signals']) + start_analysis_entries_exits(args) + captured = capsys.readouterr() + assert 'no rejected signals' in captured.out From e3ff2ccc97433a84924bc510c02defcff87d8d6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 May 2023 06:45:39 +0200 Subject: [PATCH 54/56] Slightly reword documentation to be more clear --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index a93dcecdf..2749d1281 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -227,8 +227,8 @@ for val in self.buy_ema_short.range: f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val) })) -# Append columns to existing dataframe -merged_frame = pd.concat(frames, axis=1) +# Combine all dataframes, and reassign the original dataframe column +dataframe = pd.concat(frames, axis=1) ``` Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant. From 306dfc4ae8fdec18f3adea00aa0685e37ab611a3 Mon Sep 17 00:00:00 2001 From: Tommaso Falchi Date: Fri, 5 May 2023 13:04:53 +0200 Subject: [PATCH 55/56] refactor(BasePyTorchClassifier.py): convert tensor to list before creating DataFrame to avoid TypeError. docs(BasePyTorchClassifier.py): add missing parameter description in predict method --- freqtrade/freqai/base_models/BasePyTorchClassifier.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py index 977152cc5..3a4de4df0 100644 --- a/freqtrade/freqai/base_models/BasePyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -45,6 +45,7 @@ class BasePyTorchClassifier(BasePyTorchModel): ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. + :param dk: dk: The datakitchen object :param unfiltered_df: Full dataframe for the current backtest period. :return: :pred_df: dataframe containing the predictions @@ -78,7 +79,9 @@ class BasePyTorchClassifier(BasePyTorchModel): probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) predicted_classes_str = self.decode_class_names(predicted_classes) - pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) + # used .tolist to convert probs into an iterable, in this way Tensors + # are automatically moved to the CPU first if necessary. + pred_df_prob = DataFrame(probs.detach().tolist(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) From 908a2e817a34efaa3d712a99b5be77284336bdef Mon Sep 17 00:00:00 2001 From: Tommaso Falchi Date: Fri, 5 May 2023 15:43:48 +0200 Subject: [PATCH 56/56] Align BasePyTorchRegressor tensors to cpu as in BasePyTorchClassifier --- freqtrade/freqai/base_models/BasePyTorchRegressor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index ea6fabe49..069387011 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -45,6 +45,5 @@ class BasePyTorchRegressor(BasePyTorchModel): device=self.device ) y = self.model.model(x) - y = y.cpu() - pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) + pred_df = DataFrame(y.detach().tolist(), columns=[dk.label_list[0]]) return (pred_df, dk.do_predict)