From 5a4e99b413f84f818662cc3012819db76aec47c1 Mon Sep 17 00:00:00 2001 From: froggleston Date: Mon, 5 Dec 2022 15:34:31 +0000 Subject: [PATCH 001/186] 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 002/186] 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 003/186] 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 004/186] 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 005/186] 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 006/186] 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 007/186] 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 cab82e8e606a8052a2c13ef8ffa10ab6945f4e12 Mon Sep 17 00:00:00 2001 From: initrv Date: Sun, 2 Apr 2023 02:59:02 +0300 Subject: [PATCH 008/186] Add sb3 learn progress bar --- config_examples/config_freqai.example.json | 3 ++- docs/freqai-parameter-table.md | 1 + freqtrade/constants.py | 1 + freqtrade/freqai/prediction_models/ReinforcementLearner.py | 3 ++- requirements-freqai-rl.txt | 3 +++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index 65a93379e..7878bd2c2 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -73,7 +73,8 @@ 10, 20 ], - "plot_feature_importances": 0 + "plot_feature_importances": 0, + "progress_bar": false }, "data_split_parameters": { "test_size": 0.33, diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 9822a895a..fe265d483 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -85,6 +85,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[, dict(vf=[], pi=[])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each. | `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting.
**Datatype:** bool.
Default: `False`. | `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases)
**Datatype:** Boolean.
**Default:** `False` +| `progress_bar` | Display a progress bar with the current progress, elapsed time and estimated remaining time.
**Datatype:** Boolean.
Default: `False`. ### Additional parameters diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ebb946221..00f072678 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -599,6 +599,7 @@ CONF_SCHEMA = { "policy_type": {"type": "string", "default": "MlpPolicy"}, "net_arch": {"type": "array", "default": [128, 128]}, "randomize_startinng_position": {"type": "boolean", "default": False}, + "progress_bar": {"type": "boolean", "default": False}, "model_reward_parameters": { "type": "object", "properties": { diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index e795703d4..a194c7290 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -71,7 +71,8 @@ class ReinforcementLearner(BaseReinforcementLearningModel): model.learn( total_timesteps=int(total_timesteps), - callback=[self.eval_callback, self.tensorboard_callback] + callback=[self.eval_callback, self.tensorboard_callback], + progress_bar=self.freqai_info["rl_config"]["progress_bar"] ) if Path(dk.data_path / "best_model.zip").is_file(): diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 4de7d8fab..5d76b0600 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -8,3 +8,6 @@ sb3-contrib==1.7.0; python_version < '3.11' # 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' +# Progress bar for stable-baselines3 and sb3-contrib +tqdm==4.65.0; python_version < '3.11' +rich==13.3.3; python_version < '3.11' From 4c608e7167e0f7c349e92e5a844f5e3b9cb18ed0 Mon Sep 17 00:00:00 2001 From: initrv Date: Sun, 2 Apr 2023 03:12:24 +0300 Subject: [PATCH 009/186] remove misplaced param --- config_examples/config_freqai.example.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index 7878bd2c2..65a93379e 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -73,8 +73,7 @@ 10, 20 ], - "plot_feature_importances": 0, - "progress_bar": false + "plot_feature_importances": 0 }, "data_split_parameters": { "test_size": 0.33, From cfc04103888e27f4f9f27936bb4b74ad98fc06ba Mon Sep 17 00:00:00 2001 From: initrv Date: Sun, 2 Apr 2023 04:08:07 +0300 Subject: [PATCH 010/186] use rl_config get instead of freqai_info --- freqtrade/freqai/prediction_models/ReinforcementLearner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index a194c7290..65990da87 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -72,7 +72,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): model.learn( total_timesteps=int(total_timesteps), callback=[self.eval_callback, self.tensorboard_callback], - progress_bar=self.freqai_info["rl_config"]["progress_bar"] + progress_bar=self.rl_config.get('progress_bar', False) ) if Path(dk.data_path / "best_model.zip").is_file(): From 20d17cbc52e30012f49eea19d16ca893242c48f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 17:39:23 +0200 Subject: [PATCH 011/186] Disable telegram from default_conf --- tests/conftest.py | 2 +- tests/rpc/test_rpc_manager.py | 10 +++++++--- tests/rpc/test_rpc_telegram.py | 7 +++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0aa6e70a8..2f2345d54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -504,7 +504,7 @@ def get_default_conf(testdatadir): {"method": "StaticPairList"} ], "telegram": { - "enabled": True, + "enabled": False, "token": "token", "chat_id": "0", "notification_settings": {}, diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 21c8b0813..f0bb72fc9 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -28,6 +28,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) @@ -52,6 +53,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) @@ -85,7 +87,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', side_effect=ValueError()) - + default_conf['telegram']['enabled'] = True freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ @@ -99,6 +101,7 @@ def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None: def test_process_msg_queue(mocker, default_conf, caplog) -> None: telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + default_conf['telegram']['enabled'] = True default_conf['telegram']['allow_custom_messages'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init') @@ -115,9 +118,9 @@ def test_process_msg_queue(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: + default_conf['telegram']['enabled'] = True telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') mocker.patch('freqtrade.rpc.telegram.Telegram._init') - freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ @@ -166,7 +169,8 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non caplog) -def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: +def test_startupmessages_telegram_enabled(mocker, default_conf) -> None: + default_conf['telegram']['enabled'] = True telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index cc83f96e0..7978a2a23 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -36,6 +36,13 @@ from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, patch_exchange, patch_get_signal, patch_whitelist) +@pytest.fixture +def default_conf(default_conf) -> dict: + # Telegram is enabled by default + default_conf['telegram']['enabled'] = True + return default_conf + + class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator From 1d40162e9d1660a73d945a4bf7a30b48cc70ede4 Mon Sep 17 00:00:00 2001 From: Bohdan Kamuz Date: Sat, 15 Apr 2023 15:57:31 +0000 Subject: [PATCH 012/186] Telegram. Fixed the blacklist removal message. --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 51f7fb9ee..92d2c13a5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1413,12 +1413,12 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), ['adding', 'to']) - def send_blacklist_msg(self, blacklist: Dict): + def send_blacklist_msg(self, blacklist: Dict, action): errmsgs = [] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error {action[0]} `{pair}` {action[1]} blacklist: `{error['error_msg']}`") if errmsgs: self._send_msg('\n'.join(errmsgs)) @@ -1434,7 +1434,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), ['deleting', 'from']) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From 6bc8759321b32e348268b7d36d9901428596abbe Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Sat, 15 Apr 2023 20:01:12 +0200 Subject: [PATCH 013/186] Update constants.py Make progress bar true by default --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0da6a3b18..b8e240419 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -600,7 +600,7 @@ CONF_SCHEMA = { "policy_type": {"type": "string", "default": "MlpPolicy"}, "net_arch": {"type": "array", "default": [128, 128]}, "randomize_starting_position": {"type": "boolean", "default": False}, - "progress_bar": {"type": "boolean", "default": False}, + "progress_bar": {"type": "boolean", "default": True}, "model_reward_parameters": { "type": "object", "properties": { From f6e93114e66ef9a650fc34505ecb6fc67fad7f8b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Apr 2023 08:35:17 +0200 Subject: [PATCH 014/186] Update binance leverage tiers file --- .../exchange/binance_leverage_tiers.json | 1286 ++++++++++++----- 1 file changed, 903 insertions(+), 383 deletions(-) diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 597db27ff..0b9be0f55 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -1743,6 +1743,120 @@ } } ], + "AMB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "ANC/BUSD:BUSD": [ { "tier": 1.0, @@ -3526,10 +3640,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -3542,10 +3656,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -3558,10 +3672,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -3620,13 +3734,13 @@ "tier": 7.0, "currency": "BUSD", "minNotional": 3000000.0, - "maxNotional": 8000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "8000000", + "notionalCap": "4000000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", "cum": "949400.0" @@ -5487,120 +5601,6 @@ } } ], - "BTC/USDT:USDT-230331": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 375000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "375000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 375000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "2000000", - "notionalFloor": "375000", - "maintMarginRatio": "0.05", - "cum": "11250.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "4000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.1", - "cum": "111250.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "4", - "initialLeverage": "4", - "notionalCap": "10000000", - "notionalFloor": "4000000", - "maintMarginRatio": "0.125", - "cum": "211250.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "5", - "initialLeverage": "3", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.15", - "cum": "461250.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "40000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.25", - "cum": "2461250.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 40000000.0, - "maxNotional": 400000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "400000000", - "notionalFloor": "40000000", - "maintMarginRatio": "0.5", - "cum": "1.246125E7" - } - } - ], "BTC/USDT:USDT-230630": [ { "tier": 1.0, @@ -6016,10 +6016,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -6032,10 +6032,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -6046,13 +6046,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -6061,33 +6061,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "35650.0" } }, { @@ -6095,15 +6095,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "910650.0" } } ], @@ -6114,10 +6130,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -6130,10 +6146,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -6144,13 +6160,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -6159,33 +6175,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "35650.0" } }, { @@ -6193,15 +6209,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "910650.0" } } ], @@ -10097,120 +10129,6 @@ } } ], - "ETH/USDT:USDT-230331": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 375000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "375000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 375000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "2000000", - "notionalFloor": "375000", - "maintMarginRatio": "0.05", - "cum": "11250.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "4000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.1", - "cum": "111250.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "4", - "initialLeverage": "4", - "notionalCap": "10000000", - "notionalFloor": "4000000", - "maintMarginRatio": "0.125", - "cum": "211250.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "5", - "initialLeverage": "3", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.15", - "cum": "461250.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "40000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.25", - "cum": "2461250.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 40000000.0, - "maxNotional": 400000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "400000000", - "notionalFloor": "40000000", - "maintMarginRatio": "0.5", - "cum": "1.246125E7" - } - } - ], "ETH/USDT:USDT-230630": [ { "tier": 1.0, @@ -11930,10 +11848,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -11946,10 +11864,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -11962,10 +11880,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -12008,13 +11926,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -12559,6 +12477,120 @@ } } ], + "HFT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "HIGH/USDT:USDT": [ { "tier": 1.0, @@ -13945,6 +13977,120 @@ } } ], + "JOE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "KAVA/USDT:USDT": [ { "tier": 1.0, @@ -14682,19 +14828,133 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "2000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386885.0" } } ], + "LEVER/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "LINA/USDT:USDT": [ { "tier": 1.0, @@ -16850,10 +17110,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -16866,10 +17126,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -16882,10 +17142,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -16928,13 +17188,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -18154,10 +18414,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 11.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "11", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -18170,10 +18430,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -18186,10 +18446,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -18232,13 +18492,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -18653,6 +18913,120 @@ } } ], + "RDNT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "REEF/USDT:USDT": [ { "tier": 1.0, @@ -19508,10 +19882,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -19524,10 +19898,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -19540,10 +19914,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -19586,13 +19960,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -21251,14 +21625,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 15.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "15", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -21268,78 +21642,94 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "8", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 6500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "6500000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820650.0" } } ], @@ -21578,10 +21968,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -21594,10 +21984,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -21610,10 +22000,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -21656,13 +22046,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -21675,14 +22065,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -21692,78 +22082,94 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "898150.0" } } ], @@ -22458,10 +22864,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -22474,10 +22880,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -22490,10 +22896,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -22536,13 +22942,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -23965,6 +24371,120 @@ } } ], + "XVS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "YFI/USDT:USDT": [ { "tier": 1.0, From 4ed670f828ae6163b9630e3eef30374aebb0bf00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Apr 2023 08:56:47 +0200 Subject: [PATCH 015/186] pin pip installation to <23.1 - which breaks gym installation --- .github/workflows/ci.yml | 6 +++--- Dockerfile | 2 +- setup.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52c772bd3..21d6f9ef4 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 wheel + python -m pip install --upgrade pip==23.0.1 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 wheel + python -m pip install --upgrade pip==23.0.1 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 wheel + python -m pip install --upgrade pip==23.0.1 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 655f9ee94..422caecaf 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 + && pip install --upgrade pip==23.0.1 # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/setup.sh b/setup.sh index 77c77000d..805e13237 100755 --- a/setup.sh +++ b/setup.sh @@ -50,7 +50,7 @@ function updateenv() { 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 wheel setuptools==65.5.1 + ${PYTHON} -m pip install --upgrade pip==23.0.1 wheel setuptools==65.5.1 REQUIREMENTS_HYPEROPT="" REQUIREMENTS_PLOT="" REQUIREMENTS_FREQAI="" From 501a290f81a4ac32cee6d10e2468b61c84fcb066 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Apr 2023 09:56:26 +0200 Subject: [PATCH 016/186] Pin pip also for windows installs --- build_helpers/install_windows.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 2fc21d317..cf6fbdf07 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 wheel +python -m pip install --upgrade pip==23.0.1 wheel $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 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 017/186] 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 dba21c815a50366cfc6fc4a792bb1ad6ac28069b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 03:56:47 +0000 Subject: [PATCH 018/186] Bump tensorboard from 2.12.1 to 2.12.2 Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.12.1 to 2.12.2. - [Release notes](https://github.com/tensorflow/tensorboard/releases) - [Changelog](https://github.com/tensorflow/tensorboard/blob/master/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorboard/commits) --- updated-dependencies: - dependency-name: tensorboard dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 840598d23..51396ab91 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -8,4 +8,4 @@ joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 xgboost==1.7.5 -tensorboard==2.12.1 +tensorboard==2.12.2 From f1730ab51bd8b89601c4a729893fbbf763583334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 03:56:52 +0000 Subject: [PATCH 019/186] Bump httpx from 0.23.3 to 0.24.0 Bumps [httpx](https://github.com/encode/httpx) from 0.23.3 to 0.24.0. - [Release notes](https://github.com/encode/httpx/releases) - [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/httpx/compare/0.23.3...0.24.0) --- updated-dependencies: - dependency-name: httpx 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 690a7ee70..8c7738f64 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ isort==5.12.0 # For datetime mocking time-machine==2.9.0 # fastapi testing -httpx==0.23.3 +httpx==0.24.0 # Convert jupyter notebooks to markdown documents nbconvert==7.3.1 From f1fcd0760d08e9cbbdc72d6078572c6aebb43e72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 03:57:07 +0000 Subject: [PATCH 020/186] Bump pytest from 7.3.0 to 7.3.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.0 to 7.3.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.3.0...7.3.1) --- updated-dependencies: - dependency-name: pytest 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 690a7ee70..e90930b62 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ coveralls==3.3.1 ruff==0.0.261 mypy==1.2.0 pre-commit==3.2.2 -pytest==7.3.0 +pytest==7.3.1 pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 From 864f53bc4b06a87893a3e641b65d448a75730f07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 03:57:23 +0000 Subject: [PATCH 021/186] Bump cryptography from 40.0.1 to 40.0.2 Bumps [cryptography](https://github.com/pyca/cryptography) from 40.0.1 to 40.0.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/40.0.1...40.0.2) --- updated-dependencies: - dependency-name: cryptography 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 e09f8ba77..9e66e7f9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.5.3 pandas-ta==0.3.14b ccxt==3.0.59 -cryptography==40.0.1 +cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.9 python-telegram-bot==13.15 From 7c95848271d1b1cada8057a240d429aae7ec5064 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 03:57:33 +0000 Subject: [PATCH 022/186] Bump rich from 13.3.3 to 13.3.4 Bumps [rich](https://github.com/Textualize/rich) from 13.3.3 to 13.3.4. - [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.3...v13.3.4) --- 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 e09f8ba77..620010234 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 joblib==1.2.0 -rich==13.3.3 +rich==13.3.4 pyarrow==11.0.0; platform_machine != 'armv7l' # find first, C search in arrays From d1b600e7b0c419d433c7b6bfc23aff4732cf9b12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 04:29:10 +0000 Subject: [PATCH 023/186] Bump fastapi from 0.95.0 to 0.95.1 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.95.0 to 0.95.1. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.95.0...0.95.1) --- updated-dependencies: - dependency-name: fastapi 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 e09f8ba77..a0a6e760f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ orjson==3.8.10 sdnotify==0.3.2 # API Server -fastapi==0.95.0 +fastapi==0.95.1 pydantic==1.10.7 uvicorn==0.21.1 pyjwt==2.6.0 From 8a8cd6798810a14d179b94e05c3252d0c33277f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 07:12:03 +0200 Subject: [PATCH 024/186] Improve ccxt_ohlcv test debuggability --- tests/exchange/test_ccxt_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 4a65b16d7..34a4a1f0b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -530,7 +530,8 @@ class TestCCXTExchange(): candles = res[3] candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9 candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms - assert len(candles) >= min(candle_count, candle_count1) + assert len(candles) >= min(candle_count, candle_count1), \ + f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} " assert candles[0][0] == since_ms or (since_ms + timeframe_ms) def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): From 6c6d2a0f43e445084bbfac7f48870bcbf5922d24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 07:22:26 +0200 Subject: [PATCH 025/186] Improve live test resiliance --- tests/exchange/test_ccxt_compat.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 34a4a1f0b..60855ca54 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -528,10 +528,11 @@ class TestCCXTExchange(): assert res[1] == timeframe assert res[2] == candle_type candles = res[3] - candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9 - candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms + factor = 0.9 + candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor + candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor assert len(candles) >= min(candle_count, candle_count1), \ - f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} " + f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}" assert candles[0][0] == since_ms or (since_ms + timeframe_ms) def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): From 8aec71e27ef605f1cf06acb39c99523a3ff93317 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 07:25:09 +0200 Subject: [PATCH 026/186] Add bitvavo sublass to properly set ohlcv limit --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bitvavo.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 freqtrade/exchange/bitvavo.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index df10e40e5..8092d5af8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.binance import Binance from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bittrex import Bittrex +from freqtrade.exchange.bitvavo import Bitvavo from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.coinbasepro import Coinbasepro from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, diff --git a/freqtrade/exchange/bitvavo.py b/freqtrade/exchange/bitvavo.py new file mode 100644 index 000000000..ba1d355cc --- /dev/null +++ b/freqtrade/exchange/bitvavo.py @@ -0,0 +1,23 @@ +"""Kucoin exchange subclass.""" +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bitvavo(Exchange): + """Bitvavo exchange class. + + Contains adjustments needed for Freqtrade to work with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1440, + } From d34b15d6a9c4202b7b5ed2368abfa004561ef959 Mon Sep 17 00:00:00 2001 From: Bohdan Kamuz Date: Mon, 17 Apr 2023 09:40:41 +0000 Subject: [PATCH 027/186] Telegram. Fixed the blacklist removal message --- freqtrade/rpc/telegram.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92d2c13a5..a26841e14 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1413,12 +1413,13 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), ['adding', 'to']) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), 1) - def send_blacklist_msg(self, blacklist: Dict, action): + def send_blacklist_msg(self, blacklist: Dict, des): errmsgs = [] + act = ['adding', 'to'] if des == 1 else ['deleting', 'from'] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error {action[0]} `{pair}` {action[1]} blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error {act[0]} `{pair}` {act[1]} blacklist: `{error['error_msg']}`") if errmsgs: self._send_msg('\n'.join(errmsgs)) @@ -1434,7 +1435,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), ['deleting', 'from']) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), 0) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From d73e7f292a96fc0a6181ac9076174fc2bb8bda70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 19:42:41 +0200 Subject: [PATCH 028/186] simplify Leverage tier code --- freqtrade/exchange/exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ba7d79f97..b5ea247ef 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2371,12 +2371,12 @@ class Exchange: # Must fetch the leverage tiers for each market separately # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets markets = self.markets - symbols = [] - for symbol, market in markets.items(): + symbols = [ + symbol for symbol, market in markets.items() if (self.market_is_future(market) - and market['quote'] == self._config['stake_currency']): - symbols.append(symbol) + and market['quote'] == self._config['stake_currency']) + ] tiers: Dict[str, List[Dict]] = {} From b2ea464250c1030d0ef197c0922083a4e9cfd40a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 19:48:33 +0200 Subject: [PATCH 029/186] Handle individual exceptions when initializing leverage tiers closes #8515 closes #8512 closes #8514 --- freqtrade/exchange/exchange.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b5ea247ef..fd9ddfba6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2404,17 +2404,18 @@ class Exchange: with self._loop_lock: results = self.loop.run_until_complete(gather_results()) - for symbol, res in results: - tiers[symbol] = res + for res in results: + if isinstance(res, Exception): + logger.warning(f"Leverage tier exception: {repr(res)}") + continue + symbol, tier = res + tiers[symbol] = tier if len(coros) > 0: self.cache_leverage_tiers(tiers, self._config['stake_currency']) logger.info(f"Done initializing {len(symbols)} markets.") return tiers - else: - return {} - else: - return {} + return {} def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None: From 14bca509da52131c5bed0215e128bd8f58da3b4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 19:55:58 +0200 Subject: [PATCH 030/186] Cleanup some code --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fd9ddfba6..358e71c85 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2396,13 +2396,13 @@ class Exchange: else: logger.info("Using cached leverage_tiers.") - async def gather_results(): + async def gather_results(input_coro): return await asyncio.gather(*input_coro, return_exceptions=True) for input_coro in chunks(coros, 100): with self._loop_lock: - results = self.loop.run_until_complete(gather_results()) + results = self.loop.run_until_complete(gather_results(input_coro)) for res in results: if isinstance(res, Exception): From 362974b831b4e499f6f6523b87c306191522542e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 20:00:57 +0200 Subject: [PATCH 031/186] Update test to properly capture errors from leverage initialization --- tests/exchange/test_okx.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 7a3fa22f0..3824eddb7 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -1,14 +1,14 @@ from datetime import datetime, timedelta, timezone from pathlib import Path -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock import ccxt import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode -from freqtrade.exceptions import RetryableOrderError +from freqtrade.exceptions import RetryableOrderError, TemporaryError from freqtrade.exchange.exchange import timeframe_to_minutes -from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has +from tests.conftest import EXMS, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -278,7 +278,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, 'fetchLeverageTiers': False, 'fetchMarketLeverageTiers': True, }) - api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[ + api_mock.fetch_market_leverage_tiers = AsyncMock(side_effect=[ [ { 'tier': 1, @@ -341,6 +341,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, } }, ], + TemporaryError("this Failed"), [ { 'tier': 1, From f049268354d1406c7f8bf205dd8f66b7ab016996 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 18:03:14 +0000 Subject: [PATCH 032/186] Bump ccxt from 3.0.59 to 3.0.69 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.59 to 3.0.69. - [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.59...3.0.69) --- 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 07ddde148..7db852961 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.59 +ccxt==3.0.69 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.9 From 7ff35fea3c297cb5643654345aeb293edfcb87f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 20:20:38 +0200 Subject: [PATCH 033/186] Default weekly report to monday closes #8502 --- freqtrade/optimize/optimize_reports.py | 3 ++- tests/optimize/test_optimize_reports.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b4925770d..cbae6aede 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -273,7 +273,8 @@ def _get_resample_from_period(period: str) -> str: if period == 'day': return '1d' if period == 'week': - return '1w' + # Weekly defaulting to Monday. + return '1W-MON' if period == 'month': return '1M' raise ValueError(f"Period {period} is not supported.") diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 0cc32baaf..9fdd0d61e 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -465,7 +465,7 @@ def test_generate_periodic_breakdown_stats(testdatadir): def test__get_resample_from_period(): assert _get_resample_from_period('day') == '1d' - assert _get_resample_from_period('week') == '1w' + assert _get_resample_from_period('week') == '1W-MON' assert _get_resample_from_period('month') == '1M' with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') From 3fb5cd3df6efa96e65d6fa79dafb420641ef06b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 20:27:18 +0200 Subject: [PATCH 034/186] 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 dfe8b3e832f348ab5fbff1ec85ba23ab57ca3984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Apr 2023 20:32:07 +0200 Subject: [PATCH 035/186] Improve ruff rule selection --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index baf707c68..28de6a1d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,9 @@ target-version = "py38" extend-select = [ "C90", # mccabe # "N", # pep8-naming + "F", # pyflakes + "E", # pycodestyle + "W", # pycodestyle "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable From 6f401a9e150be8f569396e129fcf698182488a1f Mon Sep 17 00:00:00 2001 From: Joe Schr <8218910+TheJoeSchr@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:10:46 +0200 Subject: [PATCH 036/186] docs: use helper function `stoploss_from_absolute` in strategy callbacks "absolute" example --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 329908527..070eb6d48 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -352,7 +352,7 @@ class AwesomeStrategy(IStrategy): # Convert absolute price to percentage relative to current_rate if stoploss_price < current_rate: - return (stoploss_price / current_rate) - 1 + return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From c297d999750ada0e7c01f8b10b1d6fae65a20084 Mon Sep 17 00:00:00 2001 From: Bohdan Kamuz Date: Tue, 18 Apr 2023 10:09:48 +0000 Subject: [PATCH 037/186] Telegram. Fixed the blacklist removal message --- freqtrade/rpc/telegram.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a26841e14..e626ee598 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1413,13 +1413,12 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), 1) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - def send_blacklist_msg(self, blacklist: Dict, des): + def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] - act = ['adding', 'to'] if des == 1 else ['deleting', 'from'] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error {act[0]} `{pair}` {act[1]} blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: self._send_msg('\n'.join(errmsgs)) @@ -1435,7 +1434,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), 0) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From f9124ef5b9c5150f2d3edf58904d8d562cf74748 Mon Sep 17 00:00:00 2001 From: Joe Schr <8218910+TheJoeSchr@users.noreply.github.com> Date: Tue, 18 Apr 2023 17:36:36 +0200 Subject: [PATCH 038/186] docs: strategy-callbacks: removes outdated `leverage` argument --- docs/strategy-callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 070eb6d48..a99b2ae29 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -318,11 +318,11 @@ class AwesomeStrategy(IStrategy): # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage) + return stoploss_from_open(0.25, current_profit, is_short=trade.is_short) elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage) + return stoploss_from_open(0.15, current_profit, is_short=trade.is_short) elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From caf524c68584995e5ef6e4d2ba165ca4279e264f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Apr 2023 18:01:12 +0200 Subject: [PATCH 039/186] Don't fail on leverage tier loading error closes #8512 --- freqtrade/exchange/exchange.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 358e71c85..7e276d538 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2431,14 +2431,17 @@ class Exchange: def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]: filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json" if filename.is_file(): - tiers = file_load_json(filename) - updated = tiers.get('updated') - if updated: - updated_dt = parser.parse(updated) - if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): - logger.info("Cached leverage tiers are outdated. Will update.") - return None - return tiers['data'] + try: + tiers = file_load_json(filename) + updated = tiers.get('updated') + if updated: + updated_dt = parser.parse(updated) + if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): + logger.info("Cached leverage tiers are outdated. Will update.") + return None + return tiers['data'] + except Exception: + logger.exception("Error loading cached leverage tiers. Refreshing.") return None def fill_leverage_tiers(self) -> None: From 94e190b954d48aec11d549c0de7aa25de172fbdc Mon Sep 17 00:00:00 2001 From: TheJoeSchr <8218910+TheJoeSchr@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:22:52 +0200 Subject: [PATCH 040/186] docs: strategy-callbacks: revert `leverage=trade.leverage` removal --- docs/strategy-callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index a99b2ae29..070eb6d48 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -318,11 +318,11 @@ class AwesomeStrategy(IStrategy): # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From 5ed072c489ea1f54c3a3d30b1e783744ed5c5b67 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 20:17:31 +0800 Subject: [PATCH 041/186] Remove extraneous code block --- docs/stoploss.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index d85902be0..8fc73be21 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -209,11 +209,6 @@ You can also keep a static stoploss until the offset is reached, and then trail If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. -``` python - trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = True -``` - Configuration (offset is buy-price + 3%): ``` python From 46f4fd79afaed9e674b4d2ed89b60c1fed8c35e5 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 20:43:41 +0800 Subject: [PATCH 042/186] Fix anchor for missing heading --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8a1aeb40e..8c0f8d882 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -165,7 +165,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | | **Pricing** -| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). | `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean | `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer From 66f5f76a6c6f89eb6889c8aa86f60a2399198db6 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 20:43:41 +0800 Subject: [PATCH 043/186] Fix anchor for missing heading --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8c0f8d882..783060bbd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -138,7 +138,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_currency` | **Required.** Crypto-currency used for trading.
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. -| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. +| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. From 2f9e6c990c3908168a85903839cdec6b8996faca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Apr 2023 15:26:33 +0200 Subject: [PATCH 044/186] Update docs/strategy-callbacks.md --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 070eb6d48..a13bdfd02 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -352,7 +352,7 @@ class AwesomeStrategy(IStrategy): # Convert absolute price to percentage relative to current_rate if stoploss_price < current_rate: - return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)) + return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From 670a584d7ec42a6e4406bb8aa5a911e4dc191576 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 22:00:31 +0800 Subject: [PATCH 045/186] Fix markdown inconsistencies --- docs/configuration.md | 20 ++++++++++---------- docs/producer-consumer.md | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 783060bbd..cf3872f1c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to None.*
**Datatype:** Float +| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to `None`.*
**Datatype:** Float | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float | | **Unfilled timeout** | `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `"minutes"`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | | **Pricing** -| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). -| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) -| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled). -| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)
*Defaults to `1`.*
**Datatype:** Positive Integer | `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float | | **TODO** @@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer -| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean -| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".
*Defaults to `None`
**Datatype:** float -| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`*
**Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | | **Plugins** | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options. @@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`.
**Datatype:** float -| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `True`.
**Datatype:** boolean +| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `true`.
**Datatype:** boolean | `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.
**Datatype:** dictionary | `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function.
**Datatype:** Boolean | | **Webhook** diff --git a/docs/producer-consumer.md b/docs/producer-consumer.md index c52279f26..0bd52ac93 100644 --- a/docs/producer-consumer.md +++ b/docs/producer-consumer.md @@ -49,7 +49,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect | `wait_timeout` | Timeout until we ping again if no message is received.
*Defaults to `300`.*
**Datatype:** Integer - in seconds. | `ping_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. | `sleep_time` | Sleep time before retrying to connect.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. -| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `False`.*
**Datatype:** Boolean. +| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `false`.*
**Datatype:** Boolean. | `message_size_limit` | Size limit per message
*Defaults to `8`.*
**Datatype:** Integer - Megabytes. Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist. From f1e03a68739fd62d36eb7f893ef6b1e233f7dc64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Apr 2023 18:20:25 +0200 Subject: [PATCH 046/186] 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 1056ff0d1875030202675e9b035da1e42e52bfcf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Apr 2023 18:20:25 +0200 Subject: [PATCH 047/186] 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 b545fc5590f03d84ba7aa5eaadba8b9aca896a70 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Thu, 20 Apr 2023 13:36:14 +0800 Subject: [PATCH 048/186] Correct user namespace from `cust_` to `custom_` --- docs/strategy-advanced.md | 2 +- docs/strategy-callbacks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cbb71e810..b38bd1c3f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -15,7 +15,7 @@ If you're just getting started, please be familiar with the methods described in Storing information can be accomplished by creating a new dictionary within the strategy class. -The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. +The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. ```python class AwesomeStrategy(IStrategy): diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index a13bdfd02..855f2353b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy): if self.config['runmode'].value in ('live', 'dry_run'): # Assign this to the class by using self.* # can then be used by populate_* methods - self.cust_remote_data = requests.get('https://some_remote_source.example.com') + self.custom_remote_data = requests.get('https://some_remote_source.example.com') ``` From 818da02f6ca8c81a8b476d9065dba65390611603 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Thu, 20 Apr 2023 14:01:01 +0800 Subject: [PATCH 049/186] Edit `Advanced Strategies` intro for clarify and brevity --- docs/strategy-advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b38bd1c3f..a93dcecdf 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -1,15 +1,15 @@ # Advanced Strategies This page explains some advanced concepts available for strategies. -If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. +If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first. -[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. +The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs. !!! Note - All callback methods described below should only be implemented in a strategy if they are actually used. + Callback methods should *only* be implemented if a strategy uses them. !!! Tip - You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` + Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` ## Storing information From ca1a616b898e4384b68d5ff7d72f23c349d763d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Apr 2023 07:08:14 +0200 Subject: [PATCH 050/186] use Fstrings for log message --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73b25a7a1..7a1f7151e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1425,7 +1425,7 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('%s order %s for %s.', side, reason, trade) + logger.info(f'{side} order {reason} for {trade}.') # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') From 4a4be27ebc01b85c7af48d5f2949345082ec7959 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Apr 2023 07:14:00 +0200 Subject: [PATCH 051/186] use orderid from order, the trade one has been reset part of #8526 --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7a1f7151e..9cc26ad77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1507,7 +1507,7 @@ class FreqtradeBot(LoggingMixin): trade.exit_reason = None trade.open_order_id = None - self.update_trade_state(trade, trade.open_order_id, order) + self.update_trade_state(trade, order['id'], order) logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') trade.close_rate = None From 9bc17a92323015599d86460bd408cc3ecf3758cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Apr 2023 06:19:57 +0000 Subject: [PATCH 052/186] Downgrade wheel to isntall gym --- .github/workflows/ci.yml | 6 +++--- Dockerfile | 2 +- build_helpers/install_windows.ps1 | 2 +- setup.sh | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21d6f9ef4..7e4487ac8 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 + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 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 + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 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 + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 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 422caecaf..ee8b3f0a8 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 + && pip install --upgrade pip==23.0.1 wheel==0.38.4 # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index cf6fbdf07..3e7df5dfc 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 +python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" diff --git a/setup.sh b/setup.sh index 805e13237..d46569a53 100755 --- a/setup.sh +++ b/setup.sh @@ -50,7 +50,7 @@ function updateenv() { 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 setuptools==65.5.1 + ${PYTHON} -m pip install --upgrade pip==23.0.1 wheel==0.38.4 setuptools==65.5.1 REQUIREMENTS_HYPEROPT="" REQUIREMENTS_PLOT="" REQUIREMENTS_FREQAI="" From 0a05099713096d4c21f816d0e03c5bef7d64f3ff Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 21 Apr 2023 22:52:19 +0200 Subject: [PATCH 053/186] 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 5dccfab89c5f4788c4f0728549a07c78037faaee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 11:27:04 +0200 Subject: [PATCH 054/186] Add test for start_cap_ratio --- tests/rpc/test_rpc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ff08a0564..5a84eaa48 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -591,6 +591,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'side': 'short', } ] + assert result['starting_capital'] == 10 + assert result['starting_capital_ratio'] == 0.0 def test_rpc_start(mocker, default_conf) -> None: From ce75a032d023130c6a39ca7626197f79fa6d3dc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 10:58:49 +0200 Subject: [PATCH 055/186] Balance ratio calculation should ignore non-relevant assets closes #8532 --- freqtrade/rpc/rpc.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ffed3c6d6..6987017b8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -585,11 +585,13 @@ class RPC: """ Returns current account balance per crypto """ currencies: List[Dict] = [] total = 0.0 + total_bot = 0.0 try: tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') - + open_trades: List[Trade] = Trade.get_open_trades() + open_assets = [t.base_currency for t in open_trades] self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( @@ -618,7 +620,9 @@ class RPC: except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue - total = total + est_stake + total += est_stake + if coin == stake_currency or coin in open_assets: + total_bot += est_stake currencies.append({ 'currency': coin, 'free': balance.free, @@ -651,10 +655,12 @@ class RPC: value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + value_bot = self._fiat_converter.convert_amount( + total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0 trade_count = len(Trade.get_trades_proxy()) - starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 - starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': currencies, From cb09ef71807488fe726bdacd823af12fc9eb193a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 11:08:37 +0200 Subject: [PATCH 056/186] Extract converting wallet to est_stake --- freqtrade/rpc/rpc.py | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6987017b8..814f0d6a8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,6 +24,7 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirecti State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs +from freqtrade.exchange.types import Tickers from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade @@ -581,15 +582,38 @@ class RPC: 'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '', } + def __balance_get_est_stake( + self, coin: str, stake_currency: str, balance: Wallet, tickers) -> float: + est_stake = 0.0 + if coin == stake_currency: + est_stake = balance.total + if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + # in Futures, "total" includes the locked stake, and therefore all positions + est_stake = balance.free + else: + try: + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate: Optional[float] = tickers.get(pair, {}).get('last', None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + except (ExchangeError): + logger.warning(f"Could not get rate for pair {coin}.") + raise ValueError() + + return est_stake + def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ currencies: List[Dict] = [] total = 0.0 total_bot = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers(cached=True) + tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') + open_trades: List[Trade] = Trade.get_open_trades() open_assets = [t.base_currency for t in open_trades] self._freqtrade.wallets.update(require_update=False) @@ -601,25 +625,11 @@ class RPC: for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue + try: + est_stake = self.__balance_get_est_stake(coin, stake_currency, balance, tickers) + except ValueError: + continue - est_stake: float = 0 - if coin == stake_currency: - rate = 1.0 - est_stake = balance.total - if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - # in Futures, "total" includes the locked stake, and therefore all positions - est_stake = balance.free - else: - try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate = tickers.get(pair, {}).get('last') - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - except (ExchangeError): - logger.warning(f" Could not get rate for pair {coin}.") - continue total += est_stake if coin == stake_currency or coin in open_assets: total_bot += est_stake From 7a47500b22ae5781f643fc6584662ed0e01af505 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 14:57:13 +0200 Subject: [PATCH 057/186] Add "is_bot_managed" flag to API --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 3 ++- freqtrade/rpc/rpc.py | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 53bf7558f..a9dc6243e 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -43,6 +43,7 @@ class Balance(BaseModel): leverage: float is_position: bool position: float + is_bot_managed: bool class Balances(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 8aa706e62..5ee5e36c4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -43,7 +43,8 @@ logger = logging.getLogger(__name__) # 2.23: Allow plot config request in webserver mode # 2.24: Add cancel_open_order endpoint # 2.25: Add several profit values to /status endpoint -API_VERSION = 2.25 +# 2.26: increase /balance output +API_VERSION = 2.26 # Public API, requires no auth. router_public = APIRouter() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 814f0d6a8..a0b588bfa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -631,7 +631,8 @@ class RPC: continue total += est_stake - if coin == stake_currency or coin in open_assets: + is_bot_managed = coin == stake_currency or coin in open_assets + if is_bot_managed: total_bot += est_stake currencies.append({ 'currency': coin, @@ -643,6 +644,7 @@ class RPC: 'side': 'long', 'leverage': 1, 'position': 0, + 'is_bot_managed': is_bot_managed, 'is_position': False, }) symbol: str @@ -660,6 +662,7 @@ class RPC: 'stake': stake_currency, 'leverage': position.leverage, 'side': position.side, + 'is_bot_managed': True, 'is_position': True }) From dbf1f0897e2bbcb2dd220d20f8d05af5cc8eb99d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 15:51:28 +0200 Subject: [PATCH 058/186] Add /balance full, reduce regular balance output closes #4497 --- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 23 +++++++++++++---------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index fe990790a..e6017e271 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -191,7 +191,8 @@ official commands. You can ask at any moment for help with `/help`. | **Metrics** | | `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/performance` | Show performance of each finished trade grouped by pair -| `/balance` | Show account balance per currency +| `/balance` | Show bot managed balance per currency +| `/balance full` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/weekly ` | Shows profit or loss per week, over the last n weeks (n defaults to 8) | `/monthly ` | Shows profit or loss per month, over the last n months (n defaults to 6) @@ -202,7 +203,6 @@ official commands. You can ask at any moment for help with `/help`. | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/edge` | Show validated pairs by Edge if it is enabled. - ## Telegram commands in action Below, example of Telegram message you will receive for each command. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a0b588bfa..addd2bd02 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -678,8 +678,10 @@ class RPC: return { 'currencies': currencies, 'total': total, + 'total_bot': total_bot, 'symbol': fiat_display_currency, 'value': value, + 'value_bot': value_bot, 'stake': stake_currency, 'starting_capital': starting_capital, 'starting_capital_ratio': starting_capital_ratio, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e626ee598..c4feb588a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -905,6 +905,7 @@ class Telegram(RPCHandler): @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ + full_result = context.args and 'full' in context.args result = self._rpc._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) @@ -928,7 +929,7 @@ class Telegram(RPCHandler): total_dust_currencies = 0 for curr in result['currencies']: curr_output = '' - if curr['est_stake'] > balance_dust_level: + if curr['est_stake'] > balance_dust_level and (full_result or curr['is_bot_managed']): if curr['is_position']: curr_output = ( f"*{curr['currency']}:*\n" @@ -965,14 +966,15 @@ class Telegram(RPCHandler): tc = result['trade_count'] > 0 stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else '' fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else '' - - output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: " - f"{round_coin_value(result['total'], result['stake'], False)}`" - f"{stake_improve}\n" - f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`" - f"{fiat_val}\n") + value = round_coin_value( + result['value' if full_result else 'value_bot'], result['symbol'], False) + total_stake = round_coin_value( + result['total' if full_result else 'total_bot'], result['stake'], False) + output += ( + f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n" + f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" + f"\t`{result['symbol']}: {value}`{fiat_val}\n" + ) self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) @@ -1528,7 +1530,8 @@ class Telegram(RPCHandler): "------------\n" "*/show_config:* `Show running configuration` \n" "*/locks:* `Show currently locked pairs`\n" - "*/balance:* `Show account balance per currency`\n" + "*/balance:* `Show bot managed balance per currency`\n" + "*/balance total:* `Show account balance per currency`\n" "*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" From c4f8ff95ddd263e2c2f49a066889598c92d98e74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 16:13:27 +0200 Subject: [PATCH 059/186] Update tests --- freqtrade/rpc/api_server/api_schemas.py | 2 ++ tests/rpc/test_rpc.py | 7 ++++++- tests/rpc/test_rpc_apiserver.py | 3 +++ tests/rpc/test_rpc_telegram.py | 15 +++++++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a9dc6243e..20144d2c8 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -49,8 +49,10 @@ class Balance(BaseModel): class Balances(BaseModel): currencies: List[Balance] total: float + total_bot: float symbol: str value: float + value_bot: float stake: str note: str starting_capital: float diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5a84eaa48..d675ffe4e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -552,6 +552,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, }, { 'free': 1.0, @@ -564,7 +565,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', - + 'is_bot_managed': False, }, { 'free': 5.0, @@ -577,6 +578,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': False, }, { 'free': 0.0, @@ -589,8 +591,11 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 5.0, 'position': 1000.0, 'side': 'short', + 'is_bot_managed': True, } ] + assert pytest.approx(result['total_bot']) == 10 + assert pytest.approx(result['total']) == 30.309096 assert result['starting_capital'] == 10 assert result['starting_capital_ratio'] == 0.0 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 58c904838..ad7543430 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -486,7 +486,10 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, } + assert response['total'] == 12.159513094 + assert response['total_bot'] == 12.0 assert 'starting_capital' in response assert 'starting_capital_fiat' in response assert 'starting_capital_pct' in response diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7978a2a23..a672c62f6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -783,19 +783,27 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick patch_get_signal(freqtradebot) telegram._balance(update=update, context=MagicMock()) + context = MagicMock() + context.args = ["full"] + telegram._balance(update=update, context=context) result = msg_mock.call_args_list[0][0][0] - assert msg_mock.call_count == 1 + result_full = msg_mock.call_args_list[1][0][0] + assert msg_mock.call_count == 2 assert '*BTC:*' in result assert '*ETH:*' not in result assert '*USDT:*' not in result assert '*EUR:*' not in result - assert '*LTC:*' in result + assert '*LTC:*' not in result + + assert '*LTC:*' in result_full assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result assert 'BTC: 12' in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result + assert '*Estimated Value*:' in result_full + assert '*Estimated Value (Bot managed assets only)*:' in result def test_balance_handle_empty_response(default_conf, update, mocker) -> None: @@ -840,12 +848,15 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, }) mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ 'currencies': balances, 'total': 100.0, + 'total_bot': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'value_bot': 1000.0, 'starting_capital': 1000, 'starting_capital_fiat': 1000, }) From f937818b800343cf94d7f70d6368ab7b12c8677c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 17:13:53 +0200 Subject: [PATCH 060/186] Add "owned" fields to balance --- freqtrade/rpc/api_server/api_schemas.py | 2 ++ freqtrade/rpc/rpc.py | 24 ++++++++++++++++++------ freqtrade/rpc/telegram.py | 11 +++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 20144d2c8..dd5ca3a62 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -36,7 +36,9 @@ class Balance(BaseModel): free: float balance: float used: float + bot_owned: Optional[float] est_stake: float + est_stake_bot: Optional[float] stake: str # Starting with 2.x side: str diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index addd2bd02..06cf61dff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -583,13 +583,16 @@ class RPC: } def __balance_get_est_stake( - self, coin: str, stake_currency: str, balance: Wallet, tickers) -> float: + self, coin: str, stake_currency: str, amount: float, + balance: Wallet, tickers) -> Tuple[float, float]: est_stake = 0.0 + est_bot_stake = 0.0 if coin == stake_currency: est_stake = balance.total if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: # in Futures, "total" includes the locked stake, and therefore all positions est_stake = balance.free + est_bot_stake = est_stake else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) @@ -598,11 +601,12 @@ class RPC: if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total + est_bot_stake = rate * amount except (ExchangeError): logger.warning(f"Could not get rate for pair {coin}.") raise ValueError() - return est_stake + return est_stake, est_bot_stake def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ @@ -615,7 +619,7 @@ class RPC: raise RPCException('Error getting current tickers.') open_trades: List[Trade] = Trade.get_open_trades() - open_assets = [t.base_currency for t in open_trades] + open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades} self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( @@ -625,21 +629,29 @@ class RPC: for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue + + trade = open_assets.get(coin, None) + is_bot_managed = coin == stake_currency or trade is not None + trade_amount = trade.amount if trade else 0 + try: - est_stake = self.__balance_get_est_stake(coin, stake_currency, balance, tickers) + est_stake, est_stake_bot = self.__balance_get_est_stake( + coin, stake_currency, trade_amount, balance, tickers) except ValueError: continue total += est_stake - is_bot_managed = coin == stake_currency or coin in open_assets + if is_bot_managed: - total_bot += est_stake + total_bot += est_stake_bot currencies.append({ 'currency': coin, 'free': balance.free, 'balance': balance.total, 'used': balance.used, + 'bot_owned': trade_amount, 'est_stake': est_stake or 0, + 'est_stake_bot': est_stake_bot if is_bot_managed else 0, 'stake': stake_currency, 'side': 'long', 'leverage': 1, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c4feb588a..2b997efac 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -916,8 +916,7 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: output += "*Warning:* Simulated balances in Dry Mode.\n" - starting_cap = round_coin_value( - result['starting_capital'], self._config['stake_currency']) + starting_cap = round_coin_value(result['starting_capital'], self._config['stake_currency']) output += f"Starting capital: `{starting_cap}`" starting_cap_fiat = round_coin_value( result['starting_capital_fiat'], self._config['fiat_display_currency'] @@ -938,13 +937,17 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: + est_stake = round_coin_value( + curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False) + curr_output = ( f"*{curr['currency']}:*\n" f"\t`Available: {curr['free']:.8f}`\n" f"\t`Balance: {curr['balance']:.8f}`\n" f"\t`Pending: {curr['used']:.8f}`\n" - f"\t`Est. {curr['stake']}: " - f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") + f"\t`Bot Owned: {curr['bot_owned']:.8f}`\n" + f"\t`Est. {curr['stake']}: {est_stake}`\n") + elif curr['est_stake'] <= balance_dust_level: total_dust_balance += curr['est_stake'] total_dust_currencies += 1 From 741834301f24630a209663b375cdb2326913fb27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 17:21:03 +0200 Subject: [PATCH 061/186] Update tests --- tests/rpc/test_rpc.py | 6 ++++++ tests/rpc/test_rpc_telegram.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d675ffe4e..34095f4f2 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -546,7 +546,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 10.0, 'balance': 12.0, 'used': 2.0, + 'bot_owned': 0, 'est_stake': 10.0, # In futures mode, "free" is used here. + 'est_stake_bot': 10, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, @@ -558,7 +560,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 1.0, 'balance': 5.0, 'currency': 'ETH', + 'bot_owned': 0, 'est_stake': 0.30794, + 'est_stake_bot': 0, 'used': 4.0, 'stake': 'BTC', 'is_position': False, @@ -571,7 +575,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 5.0, 'balance': 10.0, 'currency': 'USDT', + 'bot_owned': 0, 'est_stake': 0.0011562404610161968, + 'est_stake_bot': 0, 'used': 5.0, 'stake': 'BTC', 'is_position': False, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a672c62f6..97ac03c9f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -842,7 +842,9 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'free': 1.0, 'used': 0.5, 'balance': i, + 'bot_owned': 0.5, 'est_stake': 1, + 'est_stake_bot': 1, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, From 94a6bc608ca78cb8eff7a0bc46cab3e254b72fa0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 17:42:09 +0200 Subject: [PATCH 062/186] Update stake-currency behavior --- freqtrade/rpc/rpc.py | 2 ++ tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 06cf61dff..222912aa0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -633,6 +633,8 @@ class RPC: trade = open_assets.get(coin, None) is_bot_managed = coin == stake_currency or trade is not None trade_amount = trade.amount if trade else 0 + if coin == stake_currency: + trade_amount = self._freqtrade.wallets.get_available_stake_amount() try: est_stake, est_stake_bot = self.__balance_get_est_stake( diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 34095f4f2..5335cebf9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -546,7 +546,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 10.0, 'balance': 12.0, 'used': 2.0, - 'bot_owned': 0, + 'bot_owned': 9.9, # available stake - reducing by reserved amount 'est_stake': 10.0, # In futures mode, "free" is used here. 'est_stake_bot': 10, 'stake': 'BTC', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ad7543430..a4638a07e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -480,7 +480,9 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'free': 12.0, 'balance': 12.0, 'used': 0.0, + 'bot_owned': pytest.approx(11.879999), 'est_stake': 12.0, + 'est_stake_bot': 12.0, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, From c20b074880ab37d2df425eb9e5653629fa4bcdb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:20:24 +0200 Subject: [PATCH 063/186] Add FAQ entry about permission error --- docs/faq.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index b52a77c6b..7b8cc2580 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -142,6 +142,13 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. +### I'm trying to start the bot live, but get an API permission error + +Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say. +Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console. +Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary. +Futures will usually have to be enabled specifically. + ### How do I search the bot logs for something? By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. From c5dc21e80c41322a6551a3d1d53b05d474156c85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:30:30 +0200 Subject: [PATCH 064/186] Update freqAI documentation with missing typehints --- docs/freqai-configuration.md | 9 +++++---- docs/freqai-reinforcement-learning.md | 6 ++++-- docs/strategy_migration.md | 8 ++++---- freqtrade/strategy/interface.py | 7 ++++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 233edf2c5..71c614b4a 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -52,7 +52,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -139,6 +139,7 @@ The FreqAI strategy requires including the following lines of code in the standa / dataframe["close"] - 1 ) + return dataframe ``` Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`. diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index f298dbf4d..962827348 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -37,7 +37,7 @@ freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --con where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column: ```python - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -53,17 +53,19 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from # For RL, there are no direct targets to set. This is filler (neutral) # until the agent sends an action. dataframe["&-action"] = 0 + return dataframe ``` Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: ```python - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: # The following features are necessary for RL models dataframe[f"%-raw_close"] = dataframe["close"] dataframe[f"%-raw_open"] = dataframe["open"] dataframe[f"%-raw_high"] = dataframe["high"] dataframe[f"%-raw_low"] = dataframe["low"] + return dataframe ``` Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action. diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 22e3d2c22..5ef7a5a4c 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -578,7 +578,7 @@ def populate_any_indicators( Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed. ``` python linenums="1" - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as Basic features. Make sure to remove the `{pair}` part from your features. ``` python linenums="1" - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. ### FreqAI - feature engineering standard ``` python linenums="1" - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. Targets now get their own, dedicated method. ``` python linenums="1" - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3bc766d91..9bc23275d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -618,7 +618,7 @@ class IStrategy(ABC, HyperStrategyMixin): return df def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -644,7 +644,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -697,7 +698,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. From ad4996259eb7db685803cc2329177457bd50d1e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:36:17 +0200 Subject: [PATCH 065/186] Further increase typehints in freqAI interface --- docs/freqai-configuration.md | 2 +- docs/freqai-feature-engineering.md | 8 ++++---- freqtrade/strategy/interface.py | 3 ++- freqtrade/templates/FreqaiExampleHybridStrategy.py | 10 ++++++---- freqtrade/templates/FreqaiExampleStrategy.py | 10 ++++++---- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 71c614b4a..e7aca20be 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -387,7 +387,7 @@ Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. T For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows: ```python - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > dataframe["close"], 'up', 'down') diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 05c6db523..52d5e1b2c 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: ```python - def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -67,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in return dataframe - def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -96,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, metadata, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, metadata, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9bc23275d..7adb7a154 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -674,7 +674,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. diff --git a/freqtrade/templates/FreqaiExampleHybridStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py index 3f27ee4a1..03446d76e 100644 --- a/freqtrade/templates/FreqaiExampleHybridStrategy.py +++ b/freqtrade/templates/FreqaiExampleHybridStrategy.py @@ -97,7 +97,7 @@ class FreqaiExampleHybridStrategy(IStrategy): exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -151,7 +151,8 @@ class FreqaiExampleHybridStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -183,7 +184,8 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -209,7 +211,7 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0093c7f7a..493ea17f3 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -48,7 +48,7 @@ class FreqaiExampleStrategy(IStrategy): [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -106,7 +106,8 @@ class FreqaiExampleStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -142,7 +143,8 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -172,7 +174,7 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. From 2dd3b341360dc3f85068bf1408c6d2414cd08ace Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:36:27 +0200 Subject: [PATCH 066/186] Fix malrendering in freqAI docs --- docs/freqai-feature-engineering.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 52d5e1b2c..82b7569a5 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -181,15 +181,14 @@ You can ask for each of the defined features to be included also for informative In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles` $= 3 * 3 * 3 * 2 * 2 = 108$. - ### Gain finer control over `feature_engineering_*` functions with `metadata` - All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. +All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. - ```python -def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs): - if metadata["tf"] == "1h": - dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) +```python +def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: + if metadata["tf"] == "1h": + dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) ``` This will block `ta.ROC()` from being added to any timeframes other than `"1h"`. From 185ea9c98c187ddf56817c7f6faae9110abe3972 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:56:58 +0000 Subject: [PATCH 067/186] Bump websockets from 11.0.1 to 11.0.2 Bumps [websockets](https://github.com/aaugustin/websockets) from 11.0.1 to 11.0.2. - [Release notes](https://github.com/aaugustin/websockets/releases) - [Commits](https://github.com/aaugustin/websockets/compare/11.0.1...11.0.2) --- updated-dependencies: - dependency-name: websockets 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 7db852961..2872daab6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,7 +54,7 @@ python-dateutil==2.8.2 schedule==1.2.0 #WS Messages -websockets==11.0.1 +websockets==11.0.2 janus==1.0.0 ast-comments==1.0.1 From d320ea052f25ab711eea08884d9745334ac1c840 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:57:22 +0000 Subject: [PATCH 068/186] Bump ccxt from 3.0.69 to 3.0.75 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.69 to 3.0.75. - [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.69...3.0.75) --- 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 7db852961..10a27526a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.69 +ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.9 From a64b641fdff0bd298fc82d3c8f2c4e013e88f94b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:57:40 +0000 Subject: [PATCH 069/186] Bump ruff from 0.0.261 to 0.0.262 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.261 to 0.0.262. - [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.261...v0.0.262) --- 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 fc0efcfe7..cd4c96eea 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.261 +ruff==0.0.262 mypy==1.2.0 pre-commit==3.2.2 pytest==7.3.1 From 43d0c7ff98963d4519b495452f57dcac9b5dcdc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:59:22 +0000 Subject: [PATCH 070/186] Bump numpy from 1.24.2 to 1.24.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.24.2 to 1.24.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.24.2...v1.24.3) --- updated-dependencies: - dependency-name: numpy 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 7db852961..fae87d689 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.24.2 +numpy==1.24.3 pandas==1.5.3 pandas-ta==0.3.14b From 15fdaecd7f2b9fc95c75650376176c3be5069fd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:59:43 +0000 Subject: [PATCH 071/186] Bump psutil from 5.9.4 to 5.9.5 Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.4 to 5.9.5. - [Release notes](https://github.com/giampaolo/psutil/releases) - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.4...release-5.9.5) --- updated-dependencies: - dependency-name: psutil 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 7db852961..de9ad7434 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ pydantic==1.10.7 uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 -psutil==5.9.4 +psutil==5.9.5 # Support for colorized terminal output colorama==0.4.6 From 598478e48da09fa2795e75601f83d56803821810 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 04:00:11 +0000 Subject: [PATCH 072/186] Bump filelock from 3.11.0 to 3.12.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.11.0 to 3.12.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.11.0...3.12.0) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index c81c17f63..87b1fd3c8 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,4 +5,4 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.11.0 +filelock==3.12.0 From c513d1077fdf64d643ae7172e06350a50abdcb03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 05:32:38 +0000 Subject: [PATCH 073/186] Bump sqlalchemy from 2.0.9 to 2.0.10 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.9 to 2.0.10. - [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 49f51c315..1e3c13504 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.9 +SQLAlchemy==2.0.10 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From 0329c0c3f9bd1043eae628388c5dac2649df619b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 07:59:21 +0200 Subject: [PATCH 074/186] pre-commit - bump sqlalchemy --- .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 89370eacc..0031300cd 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.9 + - SQLAlchemy==2.0.10 # stages: [push] - repo: https://github.com/pycqa/isort From f7c6828e6a956a0813a498fda092ea7091d090f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:02:27 +0000 Subject: [PATCH 075/186] Bump mkdocs-material from 9.1.6 to 9.1.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.6 to 9.1.7. - [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.6...9.1.7) --- 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 550c3b54c..91b0e993b 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.6 +mkdocs-material==9.1.7 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 jinja2==3.1.2 From d1e9e7039607a1bff60a95e772244625707b30c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 09:41:36 +0200 Subject: [PATCH 076/186] Improve Resample-period test --- tests/optimize/test_optimize_reports.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 9fdd0d61e..6428177c5 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -9,7 +9,7 @@ import pytest from arrow import Arrow from freqtrade.configuration import TimeRange -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) @@ -470,6 +470,9 @@ def test__get_resample_from_period(): with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') + for period in BACKTEST_BREAKDOWNS: + assert isinstance(_get_resample_from_period(period), str) + def test_show_sorted_pairlist(testdatadir, default_conf, capsys): filename = testdatadir / "backtest_results/backtest-result.json" From 3948890c3b9b45bfd0571bf923be1774f97cbe32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 10:35:46 +0200 Subject: [PATCH 077/186] Add --breakdown to backtest-show --- freqtrade/commands/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 47aa37fdf..109516f87 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"] +ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] From e99af87b6dc164fa1f32ad395d5bc52d69f0ec55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 10:36:30 +0200 Subject: [PATCH 078/186] store periodic breakdown in backtest results This will enable the webserver to use this data. --- freqtrade/optimize/hyperopt.py | 3 ++- freqtrade/optimize/optimize_reports.py | 29 ++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ee5599e20..fe590f0d2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,8 @@ class Hyperopt: strat_stats = generate_strategy_stats( self.pairlist, self.backtesting.strategy.get_strategy_name(), - backtesting_results, min_date, max_date, market_change=self.market_change + backtesting_results, min_date, max_date, market_change=self.market_change, + is_hyperopt=True, ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index cbae6aede..1c5088cc1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -7,8 +7,8 @@ from typing import Any, Dict, List, Union from pandas import DataFrame, to_datetime from tabulate import tabulate -from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT, - Config, IntOrInf) +from freqtrade.constants import (BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, + UNLIMITED_STAKE_AMOUNT, Config, IntOrInf) from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) @@ -296,6 +296,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic stats.append( { 'date': name.strftime('%d/%m/%Y'), + 'date_ts': int(name.to_pydatetime().timestamp() * 1000), 'profit_abs': profit_abs, 'wins': wins, 'draws': draws, @@ -305,6 +306,13 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic return stats +def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: + result = {} + for period in BACKTEST_BREAKDOWNS: + result[period] = generate_periodic_breakdown_stats(trade_list, period) + return result + + def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: """ Generate overall trade statistics """ if len(results) == 0: @@ -381,7 +389,8 @@ def generate_strategy_stats(pairlist: List[str], strategy: str, content: Dict[str, Any], min_date: datetime, max_date: datetime, - market_change: float + market_change: float, + is_hyperopt: bool = False, ) -> Dict[str, Any]: """ :param pairlist: List of pairs to backtest @@ -416,6 +425,11 @@ def generate_strategy_stats(pairlist: List[str], daily_stats = generate_daily_stats(results) trade_stats = generate_trading_stats(results) + + periodic_breakdown = {} + if not is_hyperopt: + periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)} + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], @@ -434,7 +448,6 @@ def generate_strategy_stats(pairlist: List[str], 'results_per_enter_tag': enter_tag_results, 'exit_reason_summary': exit_reason_stats, 'left_open_trades': left_open_results, - # 'days_breakdown_stats': days_breakdown_stats, 'total_trades': len(results), 'trade_count_long': len(results.loc[~results['is_short']]), @@ -499,6 +512,7 @@ def generate_strategy_stats(pairlist: List[str], 'exit_profit_only': config['exit_profit_only'], 'exit_profit_offset': config['exit_profit_offset'], 'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'], + **periodic_breakdown, **daily_stats, **trade_stats } @@ -891,8 +905,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(table) for period in backtest_breakdown: - days_breakdown_stats = generate_periodic_breakdown_stats( - trade_list=results['trades'], period=period) + if period in results.get('periodic_breakdown', {}): + days_breakdown_stats = results['periodic_breakdown'][period] + else: + days_breakdown_stats = generate_periodic_breakdown_stats( + trade_list=results['trades'], period=period) table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats, stake_currency=stake_currency, period=period) if isinstance(table, str) and len(table) > 0: From 68a8c79c08fab763605c6ea2ee4acd8e093d7608 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 12:03:00 +0200 Subject: [PATCH 079/186] Improve output for futures --- freqtrade/rpc/rpc.py | 4 +++- freqtrade/rpc/telegram.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 222912aa0..35e08cbc0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -592,7 +592,7 @@ class RPC: if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: # in Futures, "total" includes the locked stake, and therefore all positions est_stake = balance.free - est_bot_stake = est_stake + est_bot_stake = amount else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) @@ -665,6 +665,7 @@ class RPC: position: PositionWallet for symbol, position in self._freqtrade.wallets.get_all_positions().items(): total += position.collateral + total_bot += position.collateral currencies.append({ 'currency': symbol, @@ -673,6 +674,7 @@ class RPC: 'used': 0, 'position': position.position, 'est_stake': position.collateral, + 'est_stake_bot': position.collateral, 'stake': stake_currency, 'leverage': position.leverage, 'side': position.side, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2b997efac..e99501cc0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -928,7 +928,10 @@ class Telegram(RPCHandler): total_dust_currencies = 0 for curr in result['currencies']: curr_output = '' - if curr['est_stake'] > balance_dust_level and (full_result or curr['is_bot_managed']): + if ( + (curr['is_position'] or curr['est_stake'] > balance_dust_level) + and (full_result or curr['is_bot_managed']) + ): if curr['is_position']: curr_output = ( f"*{curr['currency']}:*\n" From 829724c0ec579d7dab9ef1473ecf70e5c02392ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 12:13:24 +0200 Subject: [PATCH 080/186] Fallback to "initialMargin" if collateral is not set --- freqtrade/wallets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 8dcc92af4..6f86398f3 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.enums import RunMode, TradingMode from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange +from freqtrade.misc import safe_value_fallback from freqtrade.persistence import LocalTrade, Trade @@ -148,7 +149,7 @@ class Wallets: # Position is not open ... continue size = self._exchange._contracts_to_amount(symbol, position['contracts']) - collateral = position['collateral'] or 0.0 + collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0) leverage = position['leverage'] self._positions[symbol] = PositionWallet( symbol, position=size, From 8086d9053573b7c4aef6afdc1ab18e9085d1eac0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 12:12:45 +0200 Subject: [PATCH 081/186] Update some tests for balance updates --- tests/rpc/test_rpc.py | 9 ++++++--- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5335cebf9..bb84ff8e9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -548,7 +548,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'used': 2.0, 'bot_owned': 9.9, # available stake - reducing by reserved amount 'est_stake': 10.0, # In futures mode, "free" is used here. - 'est_stake_bot': 10, + 'est_stake_bot': 9.9, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, @@ -591,6 +591,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'balance': 0.0, 'currency': 'ETH/USDT:USDT', 'est_stake': 20, + 'est_stake_bot': 20, 'used': 0, 'stake': 'BTC', 'is_position': True, @@ -600,10 +601,12 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'is_bot_managed': True, } ] - assert pytest.approx(result['total_bot']) == 10 + assert pytest.approx(result['total_bot']) == 29.9 assert pytest.approx(result['total']) == 30.309096 assert result['starting_capital'] == 10 - assert result['starting_capital_ratio'] == 0.0 + # Very high starting capital ratio, because the futures position really has the wrong unit. + # TODO: improve this test (see comment above) + assert result['starting_capital_ratio'] == pytest.approx(1.98999999) def test_rpc_start(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a4638a07e..e045bf487 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -482,7 +482,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'used': 0.0, 'bot_owned': pytest.approx(11.879999), 'est_stake': 12.0, - 'est_stake_bot': 12.0, + 'est_stake_bot': pytest.approx(11.879999), 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, @@ -491,7 +491,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'is_bot_managed': True, } assert response['total'] == 12.159513094 - assert response['total_bot'] == 12.0 + assert response['total_bot'] == pytest.approx(11.879999) assert 'starting_capital' in response assert 'starting_capital_fiat' in response assert 'starting_capital_pct' in response diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 97ac03c9f..9b22b73c0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -799,7 +799,8 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12' in result + assert 'BTC: 11' in result + assert 'BTC: 12' in result_full assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result assert '*Estimated Value*:' in result_full From 98db27e8f49261bf257952fb39f851d7273c3dcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 14:05:35 +0200 Subject: [PATCH 082/186] Bump develop version to 2023.5-dev --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f8955b295..f8818c35c 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.4.dev' +__version__ = '2023.5.dev' if 'dev' in __version__: from pathlib import Path From 99a4a6405280f085d13d9edaf6a293a13f373497 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 08:23:55 +0000 Subject: [PATCH 083/186] Bump python-telegram-bot from 13.15 to 20.2 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.15 to 20.2. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.15...v20.2) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d51852fc..a07ef8897 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.10 -python-telegram-bot==13.15 +python-telegram-bot==20.2 arrow==1.2.3 cachetools==4.2.2 requests==2.28.2 From da261003df117f5d221b8f32ef9bb4a99dfe71ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 10:52:51 +0100 Subject: [PATCH 084/186] Fix telegram imports to match v20.0 --- freqtrade/rpc/telegram.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e99501cc0..9fb394b1e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -17,11 +17,12 @@ from typing import Any, Callable, Dict, List, Optional, Union import arrow from tabulate import tabulate -from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, - KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update) +from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, + ReplyKeyboardMarkup, Update) +from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater -from telegram.utils.helpers import escape_markdown +from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN, Config @@ -33,6 +34,9 @@ from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc.rpc_types import RPCSendMsg +MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH + + logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') From c37b7b77e42fdc574a6a5a9d8486a55b4bab0af4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 10:55:55 +0100 Subject: [PATCH 085/186] move telegram fixture to telegram file --- tests/conftest.py | 10 +--------- tests/rpc/test_rpc_telegram.py | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2f2345d54..1c737b3aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json import logging import re from copy import deepcopy -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path from typing import Optional from unittest.mock import MagicMock, Mock, PropertyMock @@ -12,7 +12,6 @@ import arrow import numpy as np import pandas as pd import pytest -from telegram import Chat, Message, Update from freqtrade import constants from freqtrade.commands import Arguments @@ -550,13 +549,6 @@ def get_default_conf_usdt(testdatadir): return configuration -@pytest.fixture -def update(): - _update = Update(0) - _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) - return _update - - @pytest.fixture def fee(): return MagicMock(return_value=0.0025) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 9b22b73c0..0da24dc4a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -43,6 +43,14 @@ def default_conf(default_conf) -> dict: return default_conf +@pytest.fixture +def update(): + message = Message(0, datetime.utcnow(), Chat(0, 0)) + _update = Update(0, message=message) + + return _update + + class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator From 57eed50acb5fde4343705176d881c3bff9bff18f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 11:05:12 +0100 Subject: [PATCH 086/186] Fix some test failures caused by v20 update --- tests/rpc/test_rpc_telegram.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0da24dc4a..e70924a1d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -159,8 +159,8 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), datetime.utcnow(), chat) + message = Message(randint(1, 100), datetime.utcnow(), chat) + update = Update(randint(1, 100), message=message) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -193,9 +193,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None def test_telegram_status(default_conf, update, mocker) -> None: - update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" status_table = MagicMock() mocker.patch('freqtrade.rpc.telegram.Telegram._status_table', status_table) @@ -252,9 +250,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: @pytest.mark.usefixtures("init_persistence") def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -305,9 +301,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: @pytest.mark.usefixtures("init_persistence") def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -1277,7 +1271,6 @@ def test_force_enter_handle_exception(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - update.message.text = '/forcebuy ETH/Nonepair' telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 From 68ac934929a48033e58c9ffaa36a7394ea8d6b8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 11:29:29 +0100 Subject: [PATCH 087/186] Update command list to handle frozenSets --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9fb394b1e..25a75c433 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -235,7 +235,7 @@ class Telegram(RPCHandler): ) logger.info( 'rpc.telegram is listening for following commands: %s', - [h.command for h in handles] + [[x for x in sorted(h.commands)] for h in handles] ) def cleanup(self) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e70924a1d..7510255e9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -115,14 +115,14 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], " - "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " - "['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], " - "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " + "['forceexit', 'forcesell', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " + "['trades'], ['delete'], ['cancel_open_order', 'coo'], ['performance'], " + "['buys', 'entries'], ['exits', 'sells'], ['mix_tags'], " "['stats'], ['daily'], ['weekly'], ['monthly'], " - "['count'], ['locks'], ['unlock', 'delete_locks'], " - "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['count'], ['locks'], ['delete_locks', 'unlock'], " + "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " - "['blacklist_delete', 'bl_delete'], " + "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']" "]") From 14b501a4f74a38d07e8fb88e677b6b55e4fce37a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 13:49:25 +0100 Subject: [PATCH 088/186] Initial changes for telegram migration --- freqtrade/rpc/telegram.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 25a75c433..5a070bb2e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -21,7 +21,7 @@ from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Update) from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError -from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater +from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -104,6 +104,7 @@ class Telegram(RPCHandler): super().__init__(rpc, config) self._updater: Updater + self._app: Application self._init_keyboard() self._init() @@ -162,8 +163,9 @@ class Telegram(RPCHandler): registers all known command handlers and starts polling for message updates """ - self._updater = Updater(token=self._config['telegram']['token'], workers=0, - use_context=True) + self._app = Application.builder().token(self._config['telegram']['token']).build + # self._updater = Updater(token=, workers=0, + # use_context=True) # Register command handler and start telegram message polling handles = [ @@ -222,12 +224,12 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), ] for handle in handles: - self._updater.dispatcher.add_handler(handle) + self._app.add_handler(handle) for callback in callbacks: - self._updater.dispatcher.add_handler(callback) + self._app.add_handler(callback) - self._updater.start_polling( + self._app.run_polling( bootstrap_retries=-1, timeout=20, read_latency=60, # Assumed transmission latency @@ -244,7 +246,7 @@ class Telegram(RPCHandler): :return: None """ # This can take up to `timeout` from the call to `start_polling`. - self._updater.stop() + self._app.stop() def _exchange_from_msg(self, msg: Dict[str, Any]) -> str: """ From cb45689c1da939f7b28ea67edf41be2c28fb6e7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Apr 2023 18:03:35 +0200 Subject: [PATCH 089/186] Small fixes to new telegram implementation --- freqtrade/rpc/telegram.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5a070bb2e..d5edd0871 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -163,7 +163,7 @@ class Telegram(RPCHandler): registers all known command handlers and starts polling for message updates """ - self._app = Application.builder().token(self._config['telegram']['token']).build + self._app = Application.builder().token(self._config['telegram']['token']).build() # self._updater = Updater(token=, workers=0, # use_context=True) @@ -229,16 +229,16 @@ class Telegram(RPCHandler): for callback in callbacks: self._app.add_handler(callback) - self._app.run_polling( - bootstrap_retries=-1, - timeout=20, - read_latency=60, # Assumed transmission latency - drop_pending_updates=True, - ) logger.info( 'rpc.telegram is listening for following commands: %s', [[x for x in sorted(h.commands)] for h in handles] ) + self._app.run_polling( + bootstrap_retries=-1, + timeout=20, + # read_latency=60, # Assumed transmission latency + drop_pending_updates=True, + ) def cleanup(self) -> None: """ From 5134bf8ec382240480214eea6b29c12a1f8c612c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 09:46:51 +0200 Subject: [PATCH 090/186] Authorized-only and /version to async --- freqtrade/rpc/telegram.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d5edd0871..99e6a9ba7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -58,7 +58,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: :return: decorated function """ - def wrapper(self, *args, **kwargs): + async def wrapper(self, *args, **kwargs): """ Decorator logic """ update = kwargs.get('update') or args[0] @@ -80,9 +80,9 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: chat_id ) try: - return command_handler(self, *args, **kwargs) + return await command_handler(self, *args, **kwargs) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) except BaseException: logger.exception('Exception occurred within Telegram module') finally: @@ -1589,7 +1589,7 @@ class Telegram(RPCHandler): self._send_msg(message) @authorized_only - def _version(self, update: Update, context: CallbackContext) -> None: + async def _version(self, update: Update, context: CallbackContext) -> None: """ Handler for /version. Show version information @@ -1602,7 +1602,7 @@ class Telegram(RPCHandler): if strategy_version is not None: version_string += f', *Strategy version: * `{strategy_version}`' - self._send_msg(version_string) + await self._send_msg(version_string) @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: @@ -1680,12 +1680,12 @@ class Telegram(RPCHandler): except TelegramError as telegram_err: logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) - def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False, - keyboard: Optional[List[List[InlineKeyboardButton]]] = None, - callback_path: str = "", - reload_able: bool = False, - query: Optional[CallbackQuery] = None) -> None: + async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, + disable_notification: bool = False, + keyboard: Optional[List[List[InlineKeyboardButton]]] = None, + callback_path: str = "", + reload_able: bool = False, + query: Optional[CallbackQuery] = None) -> None: """ Send given markdown message :param msg: message @@ -1708,7 +1708,7 @@ class Telegram(RPCHandler): reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, @@ -1722,7 +1722,7 @@ class Telegram(RPCHandler): 'Telegram NetworkError: %s! Trying one more time.', network_err.message ) - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, From e7e6f719e4a769cef61e47b64dbdb8acbfce082f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 09:52:14 +0200 Subject: [PATCH 091/186] _update_msg to async --- freqtrade/rpc/telegram.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 99e6a9ba7..f85fd5e08 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -103,7 +103,6 @@ class Telegram(RPCHandler): """ super().__init__(rpc, config) - self._updater: Updater self._app: Application self._init_keyboard() self._init() @@ -164,8 +163,6 @@ class Telegram(RPCHandler): and starts polling for message updates """ self._app = Application.builder().token(self._config['telegram']['token']).build() - # self._updater = Updater(token=, workers=0, - # use_context=True) # Register command handler and start telegram message polling handles = [ @@ -1650,8 +1647,8 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", - reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", + reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: if reload_able: reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)], @@ -1665,7 +1662,7 @@ class Telegram(RPCHandler): message_id = query.message.message_id try: - self._updater.bot.edit_message_text( + await self._app.bot.edit_message_text( chat_id=chat_id, message_id=message_id, text=msg, @@ -1695,8 +1692,8 @@ class Telegram(RPCHandler): """ reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] if query: - self._update_msg(query=query, msg=msg, parse_mode=parse_mode, - callback_path=callback_path, reload_able=reload_able) + await self._update_msg(query=query, msg=msg, parse_mode=parse_mode, + callback_path=callback_path, reload_able=reload_able) return if reload_able and self._config['telegram'].get('reload', True): reply_markup = InlineKeyboardMarkup([ From 54732b72fd99dcf0174db1ff0292b018ae7ababb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:10:14 +0200 Subject: [PATCH 092/186] Manage startup/teardown of telegram manually --- freqtrade/rpc/telegram.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f85fd5e08..8202a03c2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +import asyncio import json import logging import re @@ -13,6 +14,7 @@ from functools import partial from html import escape from itertools import chain from math import isnan +from threading import Thread from typing import Any, Callable, Dict, List, Optional, Union import arrow @@ -104,8 +106,16 @@ class Telegram(RPCHandler): super().__init__(rpc, config) self._app: Application + self._loop: asyncio.AbstractEventLoop self._init_keyboard() - self._init() + self._start_thread() + + def _start_thread(self): + """ + Creates and starts the polling thread + """ + self._thread = Thread(target=self._init, name='FTTelegram') + self._thread.start() def _init_keyboard(self) -> None: """ @@ -161,7 +171,14 @@ class Telegram(RPCHandler): Initializes this module with the given config, registers all known command handlers and starts polling for message updates + Runs in a separate thread. """ + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self._app = Application.builder().token(self._config['telegram']['token']).build() # Register command handler and start telegram message polling @@ -230,12 +247,27 @@ class Telegram(RPCHandler): 'rpc.telegram is listening for following commands: %s', [[x for x in sorted(h.commands)] for h in handles] ) - self._app.run_polling( + self._loop.run_until_complete(self._startup_telegram()) + + async def _startup_telegram(self) -> None: + await self._app.initialize() + await self._app.start() + await self._app.updater.start_polling( bootstrap_retries=-1, timeout=20, # read_latency=60, # Assumed transmission latency drop_pending_updates=True, + # stop_signals=[], # Necessary as we don't run on the main thread ) + while True: + await asyncio.sleep(10) + if not self._app.updater.running: + break + + async def _cleanup_telegram(self) -> None: + await self._app.updater.stop() + await self._app.stop() + await self._app.shutdown() def cleanup(self) -> None: """ @@ -243,7 +275,8 @@ class Telegram(RPCHandler): :return: None """ # This can take up to `timeout` from the call to `start_polling`. - self._app.stop() + asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop) + self._thread.join() def _exchange_from_msg(self, msg: Dict[str, Any]) -> str: """ From 3d0e1d142f4cd03e9305b14f0d67f2d06f9e68b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:24:12 +0200 Subject: [PATCH 093/186] Convert endpoints to async --- freqtrade/rpc/telegram.py | 241 +++++++++++++++++++------------------- 1 file changed, 121 insertions(+), 120 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8202a03c2..c61e36116 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -23,7 +23,7 @@ from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Update) from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError -from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler, Updater +from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -489,7 +489,9 @@ class Telegram(RPCHandler): message = self.compose_message(deepcopy(msg), msg_type) # type: ignore if message: - self._send_msg(message, disable_notification=(noti == 'silent')) + asyncio.run_coroutine_threadsafe( + self._send_msg(message, disable_notification=(noti == 'silent')), + self._loop) def _get_sell_emoji(self, msg): """ @@ -572,7 +574,7 @@ class Telegram(RPCHandler): return lines_detail @authorized_only - def _status(self, update: Update, context: CallbackContext) -> None: + async def _status(self, update: Update, context: CallbackContext) -> None: """ Handler for /status. Returns the current TradeThread status @@ -582,12 +584,12 @@ class Telegram(RPCHandler): """ if context.args and 'table' in context.args: - self._status_table(update, context) + await self._status_table(update, context) return else: - self._status_msg(update, context) + await self._status_msg(update, context) - def _status_msg(self, update: Update, context: CallbackContext) -> None: + async def _status_msg(self, update: Update, context: CallbackContext) -> None: """ handler for `/status` and `/status `. @@ -671,9 +673,9 @@ class Telegram(RPCHandler): lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") - self.__send_status_msg(lines, r) + await self.__send_status_msg(lines, r) - def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: + async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: """ Send status message. """ @@ -684,13 +686,13 @@ class Telegram(RPCHandler): if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: msg += line + '\n' else: - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) @authorized_only - def _status_table(self, update: Update, context: CallbackContext) -> None: + async def _status_table(self, update: Update, context: CallbackContext) -> None: """ Handler for /status table. Returns the current TradeThread status in table format @@ -723,12 +725,11 @@ class Telegram(RPCHandler): # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_status_table", - query=update.callback_query) + await self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_status_table", + query=update.callback_query) - @authorized_only - def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: + async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -775,11 +776,11 @@ class Telegram(RPCHandler): f'{val.message} Profit over the last {timescale} {val.message2}:\n' f'
{stats_tab}
' ) - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path=val.callback, query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path=val.callback, query=update.callback_query) @authorized_only - def _daily(self, update: Update, context: CallbackContext) -> None: + async def _daily(self, update: Update, context: CallbackContext) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -787,10 +788,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'days') + await self._timeunit_stats(update, context, 'days') @authorized_only - def _weekly(self, update: Update, context: CallbackContext) -> None: + async def _weekly(self, update: Update, context: CallbackContext) -> None: """ Handler for /weekly Returns a weekly profit (in BTC) over the last n weeks. @@ -798,10 +799,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'weeks') + await self._timeunit_stats(update, context, 'weeks') @authorized_only - def _monthly(self, update: Update, context: CallbackContext) -> None: + async def _monthly(self, update: Update, context: CallbackContext) -> None: """ Handler for /monthly Returns a monthly profit (in BTC) over the last n months. @@ -809,10 +810,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'months') + await self._timeunit_stats(update, context, 'months') @authorized_only - def _profit(self, update: Update, context: CallbackContext) -> None: + async def _profit(self, update: Update, context: CallbackContext) -> None: """ Handler for /profit. Returns a cumulative profit statistics. @@ -886,11 +887,11 @@ class Telegram(RPCHandler): f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`" ) - self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", - query=update.callback_query) + await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", + query=update.callback_query) @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: + async def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats Show stats of recent trades @@ -921,7 +922,7 @@ class Telegram(RPCHandler): headers=['Exit Reason', 'Exits', 'Wins', 'Losses'] ) if len(exit_reasons_tabulate) > 25: - self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) + await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) exit_reasons_msg = '' durations = stats['durations'] @@ -936,10 +937,10 @@ class Telegram(RPCHandler): ) msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""") - self._send_msg(msg, ParseMode.MARKDOWN) + await self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only - def _balance(self, update: Update, context: CallbackContext) -> None: + async def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ full_result = context.args and 'full' in context.args result = self._rpc._rpc_balance(self._config['stake_currency'], @@ -1017,11 +1018,11 @@ class Telegram(RPCHandler): f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" f"\t`{result['symbol']}: {value}`{fiat_val}\n" ) - self._send_msg(output, reload_able=True, callback_path="update_balance", - query=update.callback_query) + await self._send_msg(output, reload_able=True, callback_path="update_balance", + query=update.callback_query) @authorized_only - def _start(self, update: Update, context: CallbackContext) -> None: + async def _start(self, update: Update, context: CallbackContext) -> None: """ Handler for /start. Starts TradeThread @@ -1030,10 +1031,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_start() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stop(self, update: Update, context: CallbackContext) -> None: + async def _stop(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop. Stops TradeThread @@ -1042,10 +1043,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stop() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _reload_config(self, update: Update, context: CallbackContext) -> None: + async def _reload_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /reload_config. Triggers a config file reload @@ -1054,10 +1055,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_reload_config() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stopentry(self, update: Update, context: CallbackContext) -> None: + async def _stopentry(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy. Sets max_open_trades to 0 and gracefully sells all open trades @@ -1066,10 +1067,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stopentry() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _force_exit(self, update: Update, context: CallbackContext) -> None: + async def _force_exit(self, update: Update, context: CallbackContext) -> None: """ Handler for /forceexit . Sells the given trade at current price @@ -1100,51 +1101,51 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton( text='Cancel', callback_data='force_exit__cancel')]) - self._send_msg(msg="Which trade?", keyboard=buttons_aligned) + await self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - def _force_exit_action(self, trade_id): + async def _force_exit_action(self, trade_id): if trade_id != 'cancel': try: self._rpc._rpc_force_exit(trade_id) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) - def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '__' in query.data: # Input data is "force_exit__" trade_id = query.data.split("__")[1].split(' ')[0] if trade_id == 'cancel': - query.answer() - query.edit_message_text(text="Force exit canceled.") + await query.answer() + await query.edit_message_text(text="Force exit canceled.") return trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() query.answer() if trade: - query.edit_message_text( + await query.edit_message_text( text=f"Manually exiting Trade #{trade_id}, {trade.pair}") - self._force_exit_action(trade_id) + await self._force_exit_action(trade_id) else: - query.edit_message_text(text=f"Trade {trade_id} not found.") + await query.edit_message_text(text=f"Trade {trade_id} not found.") - def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): + async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': try: - self._rpc._rpc_force_entry(pair, price, order_side=order_side) + await self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: logger.exception("Forcebuy error!") - self._send_msg(str(e), ParseMode.HTML) + await self._send_msg(str(e), ParseMode.HTML) - def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '_||_' in query.data: pair, side = query.data.split('_||_') order_side = SignalDirection(side) - query.answer() - query.edit_message_text(text=f"Manually entering {order_side} for {pair}") - self._force_enter_action(pair, None, order_side) + await query.answer() + await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + await self._force_enter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard( @@ -1157,7 +1158,7 @@ class Telegram(RPCHandler): return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only - def _force_enter( + async def _force_enter( self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: """ Handler for /forcelong and `/forceshort @@ -1169,7 +1170,7 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - self._force_enter_action(pair, price, order_side) + await self._force_enter_action(pair, price, order_side) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] pair_buttons = [ @@ -1179,12 +1180,12 @@ class Telegram(RPCHandler): buttons_aligned = self._layout_inline_keyboard(pair_buttons) buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) - self._send_msg(msg="Which pair?", - keyboard=buttons_aligned, - query=update.callback_query) + await self._send_msg(msg="Which pair?", + keyboard=buttons_aligned, + query=update.callback_query) @authorized_only - def _trades(self, update: Update, context: CallbackContext) -> None: + async def _trades(self, update: Update, context: CallbackContext) -> None: """ Handler for /trades Returns last n recent trades. @@ -1213,10 +1214,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_trade(self, update: Update, context: CallbackContext) -> None: + async def _delete_trade(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete . Delete the given trade @@ -1228,13 +1229,13 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) msg = self._rpc._rpc_delete(trade_id) - self._send_msg( + await self._send_msg( f"`{msg['result_msg']}`\n" 'Please make sure to take care of this asset on the exchange manually.' ) @authorized_only - def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: + async def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: """ Handler for /cancel_open_order . Cancel open order for tradeid @@ -1246,10 +1247,10 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) self._rpc._rpc_cancel_open_order(trade_id) - self._send_msg('Open order canceled.') + await self._send_msg('Open order canceled.') @authorized_only - def _performance(self, update: Update, context: CallbackContext) -> None: + async def _performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /performance. Shows a performance statistic from finished trades @@ -1272,12 +1273,12 @@ class Telegram(RPCHandler): else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_performance", + query=update.callback_query) @authorized_only - def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /buys PAIR . Shows a performance statistic from finished trades @@ -1304,12 +1305,12 @@ class Telegram(RPCHandler): else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_enter_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_enter_tag_performance", + query=update.callback_query) @authorized_only - def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: + async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -1331,17 +1332,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_exit_reason_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_exit_reason_performance", + query=update.callback_query) @authorized_only - def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /mix_tags. Shows a performance statistic from finished trades @@ -1363,17 +1364,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_mix_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_mix_tag_performance", + query=update.callback_query) @authorized_only - def _count(self, update: Update, context: CallbackContext) -> None: + async def _count(self, update: Update, context: CallbackContext) -> None: """ Handler for /count. Returns the number of trades running @@ -1387,19 +1388,19 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{message}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_count", - query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_count", + query=update.callback_query) @authorized_only - def _locks(self, update: Update, context: CallbackContext) -> None: + async def _locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /locks. Returns the currently active locks """ rpc_locks = self._rpc._rpc_locks() if not rpc_locks['locks']: - self._send_msg('No active locks.', parse_mode=ParseMode.HTML) + await self._send_msg('No active locks.', parse_mode=ParseMode.HTML) for locks in chunks(rpc_locks['locks'], 25): message = tabulate([[ @@ -1411,10 +1412,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{escape(message)}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_locks(self, update: Update, context: CallbackContext) -> None: + async def _delete_locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete_locks. Returns the currently active locks @@ -1429,10 +1430,10 @@ class Telegram(RPCHandler): pair = arg self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) - self._locks(update, context) + await self._locks(update, context) @authorized_only - def _whitelist(self, update: Update, context: CallbackContext) -> None: + async def _whitelist(self, update: Update, context: CallbackContext) -> None: """ Handler for /whitelist Shows the currently active whitelist @@ -1449,39 +1450,39 @@ class Telegram(RPCHandler): message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist(self, update: Update, context: CallbackContext) -> None: + async def _blacklist(self, update: Update, context: CallbackContext) -> None: """ Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + await self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - def send_blacklist_msg(self, blacklist: Dict): + async def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: - self._send_msg('\n'.join(errmsgs)) + await self._send_msg('\n'.join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: + async def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: """ Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) + await self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only - def _logs(self, update: Update, context: CallbackContext) -> None: + async def _logs(self, update: Update, context: CallbackContext) -> None: """ Handler for /logs Shows the latest logs @@ -1500,17 +1501,17 @@ class Telegram(RPCHandler): escape_markdown(logrec[4], version=2)) if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: # Send message immediately if it would become too long - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' else: # Append message to messages to send msgs += msg + '\n' if msgs: - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) @authorized_only - def _edge(self, update: Update, context: CallbackContext) -> None: + async def _edge(self, update: Update, context: CallbackContext) -> None: """ Handler for /edge Shows information related to Edge @@ -1518,17 +1519,17 @@ class Telegram(RPCHandler): edge_pairs = self._rpc._rpc_edge() if not edge_pairs: message = 'Edge only validated following pairs:' - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) for chunk in chunks(edge_pairs, 25): edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') message = (f'Edge only validated following pairs:\n' f'
{edge_pairs_tab}
') - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _help(self, update: Update, context: CallbackContext) -> None: + async def _help(self, update: Update, context: CallbackContext) -> None: """ Handler for /help. Show commands of the bot @@ -1606,17 +1607,17 @@ class Telegram(RPCHandler): "*/version:* `Show version`" ) - self._send_msg(message, parse_mode=ParseMode.MARKDOWN) + await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @authorized_only - def _health(self, update: Update, context: CallbackContext) -> None: + async def _health(self, update: Update, context: CallbackContext) -> None: """ Handler for /health Shows the last process timestamp """ health = self._rpc.health() message = f"Last process: `{health['last_process_loc']}`" - self._send_msg(message) + await self._send_msg(message) @authorized_only async def _version(self, update: Update, context: CallbackContext) -> None: @@ -1635,7 +1636,7 @@ class Telegram(RPCHandler): await self._send_msg(version_string) @authorized_only - def _show_config(self, update: Update, context: CallbackContext) -> None: + async def _show_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /show_config. Show config information information @@ -1664,7 +1665,7 @@ class Telegram(RPCHandler): else: pa_info = "*Position adjustment:* Off\n" - self._send_msg( + await self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Market: * `{val['trading_mode']}`\n" @@ -1766,7 +1767,7 @@ class Telegram(RPCHandler): ) @authorized_only - def _changemarketdir(self, update: Update, context: CallbackContext) -> None: + async def _changemarketdir(self, update: Update, context: CallbackContext) -> None: """ Handler for /marketdir. Updates the bot's market_direction @@ -1789,14 +1790,14 @@ class Telegram(RPCHandler): if new_market_dir is not None: self._rpc._update_market_direction(new_market_dir) - self._send_msg("Successfully updated market direction" + await self._send_msg("Successfully updated market direction" f" from *{old_market_dir}* to *{new_market_dir}*.") else: raise RPCException("Invalid market direction provided. \n" "Valid market directions: *long, short, even, none*") elif context.args is not None and len(context.args) == 0: old_market_dir = self._rpc._get_market_direction() - self._send_msg(f"Currently set market direction: *{old_market_dir}*") + await self._send_msg(f"Currently set market direction: *{old_market_dir}*") else: raise RPCException("Invalid usage of command /marketdir. \n" "Usage: */marketdir [short | long | even | none]*") From b1367ac46fbaddccd7d45cd0f4389f4617431146 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:29:42 +0200 Subject: [PATCH 094/186] Update decorator typehint --- freqtrade/rpc/telegram.py | 42 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c61e36116..7f3c79194 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -15,7 +15,7 @@ from html import escape from itertools import chain from math import isnan from threading import Thread -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Coroutine, Dict, List, Optional, Union import arrow from tabulate import tabulate @@ -53,7 +53,7 @@ class TimeunitMappings: default: int -def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: +def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): """ Decorator to check if the message comes from the correct chat_id :param command_handler: Telegram CommandHandler @@ -252,20 +252,22 @@ class Telegram(RPCHandler): async def _startup_telegram(self) -> None: await self._app.initialize() await self._app.start() - await self._app.updater.start_polling( - bootstrap_retries=-1, - timeout=20, - # read_latency=60, # Assumed transmission latency - drop_pending_updates=True, - # stop_signals=[], # Necessary as we don't run on the main thread - ) - while True: - await asyncio.sleep(10) - if not self._app.updater.running: - break + if self._app.updater: + await self._app.updater.start_polling( + bootstrap_retries=-1, + timeout=20, + # read_latency=60, # Assumed transmission latency + drop_pending_updates=True, + # stop_signals=[], # Necessary as we don't run on the main thread + ) + while True: + await asyncio.sleep(10) + if not self._app.updater.running: + break async def _cleanup_telegram(self) -> None: - await self._app.updater.stop() + if self._app.updater: + await self._app.updater.stop() await self._app.stop() await self._app.shutdown() @@ -994,7 +996,7 @@ class Telegram(RPCHandler): # Handle overflowing message length if len(output + curr_output) >= MAX_MESSAGE_LENGTH: - self._send_msg(output) + await self._send_msg(output) output = curr_output else: output += curr_output @@ -1088,7 +1090,7 @@ class Telegram(RPCHandler): statlist, _, _ = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) except RPCException: - self._send_msg(msg='No open trade found.') + await self._send_msg(msg='No open trade found.') return trades = [] for trade in statlist: @@ -1121,7 +1123,7 @@ class Telegram(RPCHandler): await query.edit_message_text(text="Force exit canceled.") return trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() - query.answer() + await query.answer() if trade: await query.edit_message_text( text=f"Manually exiting Trade #{trade_id}, {trade.pair}") @@ -1132,7 +1134,7 @@ class Telegram(RPCHandler): async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': try: - await self._rpc._rpc_force_entry(pair, price, order_side=order_side) + self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: logger.exception("Forcebuy error!") await self._send_msg(str(e), ParseMode.HTML) @@ -1268,7 +1270,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line @@ -1300,7 +1302,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line From 914d7350faca31f597fdb189d31f0336eb590ee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:41:00 +0200 Subject: [PATCH 095/186] Update mocks in apimanager tests --- tests/rpc/test_rpc_apiserver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e045bf487..8123e4689 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -283,7 +283,7 @@ def test_api__init__(default_conf, mocker): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock()) apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) @@ -341,7 +341,7 @@ def test_api_run(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_inst_mock = MagicMock() server_inst_mock.run_in_thread = MagicMock() @@ -419,7 +419,7 @@ def test_api_cleanup(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_mock = MagicMock() server_mock.cleanup = MagicMock() @@ -1877,7 +1877,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog): "password": _TEST_PASS, "ws_token": _TEST_WS_TOKEN }}) - mocker.patch('freqtrade.rpc.telegram.Updater') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api') apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) From fb56889b43f50bc7bb1e8ad55afb4546f935c020 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 14:15:38 +0200 Subject: [PATCH 096/186] Update a few tests ... --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 201 ++++++++++++++++----------------- 2 files changed, 101 insertions(+), 102 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7f3c79194..192e2b6d7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -177,7 +177,7 @@ class Telegram(RPCHandler): self._loop = asyncio.get_running_loop() except RuntimeError: self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) + asyncio.set_event_loop(self._loop) self._app = Application.builder().token(self._config['telegram']['token']).build() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7510255e9..5b7950e4c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone from functools import reduce from random import choice, randint from string import ascii_uppercase -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, AsyncMock, MagicMock import arrow import pytest @@ -79,12 +79,13 @@ class DummyCls(Telegram): def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): - msg_mock = MagicMock() + msg_mock = AsyncMock() if mock: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _send_msg=msg_mock + _send_msg=msg_mock, + _start_thread=MagicMock(), ) if not ftbot: ftbot = get_patched_freqtradebot(mocker, default_conf) @@ -95,7 +96,6 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): def test_telegram__init__(default_conf, mocker) -> None: - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram, _, _ = get_telegram_testobject(mocker, default_conf) @@ -192,7 +192,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker) -> None: +async def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['enabled'] = False status_table = MagicMock() @@ -238,18 +238,18 @@ def test_telegram_status(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 context = MagicMock() # /status table context.args = ["table"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert status_table.call_count == 1 @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: +async def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: default_conf['telegram']['enabled'] = False default_conf['position_adjustment_enable'] = True mocker.patch.multiple( @@ -288,7 +288,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: trade.recalc_trade_from_orders() Trade.commit() - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 4 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) @@ -300,8 +300,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: - default_conf['telegram']['enabled'] = False +async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -315,7 +314,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None trade = Trade.get_trades([Trade.is_open.is_(False)]).first() context = MagicMock() context.args = [str(trade.id)] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 1 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Close Date:', msg) @@ -749,7 +748,7 @@ def test_telegram_profit_handle( @pytest.mark.parametrize('is_short', [True, False]) -def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: +async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( EXMS, @@ -759,7 +758,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -767,7 +766,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> # Create some test data create_mock_trades(fee, is_short=is_short) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Exit Reason' in msg_mock.call_args_list[-1][0][0] assert 'ROI' in msg_mock.call_args_list[-1][0][0] @@ -775,7 +774,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> msg_mock.reset_mock() -def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: +async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) mocker.patch(f'{EXMS}.get_tickers', tickers) @@ -784,10 +783,10 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) context = MagicMock() context.args = ["full"] - telegram._balance(update=update, context=context) + await telegram._balance(update=update, context=context) result = msg_mock.call_args_list[0][0][0] result_full = msg_mock.call_args_list[1][0][0] assert msg_mock.call_count == 2 @@ -809,7 +808,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*Estimated Value (Bot managed assets only)*:' in result -def test_balance_handle_empty_response(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response(default_conf, update, mocker) -> None: default_conf['dry_run'] = False mocker.patch(f'{EXMS}.get_balances', return_value={}) @@ -817,26 +816,26 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.config['dry_run'] = False - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert 'Starting capital: `0 BTC' in result -def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert "*Warning:* Simulated balances in Dry Mode." in result assert "Starting capital: `1000 BTC`" in result -def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: +async def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: balances = [] for i in range(100): curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) @@ -869,7 +868,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 # Test if wrap happens around 4000 - # and each single currency-output is around 120 characters long so we need @@ -878,78 +877,78 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) -def test_start_handle(default_conf, update, mocker) -> None: +async def test_start_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 -def test_start_handle_already_running(default_conf, update, mocker) -> None: +async def test_start_handle_already_running(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] -def test_stop_handle(default_conf, update, mocker) -> None: +async def test_stop_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'stopping trader' in msg_mock.call_args_list[0][0][0] -def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: +async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] -def test_stopbuy_handle(default_conf, update, mocker) -> None: +async def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 - telegram._stopentry(update=update, context=MagicMock()) + await telegram._stopentry(update=update, context=MagicMock()) assert freqtradebot.config['max_open_trades'] == 0 assert msg_mock.call_count == 1 assert 'No more entries will occur from now. Run /reload_config to reset.' \ in msg_mock.call_args_list[0][0][0] -def test_reload_config_handle(default_conf, update, mocker) -> None: +async def test_reload_config_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._reload_config(update=update, context=MagicMock()) + await telegram._reload_config(update=update, context=MagicMock()) assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 assert 'Reloading config' in msg_mock.call_args_list[0][0][0] -def test_telegram_forceexit_handle(default_conf, update, ticker, fee, +async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -980,7 +979,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-2][0][0] @@ -1016,8 +1015,8 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, } == last_msg -def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, mocker) -> None: +async def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -1052,7 +1051,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 @@ -1089,7 +1088,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, } == last_msg -def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1115,7 +1114,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None # /forceexit all context = MagicMock() context.args = ["all"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Called for each trade 2 times assert msg_mock.call_count == 8 @@ -1152,7 +1151,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None } == msg -def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: +async def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1164,7 +1163,7 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -1174,12 +1173,12 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 123456 context = MagicMock() context.args = ["123456"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: +async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( EXMS, @@ -1195,7 +1194,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # /forceexit context = MagicMock() context.args = [] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # No pair assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.' @@ -1204,7 +1203,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # /forceexit - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) keyboard = msg_mock.call_args_list[0][1]['keyboard'] # 4 pairs + cancel assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 @@ -1214,7 +1213,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = keyboard[1][0].callback_data - telegram._force_exit_inline(update, None) + await telegram._force_exit_inline(update, None) assert update.callback_query.answer.call_count == 1 assert update.callback_query.edit_message_text.call_count == 1 assert femock.call_count == 1 @@ -1222,17 +1221,17 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # Retry exiting - but cancel instead update.callback_query.reset_mock() - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Use cancel button update.callback_query.data = keyboard[-1][0].callback_data - telegram._force_exit_inline(update, None) + await telegram._force_exit_inline(update, None) query = update.callback_query assert query.answer.call_count == 1 assert query.edit_message_text.call_count == 1 assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force exit canceled." -def test_force_enter_handle(default_conf, update, mocker) -> None: +async def test_force_enter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1244,7 +1243,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1257,7 +1256,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1265,19 +1264,19 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_force_enter_handle_exception(default_conf, update, mocker) -> None: +async def test_force_enter_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 assert msg_mock.call_args_list[0][0][0] == 'Force_entry not enabled.' -def test_force_enter_no_pair(default_conf, update, mocker) -> None: +async def test_force_enter_no_pair(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1289,7 +1288,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 0 assert msg_mock.call_count == 1 @@ -1301,11 +1300,11 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = 'XRP/USDT_||_long' - telegram._force_enter_inline(update, None) + await telegram._force_enter_inline(update, None) assert fbuy_mock.call_count == 1 -def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +async def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, @@ -1317,13 +1316,13 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc # Create some test data create_mock_trades_usdt(fee) - telegram._performance(update=update, context=MagicMock()) + await telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'XRP/USDT\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] -def test_telegram_entry_tag_performance_handle( +async def test_telegram_entry_tag_performance_handle( default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, @@ -1336,26 +1335,26 @@ def test_telegram_entry_tag_performance_handle( create_mock_trades_usdt(fee) context = MagicMock() - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0] assert 'TEST1\t3.987 USDT (5.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_enter_tag_performance', side_effect=RPCException('Error')) - telegram._enter_tag_performance(update=update, context=MagicMock()) + await telegram._enter_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_exit_reason_performance_handle( + default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1367,26 +1366,26 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick create_mock_trades_usdt(fee) context = MagicMock() - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] assert 'roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_exit_reason_performance', side_effect=RPCException('Error')) - telegram._exit_reason_performance(update=update, context=MagicMock()) + await telegram._exit_reason_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, + mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1399,26 +1398,26 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, create_mock_trades_usdt(fee) context = MagicMock() - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] assert ('TEST3 roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0]) context.args = ['XRP/USDT'] - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance', side_effect=RPCException('Error')) - telegram._mix_tag_performance(update=update, context=MagicMock()) + await telegram._mix_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1428,7 +1427,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.state = State.STOPPED - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -1437,7 +1436,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() msg_mock.reset_mock() - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( @@ -1447,7 +1446,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg in msg_mock.call_args_list[0][0][0] -def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1455,7 +1454,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No active locks.' in msg_mock.call_args_list[0][0][0] @@ -1464,7 +1463,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert 'Pair' in msg_mock.call_args_list[0][0][0] assert 'Until' in msg_mock.call_args_list[0][0][0] @@ -1477,7 +1476,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None context = MagicMock() context.args = ['XRP/BTC'] msg_mock.reset_mock() - telegram._delete_locks(update=update, context=context) + await telegram._delete_locks(update=update, context=context) assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0] @@ -1485,11 +1484,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None assert 'deadbeef' not in msg_mock.call_args_list[0][0][0] -def test_whitelist_static(default_conf, update, mocker) -> None: +async def test_whitelist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1497,33 +1496,33 @@ def test_whitelist_static(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_whitelist_dynamic(default_conf, update, mocker) -> None: +async def test_whitelist_dynamic(default_conf, update, mocker) -> None: mocker.patch(f'{EXMS}.exchange_has', return_value=True) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1531,30 +1530,30 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_blacklist_static(default_conf, update, mocker) -> None: +async def test_blacklist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._blacklist(update=update, context=MagicMock()) + await telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1564,7 +1563,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: # /blacklist ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1573,7 +1572,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["XRP/.*"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 4 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC, XRP/.*`" @@ -1582,7 +1581,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context.args = ["DOGE/BTC"] - telegram._blacklist_delete(update=update, context=context) + await telegram._blacklist_delete(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`HOT/BTC, ETH/BTC, XRP/.*`" in msg_mock.call_args_list[0][0][0]) @@ -2374,7 +2373,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog) -def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: +async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock() @@ -2400,7 +2399,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # no keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True telegram = init_telegram(freqtradebot) - telegram._send_msg('test') + await telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -2417,7 +2416,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = custom_keys_list telegram = init_telegram(freqtradebot) - telegram._send_msg('test') + await telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard assert log_has("using custom keyboard from config.json: " From c475c818414f03a06898cc2699165a9206d24f9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 15:15:19 +0200 Subject: [PATCH 097/186] Update several tests to async behavior --- tests/rpc/test_rpc_telegram.py | 143 +++++++++++++++++---------------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5b7950e4c..6ca028c1c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -321,7 +321,7 @@ async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) - assert re.search(r'Close Profit:', msg) -def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( EXMS, @@ -341,13 +341,13 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status is also enabled when stopped - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -355,7 +355,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() # Trigger status while we have a fulfilled order for the open trade - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) # close_rate should not be included in the message as the trade is not closed # and no line should be empty @@ -372,7 +372,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: context = MagicMock() context.args = ["2", "3"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) lines = msg_mock.call_args_list[0][0][0].split('\n') assert '' not in lines[:-1] @@ -387,7 +387,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["2"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 2 @@ -399,7 +399,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert 'Trade ID:* `2` - continued' in msg2 -def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -414,13 +414,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status table is also enabled when stopped - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -428,7 +428,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) text = re.sub('', '', msg_mock.call_args_list[-1][0][0]) line = text.split("\n") @@ -440,7 +440,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 1 -def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 @@ -462,7 +462,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 2 context = MagicMock() context.args = ["2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] @@ -476,7 +476,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] @@ -493,13 +493,13 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 1 context = MagicMock() context.args = ["1"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] -def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: +async def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker @@ -514,7 +514,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily -2 context = MagicMock() context.args = ["-2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -524,11 +524,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily today context = MagicMock() context.args = ["today"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert 'Daily Profit over the last 7 days:' in msg_mock.call_args_list[0][0][0] -def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -549,7 +549,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly 2 context = MagicMock() context.args = ["2"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 2 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -565,7 +565,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -581,7 +581,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly -3 context = MagicMock() context.args = ["-3"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -591,14 +591,14 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly this week context = MagicMock() context.args = ["this week"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert ( 'Weekly Profit over the last 8 weeks (starting from Monday):' in msg_mock.call_args_list[0][0][0] ) -def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -619,7 +619,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 2 context = MagicMock() context.args = ["2"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 2 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] @@ -634,7 +634,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 # Default to 6 months assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] @@ -651,7 +651,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 12 context = MagicMock() context.args = ["12"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] @@ -668,7 +668,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly -3 context = MagicMock() context.args = ["-3"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -678,11 +678,11 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly february context = MagicMock() context.args = ["february"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] -def test_telegram_profit_handle( +async def test_telegram_profit_handle( default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, limit_sell_order_usdt, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) @@ -695,7 +695,7 @@ def test_telegram_profit_handle( telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - telegram._profit(update=update, context=MagicMock()) + await telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -707,7 +707,7 @@ def test_telegram_profit_handle( context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] @@ -730,7 +730,7 @@ def test_telegram_profit_handle( Trade.commit() context.args = [3] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0] assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`' @@ -949,7 +949,7 @@ async def test_reload_config_handle(default_conf, update, mocker) -> None: async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, - ticker_sell_up, mocker) -> None: + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -1587,7 +1587,7 @@ async def test_blacklist_static(default_conf, update, mocker) -> None: in msg_mock.call_args_list[0][0][0]) -def test_telegram_logs(default_conf, update, mocker) -> None: +async def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), @@ -1598,13 +1598,13 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 assert "freqtrade\\.rpc\\.telegram" in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() context.args = ["1"] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 msg_mock.reset_mock() @@ -1612,22 +1612,22 @@ def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 200) context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) # Called at least 2 times. Exact times will change with unrelated changes to setup messages # Therefore we don't test for this explicitly. assert msg_mock.call_count >= 2 -def test_edge_disabled(default_conf, update, mocker) -> None: +async def test_edge_disabled(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] -def test_edge_enabled(edge_conf, update, mocker) -> None: +async def test_edge_enabled(edge_conf, update, mocker) -> None: mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -1636,7 +1636,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
@@ -1645,7 +1645,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 
     mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
         return_value={}))
-    telegram._edge(update=update, context=MagicMock())
+    await telegram._edge(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
     assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
@@ -1654,20 +1654,20 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 @pytest.mark.parametrize('is_short,regex_pattern',
                          [(True, r"just now[ ]*XRP\/BTC \(#3\)  -1.00% \("),
                           (False, r"just now[ ]*XRP\/BTC \(#3\)  1.00% \(")])
-def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
+async def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
     context = MagicMock()
     context.args = []
 
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
     context.args = ['hello']
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
@@ -1676,7 +1676,7 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
     context = MagicMock()
     context.args = [5]
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     msg_mock.call_count == 1
     assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
@@ -1686,13 +1686,13 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
+async def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     context = MagicMock()
     context.args = []
 
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1700,14 +1700,14 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     context = MagicMock()
     context.args = [1]
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     msg_mock.call_count == 1
     assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
     assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
+async def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
 
     mocker.patch.multiple(
         EXMS,
@@ -1717,7 +1717,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
     context = MagicMock()
     context.args = []
 
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1725,7 +1725,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
 
     context = MagicMock()
     context.args = [5]
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1734,43 +1734,43 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
     mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object())
     context = MagicMock()
     context.args = [6]
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert msg_mock.call_count == 1
     assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
 
 
-def test_help_handle(default_conf, update, mocker) -> None:
+async def test_help_handle(default_conf, update, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._help(update=update, context=MagicMock())
+    await telegram._help(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_version_handle(default_conf, update, mocker) -> None:
+async def test_version_handle(default_conf, update, mocker) -> None:
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
     freqtradebot.strategy.version = lambda: '1.1.1'
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
     assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_show_config_handle(default_conf, update, mocker) -> None:
+async def test_show_config_handle(default_conf, update, mocker) -> None:
 
     default_conf['runmode'] = RunMode.DRY_RUN
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -1779,7 +1779,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
 
     msg_mock.reset_mock()
     freqtradebot.config['trailing_stop'] = True
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -2323,7 +2323,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected):
     assert telegram._get_sell_emoji(msg) == expected
 
 
-def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
+async def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
@@ -2331,33 +2331,33 @@ def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     telegram._updater.bot = bot
 
     telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     assert len(bot.method_calls) == 1
 
     # Test update
     query = MagicMock()
-    telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
     edit_message_text = telegram._updater.bot.edit_message_text
     assert edit_message_text.call_count == 1
     assert "Updated: " in edit_message_text.call_args_list[0][1]['text']
 
     telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified"))
-    telegram._send_msg('test', callback_path="DeadBeef", query=query)
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query)
     assert telegram._updater.bot.edit_message_text.call_count == 1
     assert not log_has_re(r"TelegramError: .*", caplog)
 
     telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest(""))
-    telegram._send_msg('test2', callback_path="DeadBeef", query=query)
+    await telegram._send_msg('test2', callback_path="DeadBeef", query=query)
     assert telegram._updater.bot.edit_message_text.call_count == 1
     assert log_has_re(r"TelegramError: .*", caplog)
 
     telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF"))
-    telegram._send_msg('test3', callback_path="DeadBeef", query=query)
+    await telegram._send_msg('test3', callback_path="DeadBeef", query=query)
 
     assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog)
 
 
-def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
+async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
@@ -2366,7 +2366,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     telegram._updater.bot = bot
 
     telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
 
     # Bot should've tried to send it twice
     assert len(bot.method_calls) == 2
@@ -2424,13 +2424,14 @@ async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
                    "'/start', '/reload_config', '/help']]", caplog)
 
 
-def test_change_market_direction(default_conf, mocker, update) -> None:
+async def test_change_market_direction(default_conf, mocker, update) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE
     context = MagicMock()
     context.args = ["long"]
-    telegram._changemarketdir(update, context)
+    await telegram._changemarketdir(update, context)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
     context = MagicMock()
     context.args = ["invalid"]
+    await telegram._changemarketdir(update, context)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG

From 678c9ae67ffc2c69fc5000982ccb9ffb0ff8cf3d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 Apr 2023 15:18:57 +0200
Subject: [PATCH 098/186] Fix some more async telegram tests

---
 freqtrade/rpc/telegram.py      | 2 +-
 tests/rpc/test_rpc_telegram.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 192e2b6d7..babe7d582 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1083,7 +1083,7 @@ class Telegram(RPCHandler):
 
         if context.args:
             trade_id = context.args[0]
-            self._force_exit_action(trade_id)
+            await self._force_exit_action(trade_id)
         else:
             fiat_currency = self._config.get('fiat_display_currency', '')
             try:
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 6ca028c1c..f17dfa190 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -1211,7 +1211,7 @@ async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) ->
 
     assert keyboard[1][0].callback_data == 'force_exit__2 '
     update = MagicMock()
-    update.callback_query = MagicMock()
+    update.callback_query = AsyncMock()
     update.callback_query.data = keyboard[1][0].callback_data
     await telegram._force_exit_inline(update, None)
     assert update.callback_query.answer.call_count == 1
@@ -1298,7 +1298,7 @@ async def test_force_enter_no_pair(default_conf, update, mocker) -> None:
     # One additional button - cancel
     assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
     update = MagicMock()
-    update.callback_query = MagicMock()
+    update.callback_query = AsyncMock()
     update.callback_query.data = 'XRP/USDT_||_long'
     await telegram._force_enter_inline(update, None)
     assert fbuy_mock.call_count == 1

From 4177afdf8be4f2de5043cbfc934a8e3ed45e28a3 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 Apr 2023 15:43:53 +0200
Subject: [PATCH 099/186] More async test updates

---
 tests/rpc/test_rpc_telegram.py | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index f17dfa190..39895abaf 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -64,14 +64,14 @@ class DummyCls(Telegram):
         pass
 
     @authorized_only
-    def dummy_handler(self, *args, **kwargs) -> None:
+    async def dummy_handler(self, *args, **kwargs) -> None:
         """
         Fake method that only change the state of the object
         """
         self.state['called'] = True
 
     @authorized_only
-    def dummy_exception(self, *args, **kwargs) -> None:
+    async def dummy_exception(self, *args, **kwargs) -> None:
         """
         Fake method that throw an exception
         """
@@ -91,6 +91,7 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
         ftbot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(ftbot)
     telegram = Telegram(rpc, default_conf)
+    telegram._loop = MagicMock()
 
     return telegram, ftbot, msg_mock
 
@@ -139,7 +140,7 @@ def test_cleanup(default_conf, mocker, ) -> None:
     assert telegram._updater.stop.call_count == 1
 
 
-def test_authorized_only(default_conf, mocker, caplog, update) -> None:
+async def test_authorized_only(default_conf, mocker, caplog, update) -> None:
     patch_exchange(mocker)
     caplog.set_level(logging.DEBUG)
     default_conf['telegram']['enabled'] = False
@@ -148,14 +149,14 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None:
     dummy = DummyCls(rpc, default_conf)
 
     patch_get_signal(bot)
-    dummy.dummy_handler(update=update, context=MagicMock())
+    await dummy.dummy_handler(update=update, context=MagicMock())
     assert dummy.state['called'] is True
     assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
     assert not log_has('Rejected unauthorized message from: 0', caplog)
     assert not log_has('Exception occurred within Telegram module', caplog)
 
 
-def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
+async def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
     patch_exchange(mocker)
     caplog.set_level(logging.DEBUG)
     chat = Chat(0xdeadbeef, 0)
@@ -168,14 +169,14 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
     dummy = DummyCls(rpc, default_conf)
 
     patch_get_signal(bot)
-    dummy.dummy_handler(update=update, context=MagicMock())
+    await dummy.dummy_handler(update=update, context=MagicMock())
     assert dummy.state['called'] is False
     assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
     assert log_has('Rejected unauthorized message from: 3735928559', caplog)
     assert not log_has('Exception occurred within Telegram module', caplog)
 
 
-def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None:
+async def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None:
     patch_exchange(mocker)
 
     default_conf['telegram']['enabled'] = False
@@ -185,7 +186,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None
     dummy = DummyCls(rpc, default_conf)
     patch_get_signal(bot)
 
-    dummy.dummy_exception(update=update, context=MagicMock())
+    await dummy.dummy_exception(update=update, context=MagicMock())
     assert dummy.state['called'] is False
     assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
     assert not log_has('Rejected unauthorized message from: 0', caplog)
@@ -2086,7 +2087,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
         telegram._rpc._fiat_converter.convert_amount = old_convamount
 
 
-def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
+async def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 

From 69f61ef767f6d7fe3bb493c5dd72ff0a5d1dbf95 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 Apr 2023 15:54:25 +0200
Subject: [PATCH 100/186] Further telegram async tests

---
 tests/rpc/test_rpc_telegram.py | 31 ++++++++++++++++---------------
 1 file changed, 16 insertions(+), 15 deletions(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 39895abaf..1b84df1ca 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -137,7 +137,7 @@ def test_cleanup(default_conf, mocker, ) -> None:
 
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
     telegram.cleanup()
-    assert telegram._updater.stop.call_count == 1
+    assert telegram._app.stop.call_count == 1
 
 
 async def test_authorized_only(default_conf, mocker, caplog, update) -> None:
@@ -2327,32 +2327,33 @@ def test__sell_emoji(default_conf, mocker, msg, expected):
 async def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
+    bot.send_message = AsyncMock()
+    bot.edit_message_text = AsyncMock()
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
-    telegram._config['telegram']['enabled'] = True
     await telegram._send_msg('test')
     assert len(bot.method_calls) == 1
 
     # Test update
     query = MagicMock()
     await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
-    edit_message_text = telegram._updater.bot.edit_message_text
+    edit_message_text = telegram._app.bot.edit_message_text
     assert edit_message_text.call_count == 1
     assert "Updated: " in edit_message_text.call_args_list[0][1]['text']
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified"))
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("not modified"))
     await telegram._send_msg('test', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert not log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest(""))
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest(""))
     await telegram._send_msg('test2', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF"))
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=TelegramError("DeadBEEF"))
     await telegram._send_msg('test3', callback_path="DeadBeef", query=query)
 
     assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog)
@@ -2363,8 +2364,8 @@ async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
     telegram._config['telegram']['enabled'] = True
     await telegram._send_msg('test')
@@ -2377,7 +2378,7 @@ async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
 async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
-    bot.send_message = MagicMock()
+    bot.send_message = AsyncMock()
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(freqtradebot)
 
@@ -2393,8 +2394,8 @@ async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
 
     def init_telegram(freqtradebot):
         telegram = Telegram(rpc, default_conf)
-        telegram._updater = MagicMock()
-        telegram._updater.bot = bot
+        telegram._app = MagicMock()
+        telegram._app.bot = bot
         return telegram
 
     # no keyboard in config -> default keyboard

From cf0b37057cd74de028b1caefb6e57395d84a70be Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 16:58:12 +0200
Subject: [PATCH 101/186] update telegram "cleanup" test

---
 tests/rpc/test_rpc_telegram.py | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 1b84df1ca..b406ddc3a 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -2,6 +2,7 @@
 # pragma pylint: disable=protected-access, unused-argument, invalid-name
 # pragma pylint: disable=too-many-lines, too-many-arguments
 
+import asyncio
 import logging
 import re
 from datetime import datetime, timedelta, timezone
@@ -130,14 +131,24 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
     assert log_has(message_str, caplog)
 
 
-def test_cleanup(default_conf, mocker, ) -> None:
-    updater_mock = MagicMock()
-    updater_mock.stop = MagicMock()
-    mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
+async def test_telegram_cleanup(default_conf, mocker, ) -> None:
+    app_mock = MagicMock()
+    app_mock.stop = AsyncMock()
+    app_mock.initialize = AsyncMock()
 
-    telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
+    updater_mock = MagicMock()
+    updater_mock.stop = AsyncMock()
+    app_mock.updater = updater_mock
+    # mocker.patch('freqtrade.rpc.telegram.Application', app_mock)
+
+    telegram, _, _ = get_telegram_testobject(mocker, default_conf)
+    telegram._app = app_mock
+    telegram._loop = asyncio.get_running_loop()
+    telegram._thread = MagicMock()
     telegram.cleanup()
-    assert telegram._app.stop.call_count == 1
+    await asyncio.sleep(0.1)
+    assert app_mock.stop.call_count == 1
+    assert telegram._thread.join.call_count == 1
 
 
 async def test_authorized_only(default_conf, mocker, caplog, update) -> None:

From c9e6137ad0d472d910941ce24acbb379e2f44adc Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:18:28 +0200
Subject: [PATCH 102/186] Fix test_telegram _init test

---
 freqtrade/rpc/telegram.py      |  5 ++++-
 tests/rpc/test_rpc_telegram.py | 15 +++++++++------
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index babe7d582..d20d1862e 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -166,6 +166,9 @@ class Telegram(RPCHandler):
                 logger.info('using custom keyboard from '
                             f'config.json: {self._keyboard}')
 
+    def _init_telegram_app(self):
+        return Application.builder().token(self._config['telegram']['token']).build()
+
     def _init(self) -> None:
         """
         Initializes this module with the given config,
@@ -179,7 +182,7 @@ class Telegram(RPCHandler):
             self._loop = asyncio.new_event_loop()
             asyncio.set_event_loop(self._loop)
 
-        self._app = Application.builder().token(self._config['telegram']['token']).build()
+        self._app = self._init_telegram_app()
 
         # Register command handler and start telegram message polling
         handles = [
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index b406ddc3a..b762c999b 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -105,15 +105,18 @@ def test_telegram__init__(default_conf, mocker) -> None:
 
 
 def test_telegram_init(default_conf, mocker, caplog) -> None:
-    start_polling = MagicMock()
-    mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
+    app_mock = MagicMock()
+    mocker.patch('freqtrade.rpc.telegram.Telegram._start_thread', MagicMock())
+    mocker.patch('freqtrade.rpc.telegram.Telegram._init_telegram_app', return_value=app_mock)
+    mocker.patch('freqtrade.rpc.telegram.Telegram._startup_telegram', AsyncMock())
 
-    get_telegram_testobject(mocker, default_conf, mock=False)
-    assert start_polling.call_count == 0
+    telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
+    telegram._init()
+    assert app_mock.call_count == 0
 
     # number of handles registered
-    assert start_polling.dispatcher.add_handler.call_count > 0
-    assert start_polling.start_polling.call_count == 1
+    assert app_mock.add_handler.call_count > 0
+    # assert start_polling.start_polling.call_count == 1
 
     message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
                    "['balance'], ['start'], ['stop'], "

From 7171fd1132a9a81c210f1426833b8f383c3ae5da Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:24:11 +0200
Subject: [PATCH 103/186] Test telegram startup

---
 tests/rpc/test_rpc_telegram.py | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index b762c999b..5fbd4f98e 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -134,6 +134,23 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
     assert log_has(message_str, caplog)
 
 
+async def test_telegram_startup(default_conf, mocker) -> None:
+    app_mock = MagicMock()
+    app_mock.initialize = AsyncMock()
+    app_mock.start = AsyncMock()
+    app_mock.updater.start_polling = AsyncMock()
+    app_mock.updater.running = False
+    sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep',AsyncMock())
+
+    telegram, _, _ = get_telegram_testobject(mocker, default_conf)
+    telegram._app = app_mock
+    await telegram._startup_telegram()
+    assert app_mock.initialize.call_count == 1
+    assert app_mock.start.call_count == 1
+    assert app_mock.updater.start_polling.call_count == 1
+    assert sleep_mock.call_count == 1
+
+
 async def test_telegram_cleanup(default_conf, mocker, ) -> None:
     app_mock = MagicMock()
     app_mock.stop = AsyncMock()

From 5608aaca26789645acffdf3841607abcef36e06c Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:45:56 +0200
Subject: [PATCH 104/186] Simplify mocking

---
 freqtrade/exchange/exchange.py | 8 ++++++--
 tests/rpc/test_rpc_telegram.py | 3 ++-
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 7e276d538..669a5c7d1 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -107,8 +107,7 @@ class Exchange:
         # Lock event loop. This is necessary to avoid race-conditions when using force* commands
         # Due to funding fee fetching.
         self._loop_lock = Lock()
-        self.loop = asyncio.new_event_loop()
-        asyncio.set_event_loop(self.loop)
+        self.loop = self._init_async_loop()
         self._config: Config = {}
 
         self._config.update(config)
@@ -212,6 +211,11 @@ class Exchange:
         if self.loop and not self.loop.is_closed():
             self.loop.close()
 
+    def _init_async_loop(self):
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        return loop
+
     def validate_config(self, config):
         # Check if timeframe is available
         self.validate_timeframes(config.get('timeframe'))
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 5fbd4f98e..adfbb4b2a 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -89,6 +89,7 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
             _start_thread=MagicMock(),
         )
     if not ftbot:
+        mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop')
         ftbot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(ftbot)
     telegram = Telegram(rpc, default_conf)
@@ -140,7 +141,7 @@ async def test_telegram_startup(default_conf, mocker) -> None:
     app_mock.start = AsyncMock()
     app_mock.updater.start_polling = AsyncMock()
     app_mock.updater.running = False
-    sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep',AsyncMock())
+    sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep', AsyncMock())
 
     telegram, _, _ = get_telegram_testobject(mocker, default_conf)
     telegram._app = app_mock

From d25e82d095956a132f6ae5ec3b465204cf8fcda6 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:56:12 +0200
Subject: [PATCH 105/186] Mock exchange loop

---
 tests/rpc/test_rpc_telegram.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index adfbb4b2a..4601e64c4 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -37,6 +37,11 @@ from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades,
                             patch_exchange, patch_get_signal, patch_whitelist)
 
 
+@pytest.fixture(autouse=True)
+def mock_exchange_loop(mocker):
+    mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop')
+
+
 @pytest.fixture
 def default_conf(default_conf) -> dict:
     # Telegram is enabled by default

From 516b49ff500cacd50222fa2946eae89805cbaf9b Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 18:15:48 +0200
Subject: [PATCH 106/186] Fix bad types

---
 freqtrade/optimize/hyperopt.py | 2 +-
 freqtrade/rpc/telegram.py      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index fe590f0d2..bf6bb1ea0 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -556,7 +556,7 @@ class Hyperopt:
         self.backtesting.exchange.close()
         self.backtesting.exchange._api = None
         self.backtesting.exchange._api_async = None
-        self.backtesting.exchange.loop = None  # type: ignore
+        self.backtesting.exchange.loop = None
         self.backtesting.exchange._loop_lock = None  # type: ignore
         self.backtesting.exchange._cache_lock = None  # type: ignore
         # self.backtesting.exchange = None  # type: ignore
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index d20d1862e..a972dc322 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1739,7 +1739,7 @@ class Telegram(RPCHandler):
                 [InlineKeyboardButton("Refresh", callback_data=callback_path)]])
         else:
             if keyboard is not None:
-                reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True)
+                reply_markup = InlineKeyboardMarkup(keyboard)
             else:
                 reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
         try:

From c06759223eaa7698eefe885360926f199d3caed8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Mon, 17 Apr 2023 06:40:18 +0200
Subject: [PATCH 107/186] Improve telegram async tests

---
 freqtrade/rpc/telegram.py      |  2 +-
 tests/rpc/test_rpc_telegram.py | 19 ++++++++++++++++++-
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index a972dc322..6e509950c 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1796,7 +1796,7 @@ class Telegram(RPCHandler):
             if new_market_dir is not None:
                 self._rpc._update_market_direction(new_market_dir)
                 await self._send_msg("Successfully updated market direction"
-                               f" from *{old_market_dir}* to *{new_market_dir}*.")
+                                     f" from *{old_market_dir}* to *{new_market_dir}*.")
             else:
                 raise RPCException("Invalid market direction provided. \n"
                                    "Valid market directions: *long, short, even, none*")
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 4601e64c4..4b4c2b028 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -5,6 +5,7 @@
 import asyncio
 import logging
 import re
+import threading
 from datetime import datetime, timedelta, timezone
 from functools import reduce
 from random import choice, randint
@@ -57,6 +58,20 @@ def update():
     return _update
 
 
+def patch_eventloop_threading(telegrambot):
+    is_init = False
+
+    def thread_fuck():
+        nonlocal is_init
+        telegrambot._loop = asyncio.new_event_loop()
+        is_init = True
+        telegrambot._loop.run_forever()
+    x = threading.Thread(target=thread_fuck, daemon=True)
+    x.start()
+    while not is_init:
+        pass
+
+
 class DummyCls(Telegram):
     """
     Dummy class for testing the Telegram @authorized_only decorator
@@ -99,6 +114,7 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
     rpc = RPC(ftbot)
     telegram = Telegram(rpc, default_conf)
     telegram._loop = MagicMock()
+    patch_eventloop_threading(telegram)
 
     return telegram, ftbot, msg_mock
 
@@ -2216,7 +2232,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
     assert msg_mock.call_args[0][0] == '*Status:* `running`'
 
 
-def test_warning_notification(default_conf, mocker) -> None:
+async def test_warning_notification(default_conf, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     telegram.send_msg({
         'type': RPCMessageType.WARNING,
@@ -2412,6 +2428,7 @@ async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
 
 
+@pytest.mark.filterwarnings("ignore:.*ChatPermissions")
 async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()

From 2615b0297e5fa2c578baeafbe84497dcfa480fe7 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 19 Apr 2023 18:12:52 +0200
Subject: [PATCH 108/186] Move httpx to regular dependencies, losely-pin

---
 requirements-dev.txt | 2 --
 requirements.txt     | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index cd4c96eea..ea75bb8f2 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -18,8 +18,6 @@ pytest-random-order==1.1.0
 isort==5.12.0
 # For datetime mocking
 time-machine==2.9.0
-# fastapi testing
-httpx==0.24.0
 
 # Convert jupyter notebooks to markdown documents
 nbconvert==7.3.1
diff --git a/requirements.txt b/requirements.txt
index a07ef8897..6eecfb630 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,6 +7,8 @@ cryptography==40.0.2
 aiohttp==3.8.4
 SQLAlchemy==2.0.10
 python-telegram-bot==20.2
+# can't be hard-pinned due to telegram-bot pinning httpx with ~
+httpx>=0.23.3
 arrow==1.2.3
 cachetools==4.2.2
 requests==2.28.2

From b49ff3d5bcc99aa796bed83ebaf203418617b673 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 22 Apr 2023 11:11:03 +0200
Subject: [PATCH 109/186] Improve type safety

---
 freqtrade/exchange/exchange.py | 2 +-
 freqtrade/optimize/hyperopt.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 669a5c7d1..6706e94ab 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -211,7 +211,7 @@ class Exchange:
         if self.loop and not self.loop.is_closed():
             self.loop.close()
 
-    def _init_async_loop(self):
+    def _init_async_loop(self) -> asyncio.AbstractEventLoop:
         loop = asyncio.new_event_loop()
         asyncio.set_event_loop(loop)
         return loop
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index bf6bb1ea0..fe590f0d2 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -556,7 +556,7 @@ class Hyperopt:
         self.backtesting.exchange.close()
         self.backtesting.exchange._api = None
         self.backtesting.exchange._api_async = None
-        self.backtesting.exchange.loop = None
+        self.backtesting.exchange.loop = None  # type: ignore
         self.backtesting.exchange._loop_lock = None  # type: ignore
         self.backtesting.exchange._cache_lock = None  # type: ignore
         # self.backtesting.exchange = None  # type: ignore

From c19d6b4e291b3112946d4aa8ab7da82d662c3de0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 24 Apr 2023 14:01:01 +0000
Subject: [PATCH 110/186] Bump pandas from 1.5.3 to 2.0.1

Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.5.3 to 2.0.1.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v1.5.3...v2.0.1)

---
updated-dependencies:
- dependency-name: pandas
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 6eecfb630..7c646aec6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 numpy==1.24.3
-pandas==1.5.3
+pandas==2.0.1
 pandas-ta==0.3.14b
 
 ccxt==3.0.75

From 6a271317bcc055d84dd802fbfc415e12705b4327 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 08:53:02 +0200
Subject: [PATCH 111/186] use stop_price_param for dry stops

closes #8555
---
 freqtrade/exchange/exchange.py       | 4 ++--
 freqtrade/persistence/trade_model.py | 9 ++++++---
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 6706e94ab..9a303426a 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -867,7 +867,7 @@ class Exchange:
         }
         if stop_loss:
             dry_order["info"] = {"stopPrice": dry_order["price"]}
-            dry_order["stopPrice"] = dry_order["price"]
+            dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
             # Workaround to avoid filling stoploss orders immediately
             dry_order["ft_order_type"] = "stoploss"
         orderbook: Optional[OrderBook] = None
@@ -1019,7 +1019,7 @@ class Exchange:
             from freqtrade.persistence import Order
             order = Order.order_by_id(order_id)
             if order:
-                ccxt_order = order.to_ccxt_object()
+                ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
                 self._dry_run_open_orders[order_id] = ccxt_order
                 return ccxt_order
             # Gracefully handle errors with dry-run orders.
diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
index 0572b45a6..20cfe3102 100644
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -158,7 +158,7 @@ class Order(ModelBase):
                 self.order_filled_date = datetime.now(timezone.utc)
         self.order_update_date = datetime.now(timezone.utc)
 
-    def to_ccxt_object(self) -> Dict[str, Any]:
+    def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]:
         order: Dict[str, Any] = {
             'id': self.order_id,
             'symbol': self.ft_pair,
@@ -170,7 +170,6 @@ class Order(ModelBase):
             'side': self.ft_order_side,
             'filled': self.filled,
             'remaining': self.remaining,
-            'stopPrice': self.stop_price,
             'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
             'timestamp': int(self.order_date_utc.timestamp() * 1000),
             'status': self.status,
@@ -178,7 +177,11 @@ class Order(ModelBase):
             'info': {},
         }
         if self.ft_order_side == 'stoploss':
-            order['ft_order_type'] = 'stoploss'
+            order.update({
+                stopPriceName: self.stop_price,
+                'ft_order_type': 'stoploss',
+            })
+
         return order
 
     def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:

From 1e9fa4c04109aee023c3c2140d2d8fa7af2730af Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 09:04:02 +0200
Subject: [PATCH 112/186] Improve test to cover to_ccxt better

---
 tests/persistence/test_persistence.py | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py
index 948973ed5..1a7d84eca 100644
--- a/tests/persistence/test_persistence.py
+++ b/tests/persistence/test_persistence.py
@@ -2481,7 +2481,7 @@ def test_select_filled_orders(fee):
 
 
 @pytest.mark.usefixtures("init_persistence")
-def test_order_to_ccxt(limit_buy_order_open):
+def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open):
 
     order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy')
     order.ft_trade_id = 1
@@ -2495,11 +2495,23 @@ def test_order_to_ccxt(limit_buy_order_open):
     del raw_order['fee']
     del raw_order['datetime']
     del raw_order['info']
-    assert raw_order['stopPrice'] is None
-    del raw_order['stopPrice']
+    assert raw_order.get('stopPrice') is None
+    raw_order.pop('stopPrice', None)
     del limit_buy_order_open['datetime']
     assert raw_order == limit_buy_order_open
 
+    order1 = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'mocked', 'sell')
+    order1.ft_order_side = 'stoploss'
+    order1.stop_price = order1.price * 0.9
+    order1.ft_trade_id = 1
+    order1.session.add(order1)
+    Order.session.commit()
+
+    order_resp1 = Order.order_by_id(limit_sell_order_usdt_open['id'])
+    raw_order1 = order_resp1.to_ccxt_object()
+
+    assert raw_order1.get('stopPrice') is not None
+
 
 @pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize('data', [

From 59f9f4d467df2bddebfec1856b775e57dbce4671 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 09:27:33 +0200
Subject: [PATCH 113/186] Fix exception typos due to newlines

---
 freqtrade/freqai/freqai_interface.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py
index 7eaaeab3e..039b6a175 100644
--- a/freqtrade/freqai/freqai_interface.py
+++ b/freqtrade/freqai/freqai_interface.py
@@ -489,9 +489,9 @@ class IFreqaiModel(ABC):
         if dk.training_features_list != feature_list:
             raise OperationalException(
                 "Trying to access pretrained model with `identifier` "
-                "but found different features furnished by current strategy."
-                "Change `identifier` to train from scratch, or ensure the"
-                "strategy is furnishing the same features as the pretrained"
+                "but found different features furnished by current strategy. "
+                "Change `identifier` to train from scratch, or ensure the "
+                "strategy is furnishing the same features as the pretrained "
                 "model. In case of --strategy-list, please be aware that FreqAI "
                 "requires all strategies to maintain identical "
                 "feature_engineering_* functions"

From 11c9f96d23fd17d39d211817c5c2def03fe40be3 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 11:45:35 +0200
Subject: [PATCH 114/186] Use lock for trade entries, too

---
 freqtrade/freqtradebot.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 9cc26ad77..76c48a444 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -490,7 +490,8 @@ class FreqtradeBot(LoggingMixin):
         # Create entity and execute trade for each pair from whitelist
         for pair in whitelist:
             try:
-                trades_created += self.create_trade(pair)
+                with self._exit_lock:
+                    trades_created += self.create_trade(pair)
             except DependencyException as exception:
                 logger.warning('Unable to create trade for %s: %s', pair, exception)
 

From e8fedb685b899c2824b40e434d8eb837fa2eb180 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 11:52:13 +0200
Subject: [PATCH 115/186] Update missleading docstring

---
 freqtrade/freqtradebot.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 76c48a444..89f0ac55d 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin):
         """
         Try refinding a lost trade.
         Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
-        Tries to walk the stored orders and sell them off eventually.
+        Tries to walk the stored orders and updates the trade state if necessary.
         """
         logger.info(f"Trying to refind lost order for {trade}")
         for order in trade.orders:

From 1b228e3705a5d0dd5f20f580286427fb97b7265d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 15:51:58 +0200
Subject: [PATCH 116/186] Improve test resiliance by removing unneeded
 MagicMock

---
 tests/test_freqtradebot.py | 63 ++++++++++++++++++++++++++++++++------
 1 file changed, 54 insertions(+), 9 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 7bded0f82..ea99061b8 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2107,6 +2107,7 @@ def test_enter_positions(mocker, default_conf_usdt, return_value, side_effect,
     assert mock_ct.call_count == len(default_conf_usdt['exchange']['pair_whitelist'])
 
 
+@pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
@@ -2115,12 +2116,33 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
     mocker.patch(f'{EXMS}.fetch_order', return_value=limit_order[entry_side(is_short)])
     mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
-    trade.is_short = is_short
-    trade.open_order_id = '123'
-    trade.open_fee = 0.001
+    order_id = '123'
+    trade = Trade(
+            open_order_id=order_id,
+            pair='ETH/USDT',
+            fee_open=0.001,
+            fee_close=0.001,
+            open_rate=0.01,
+            open_date=arrow.utcnow().datetime,
+            stake_amount=0.01,
+            amount=11,
+            exchange="binance",
+            is_short=is_short,
+            leverage=1,
+            )
+    trade.orders.append(Order(
+        ft_order_side=entry_side(is_short),
+        price=0.01,
+        ft_pair=trade.pair,
+        ft_amount=trade.amount,
+        ft_price=trade.open_rate,
+        order_id=order_id,
+
+    ))
+    Trade.session.add(trade)
+    Trade.commit()
     trades = [trade]
+    freqtrade.wallets.update()
     n = freqtrade.exit_positions(trades)
     assert n == 0
     # Test amount not modified by fee-logic
@@ -2133,17 +2155,40 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
     assert gra.call_count == 0
 
 
+@pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     order = limit_order[entry_side(is_short)]
     mocker.patch(f'{EXMS}.fetch_order', return_value=order)
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
-    trade.is_short = is_short
+    order_id = '123'
+    trade = Trade(
+        open_order_id=order_id,
+        pair='ETH/USDT',
+        fee_open=0.001,
+        fee_close=0.001,
+        open_rate=0.01,
+        open_date=arrow.utcnow().datetime,
+        stake_amount=0.01,
+        amount=11,
+        exchange="binance",
+        is_short=is_short,
+        leverage=1,
+    )
+    trade.orders.append(Order(
+        ft_order_side=entry_side(is_short),
+        price=0.01,
+        ft_pair=trade.pair,
+        ft_amount=trade.amount,
+        ft_price=trade.open_rate,
+        order_id=order_id,
+
+    ))
     trade.open_order_id = None
-    trade.pair = 'ETH/USDT'
+    Trade.session.add(trade)
+    Trade.commit()
+    freqtrade.wallets.update()
     trades = [trade]
 
     # Test raise of DependencyException exception

From c4a0910908601cb7ae63645e41353123d0c4916f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 15:33:37 +0200
Subject: [PATCH 117/186] Handle special case where exit order is for more than
 the trade amount ...

---
 freqtrade/persistence/trade_model.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
index 20cfe3102..cff2c37f0 100644
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -711,7 +711,10 @@ class LocalTrade():
         if order.ft_order_side != self.entry_side:
             amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
                                                      self.precision_mode, self.contract_size)
-            if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
+            if (
+                isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC)
+                or order.safe_amount_after_fee > amount_tr
+            ):
                 self.close(order.safe_price)
             else:
                 self.recalc_trade_from_orders()

From e29ce218ebd3dee527566a40621015c5a4ac982d Mon Sep 17 00:00:00 2001
From: robcaulk 
Date: Wed, 26 Apr 2023 10:54:54 +0200
Subject: [PATCH 118/186] 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 119/186] 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 120/186] 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 121/186] 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 122/186] 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 1d9933412a45b892a4a3bfde30cead83bce242b6 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 27 Apr 2023 06:43:57 +0200
Subject: [PATCH 123/186] improve `/version` output formatting

---
 freqtrade/rpc/telegram.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 6e509950c..caa8715ac 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1636,7 +1636,7 @@ class Telegram(RPCHandler):
         strategy_version = self._rpc._freqtrade.strategy.version()
         version_string = f'*Version:* `{__version__}`'
         if strategy_version is not None:
-            version_string += f', *Strategy version: * `{strategy_version}`'
+            version_string += f'\n*Strategy version: * `{strategy_version}`'
 
         await self._send_msg(version_string)
 

From daf564b62fb012633e457fbc1448455b1cf48716 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 27 Apr 2023 18:27:09 +0200
Subject: [PATCH 124/186] Invert logic for webhook

closes #8562
---
 freqtrade/rpc/webhook.py      |  8 ++++----
 tests/rpc/test_rpc_webhook.py | 25 +++++++++++++++----------
 2 files changed, 19 insertions(+), 14 deletions(-)

diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py
index 14b881126..80690ec0c 100644
--- a/freqtrade/rpc/webhook.py
+++ b/freqtrade/rpc/webhook.py
@@ -44,8 +44,11 @@ class Webhook(RPCHandler):
 
     def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]:
         whconfig = self._config['webhook']
+        if msg['type'].value in whconfig:
+            # Explicit types should have priority
+            valuedict = whconfig.get(msg['type'].value)
         # Deprecated 2022.10 - only keep generic method.
-        if msg['type'] in [RPCMessageType.ENTRY]:
+        elif msg['type'] in [RPCMessageType.ENTRY]:
             valuedict = whconfig.get('webhookentry')
         elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
             valuedict = whconfig.get('webhookentrycancel')
@@ -62,9 +65,6 @@ class Webhook(RPCHandler):
                              RPCMessageType.EXCEPTION,
                              RPCMessageType.WARNING):
             valuedict = whconfig.get('webhookstatus')
-        elif msg['type'].value in whconfig:
-            # Allow all types ...
-            valuedict = whconfig.get(msg['type'].value)
         elif msg['type'] in (
                 RPCMessageType.PROTECTION_TRIGGER,
                 RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py
index f55582107..d0a0f5b1e 100644
--- a/tests/rpc/test_rpc_webhook.py
+++ b/tests/rpc/test_rpc_webhook.py
@@ -17,6 +17,10 @@ def get_webhook_dict() -> dict:
         "enabled": True,
         "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
         "webhookentry": {
+            # Intentionally broken, as "entry" should have priority.
+            "value1": "Buying {pair55555}",
+        },
+        "entry": {
             "value1": "Buying {pair}",
             "value2": "limit {limit:8f}",
             "value3": "{stake_amount:8f} {stake_currency}",
@@ -89,15 +93,15 @@ def test_send_msg_webhook(default_conf, mocker):
     webhook.send_msg(msg=msg)
     assert msg_mock.call_count == 1
     assert (msg_mock.call_args[0][0]["value1"] ==
-            default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
+            default_conf["webhook"]["entry"]["value1"].format(**msg))
     assert (msg_mock.call_args[0][0]["value2"] ==
-            default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
+            default_conf["webhook"]["entry"]["value2"].format(**msg))
     assert (msg_mock.call_args[0][0]["value3"] ==
-            default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
+            default_conf["webhook"]["entry"]["value3"].format(**msg))
     assert (msg_mock.call_args[0][0]["value4"] ==
-            default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
+            default_conf["webhook"]["entry"]["value4"].format(**msg))
     assert (msg_mock.call_args[0][0]["value5"] ==
-            default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
+            default_conf["webhook"]["entry"]["value5"].format(**msg))
     # Test short
     msg_mock.reset_mock()
 
@@ -116,15 +120,15 @@ def test_send_msg_webhook(default_conf, mocker):
     webhook.send_msg(msg=msg)
     assert msg_mock.call_count == 1
     assert (msg_mock.call_args[0][0]["value1"] ==
-            default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
+            default_conf["webhook"]["entry"]["value1"].format(**msg))
     assert (msg_mock.call_args[0][0]["value2"] ==
-            default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
+            default_conf["webhook"]["entry"]["value2"].format(**msg))
     assert (msg_mock.call_args[0][0]["value3"] ==
-            default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
+            default_conf["webhook"]["entry"]["value3"].format(**msg))
     assert (msg_mock.call_args[0][0]["value4"] ==
-            default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
+            default_conf["webhook"]["entry"]["value4"].format(**msg))
     assert (msg_mock.call_args[0][0]["value5"] ==
-            default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
+            default_conf["webhook"]["entry"]["value5"].format(**msg))
     # Test buy cancel
     msg_mock.reset_mock()
 
@@ -328,6 +332,7 @@ def test_send_msg_webhook(default_conf, mocker):
 
 def test_exception_send_msg(default_conf, mocker, caplog):
     default_conf["webhook"] = get_webhook_dict()
+    del default_conf["webhook"]["entry"]
     del default_conf["webhook"]["webhookentry"]
 
     webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)

From 2a9e50a6a9baa2f1ca3216878ceb5a28c0ac024e Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 27 Apr 2023 19:43:33 +0200
Subject: [PATCH 125/186] Add test testing create-table statement creation for
 different sql dialects

closes #8561
---
 freqtrade/persistence/key_value_store.py |  2 +-
 tests/persistence/test_migrations.py     | 14 ++++++++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py
index 2d26acbd3..110a23d6c 100644
--- a/freqtrade/persistence/key_value_store.py
+++ b/freqtrade/persistence/key_value_store.py
@@ -36,7 +36,7 @@ class _KeyValueStoreModel(ModelBase):
 
     value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
 
-    string_value: Mapped[Optional[str]]
+    string_value: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
     datetime_value: Mapped[Optional[datetime]]
     float_value: Mapped[Optional[float]]
     int_value: Mapped[Optional[int]]
diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py
index 854d39994..13b3f89bf 100644
--- a/tests/persistence/test_migrations.py
+++ b/tests/persistence/test_migrations.py
@@ -1,15 +1,18 @@
 # pragma pylint: disable=missing-docstring, C0103
 import logging
+from importlib import import_module
 from pathlib import Path
 from unittest.mock import MagicMock
 
 import pytest
 from sqlalchemy import create_engine, select, text
+from sqlalchemy.schema import CreateTable
 
 from freqtrade.constants import DEFAULT_DB_PROD_URL
 from freqtrade.enums import TradingMode
 from freqtrade.exceptions import OperationalException
 from freqtrade.persistence import Trade, init_db
+from freqtrade.persistence.base import ModelBase
 from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
 from freqtrade.persistence.models import PairLock
 from tests.conftest import log_has
@@ -411,3 +414,14 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
     assert len(pairlocks) == 1
     pairlocks[0].pair == 'ETH/BTC'
     pairlocks[0].side == '*'
+
+
+@pytest.mark.parametrize('dialect', [
+    'sqlite', 'postgresql', 'mysql', 'oracle', 'mssql',
+    ])
+def test_create_table_compiles(dialect):
+
+    dialect_mod = import_module(f"sqlalchemy.dialects.{dialect}")
+    for table in ModelBase.metadata.tables.values():
+        create_sql = str(CreateTable(table).compile(dialect=dialect_mod.dialect()))
+        assert 'CREATE TABLE' in create_sql

From 76ae539e619f1b8100f6392abcdb8edd5dafa4cb Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 28 Apr 2023 14:55:17 +0200
Subject: [PATCH 126/186] 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 127/186] 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 128/186] 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 129/186] 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 130/186] 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 131/186] 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 132/186] 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 133/186] 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 134/186] 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 135/186] 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 136/186] 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 137/186] 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 138/186] 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 139/186] 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 140/186] 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 af139ffbab1a99927fd62cb970c862516566a59f Mon Sep 17 00:00:00 2001
From: robcaulk 
Date: Mon, 1 May 2023 13:18:03 +0000
Subject: [PATCH 141/186] add transformer with positional encoding, fix some
 odds and ends in pytorch, upgrade to PyTorch 2.0

---
 docs/freqai-parameter-table.md                |   2 +-
 .../base_models/BasePyTorchClassifier.py      |   1 +
 .../freqai/base_models/BasePyTorchModel.py    |   1 +
 .../base_models/BasePyTorchRegressor.py       |   1 +
 freqtrade/freqai/freqai_interface.py          |   1 +
 .../PyTorchTransformerRegressor.py            | 139 ++++++++++++++++++
 .../freqai/torch/PyTorchDataConvertor.py      |  14 +-
 freqtrade/freqai/torch/PyTorchMLPModel.py     |   5 +-
 freqtrade/freqai/torch/PyTorchModelTrainer.py |  53 +++++--
 .../freqai/torch/PyTorchTransformerModel.py   |  91 ++++++++++++
 freqtrade/freqai/torch/datasets.py            |  19 +++
 requirements-freqai-rl.txt                    |   2 +-
 tests/freqai/test_freqai_interface.py         |  14 +-
 13 files changed, 317 insertions(+), 26 deletions(-)
 create mode 100644 freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py
 create mode 100644 freqtrade/freqai/torch/PyTorchTransformerModel.py
 create mode 100644 freqtrade/freqai/torch/datasets.py

diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md
index 1487b92c2..76c175304 100644
--- a/docs/freqai-parameter-table.md
+++ b/docs/freqai-parameter-table.md
@@ -114,5 +114,5 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
 |------------|-------------|
 |  |  **Extraneous parameters**
 | `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. 
**Datatype:** Boolean.
Default: `False`. -| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction.
**Datatype:** Integer.
Default: `2`. +| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction.
**Datatype:** Integer.
Default: `2`. | `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI).
**Datatype:** Boolean.
Default: `False`. diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py index 977152cc5..1f54e7609 100644 --- a/freqtrade/freqai/base_models/BasePyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -74,6 +74,7 @@ class BasePyTorchClassifier(BasePyTorchModel): dk.data_dictionary["prediction_features"], device=self.device ) + self.model.model.eval() logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 8177b8eb8..82042d24c 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -27,6 +27,7 @@ class BasePyTorchModel(IFreqaiModel, ABC): self.device = "cuda" if torch.cuda.is_available() else "cpu" test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size') self.splits = ["train", "test"] if test_size != 0 else ["train"] + self.window_size = self.freqai_info.get("conv_width", 1) def train( self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index ea6fabe49..d5a550f58 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -44,6 +44,7 @@ class BasePyTorchRegressor(BasePyTorchModel): dk.data_dictionary["prediction_features"], device=self.device ) + self.model.model.eval() y = self.model.model(x) y = y.cpu() pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 3580963d4..6815e421c 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -80,6 +80,7 @@ class IFreqaiModel(ABC): if self.keras and self.ft_params.get("DI_threshold", 0): self.ft_params["DI_threshold"] = 0 logger.warning("DI threshold is not configured for Keras models yet. Deactivating.") + self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 diff --git a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py new file mode 100644 index 000000000..e760f6e68 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py @@ -0,0 +1,139 @@ +from typing import Any, Dict, Tuple + +import numpy as np +import numpy.typing as npt +import pandas as pd +import torch + +from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchTransformerTrainer +from freqtrade.freqai.torch.PyTorchTransformerModel import PyTorchTransformerModel + + +class PyTorchTransformerRegressor(BasePyTorchRegressor): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchRegressor + predict method that expects the model to predict tensor of type float. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": null, + "window_size": 10 + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + n_features = data_dictionary["train_features"].shape[-1] + n_labels = data_dictionary["train_labels"].shape[-1] + model = PyTorchTransformerModel( + input_dim=n_features, + output_dim=n_labels, + time_window=self.window_size, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchTransformerTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + window_size=self.window_size, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer + + def predict( + self, unfiltered_df: pd.DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[pd.DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + # if user is asking for multiple predictions, slide the window + # along the tensor + x = x.unsqueeze(0) + # create empty torch tensor + self.model.model.eval() + yb = torch.empty(0) + if x.shape[1] > 1: + ws = self.window_size + for i in range(0, x.shape[1] - ws): + xb = x[:, i:i + ws, :] + y = self.model.model(xb) + yb = torch.cat((yb, y), dim=0) + else: + yb = self.model.model(x) + + yb = yb.cpu().squeeze() + pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list) + pred_df = dk.denormalize_labels_from_metadata(pred_df) + + if x.shape[1] > 1: + zeros_df = pd.DataFrame(np.zeros((x.shape[1] - len(pred_df), len(pred_df.columns))), + columns=pred_df.columns) + pred_df = pd.concat([zeros_df, pred_df], axis=0, ignore_index=True) + return (pred_df, dk.do_predict) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index a31ccdc79..e6b815373 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import List, Optional +from typing import Optional import pandas as pd import torch @@ -12,14 +12,14 @@ class PyTorchDataConvertor(ABC): """ @abstractmethod - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: """ :param df: "*_features" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). """ @abstractmethod - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: """ :param df: "*_labels" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). @@ -45,14 +45,14 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): self._target_tensor_type = target_tensor_type self._squeeze_target_tensor = squeeze_target_tensor - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: x = torch.from_numpy(df.values).float() if device: x = x.to(device) - return [x] + return x - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: y = torch.from_numpy(df.values) if self._target_tensor_type: @@ -64,4 +64,4 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): if device: y = y.to(device) - return [y] + return y diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 62d3216df..0093388f8 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -1,5 +1,4 @@ import logging -from typing import List import torch from torch import nn @@ -47,8 +46,8 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, tensors: List[torch.Tensor]) -> torch.Tensor: - x: torch.Tensor = tensors[0] + def forward(self, x: torch.Tensor) -> torch.Tensor: + # x: torch.Tensor = tensors[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 8277ba937..a3b0d9b9c 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -12,6 +12,8 @@ from torch.utils.data import DataLoader, TensorDataset from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface +from .datasets import WindowDataset + logger = logging.getLogger(__name__) @@ -26,6 +28,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): init_model: Dict, data_convertor: PyTorchDataConvertor, model_meta_data: Dict[str, Any] = {}, + window_size: int = 1, **kwargs ): """ @@ -52,6 +55,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) self.data_convertor = data_convertor + self.window_size: int = window_size if init_model: self.load_from_checkpoint(init_model) @@ -75,16 +79,15 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): batch_size=self.batch_size, n_iters=self.max_iters ) + self.model.train() for epoch in range(1, epochs + 1): # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): - for tensor in batch_data: - tensor.to(self.device) - - xb = batch_data[:-1] - yb = batch_data[-1] + xb, yb = batch_data + xb.to(self.device) + yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) @@ -120,12 +123,10 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): if max_n_eval_batches and i > max_n_eval_batches: n_batches += 1 break + xb, yb = batch_data + xb.to(self.device) + yb.to(self.device) - for tensor in batch_data: - tensor.to(self.device) - - xb = batch_data[:-1] - yb = batch_data[-1] yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) losses.append(loss.item()) @@ -145,7 +146,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): for split in splits: x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device) y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device) - dataset = TensorDataset(*x, *y) + dataset = TensorDataset(x, y) data_loader = DataLoader( dataset, batch_size=self.batch_size, @@ -206,3 +207,33 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) self.model_meta_data = checkpoint["model_meta_data"] return self + + +class PyTorchTransformerTrainer(PyTorchModelTrainer): + """ + Creating a trainer for the Transformer model. + """ + + def create_data_loaders_dictionary( + self, + data_dictionary: Dict[str, pd.DataFrame], + splits: List[str] + ) -> Dict[str, DataLoader]: + """ + Converts the input data to PyTorch tensors using a data loader. + """ + data_loader_dictionary = {} + for split in splits: + x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device) + y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device) + dataset = WindowDataset(x, y, self.window_size) + data_loader = DataLoader( + dataset, + batch_size=self.batch_size, + shuffle=False, + drop_last=True, + num_workers=0, + ) + data_loader_dictionary[split] = data_loader + + return data_loader_dictionary diff --git a/freqtrade/freqai/torch/PyTorchTransformerModel.py b/freqtrade/freqai/torch/PyTorchTransformerModel.py new file mode 100644 index 000000000..0a252112a --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchTransformerModel.py @@ -0,0 +1,91 @@ +import math + +import torch +import torch.nn as nn + + +""" +The architecture is based on the paper “Attention Is All You Need”. +Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, +Lukasz Kaiser, and Illia Polosukhin. 2017. +""" + + +class PyTorchTransformerModel(nn.Module): + """ + A transformer approach to time series modeling using positional encoding. + The architecture is based on the paper “Attention Is All You Need”. + Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, + Lukasz Kaiser, and Illia Polosukhin. 2017. + """ + + def __init__(self, input_dim: int = 7, output_dim: int = 7, hidden_dim=1024, + n_layer=2, dropout_percent=0.1, time_window=10): + super().__init__() + self.time_window = time_window + self.input_net = nn.Sequential( + nn.Dropout(dropout_percent), nn.Linear(input_dim, hidden_dim) + ) + + # Encode the timeseries with Positional encoding + self.positional_encoding = PositionalEncoding(d_model=hidden_dim, max_len=hidden_dim) + + # Define the encoder block of the Transformer + self.encoder_layer = nn.TransformerEncoderLayer( + d_model=hidden_dim, nhead=8, dropout=dropout_percent, batch_first=True) + self.transformer = nn.TransformerEncoder(self.encoder_layer, num_layers=n_layer) + + # Pseudo decoder + self.output_net = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.LayerNorm(hidden_dim), + nn.Tanh(), + nn.Dropout(dropout_percent), + ) + + self.output_layer = nn.Sequential( + nn.Linear(hidden_dim * time_window, output_dim), + nn.Tanh() + ) + + def forward(self, x, mask=None, add_positional_encoding=True): + """ + Args: + x: Input features of shape [Batch, SeqLen, input_dim] + mask: Mask to apply on the attention outputs (optional) + add_positional_encoding: If True, we add the positional encoding to the input. + Might not be desired for some tasks. + """ + x = self.input_net(x) + if add_positional_encoding: + x = self.positional_encoding(x) + x = self.transformer(x, mask=mask) + x = self.output_net(x) + x = x.reshape(-1, 1, self.time_window * x.shape[-1]) + x = self.output_layer(x) + return x + + +class PositionalEncoding(torch.nn.Module): + def __init__(self, d_model, max_len=5000): + """ + Args + d_model: Hidden dimensionality of the input. + max_len: Maximum length of a sequence to expect. + """ + super().__init__() + + # Create matrix of [SeqLen, HiddenDim] representing the positional encoding + # for max_len inputs + pe = torch.zeros(max_len, d_model) + position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) + div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + + self.register_buffer("pe", pe, persistent=False) + + def forward(self, x): + x = x + self.pe[:, : x.size(1)] + return x diff --git a/freqtrade/freqai/torch/datasets.py b/freqtrade/freqai/torch/datasets.py new file mode 100644 index 000000000..120d8a116 --- /dev/null +++ b/freqtrade/freqai/torch/datasets.py @@ -0,0 +1,19 @@ +import torch + + +class WindowDataset(torch.utils.data.Dataset): + def __init__(self, xs, ys, window_size): + self.xs = xs + self.ys = ys + self.window_size = window_size + + def __len__(self): + return len(self.xs) - self.window_size + + def __getitem__(self, index): + idx_rev = len(self.xs) - self.window_size - index - 1 + window_x = self.xs[idx_rev:idx_rev + self.window_size, :] + # Beware of indexing, these two window_x and window_y are aimed at the same row! + # this is what happens when you use : + window_y = self.ys[idx_rev + self.window_size - 1, :].unsqueeze(0) + return window_x, window_y diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 45ccc40cc..525c25229 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -2,7 +2,7 @@ -r requirements-freqai.txt # Required for freqai-rl -torch==1.13.1; python_version < '3.11' +torch==2.0.0; python_version < '3.11' #until these branches will be released we can use this gymnasium==0.28.1 stable_baselines3==2.0.0a5 diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7346191db..ed0910089 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -50,7 +50,8 @@ def can_run_model(model: str) -> None: ('XGBoostRegressor', False, True, False, True, False, 10), ('XGBoostRFRegressor', False, False, False, True, False, 0), ('CatboostRegressor', False, False, False, True, True, 0), - ('PyTorchMLPRegressor', False, False, False, True, False, 0), + ('PyTorchMLPRegressor', False, False, False, False, False, 0), + ('PyTorchTransformerRegressor', False, False, False, False, False, 0), ('ReinforcementLearner', False, True, False, True, False, 0), ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), @@ -82,10 +83,13 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True - if 'PyTorchMLPRegressor' in model: + if 'PyTorch' in model: model_save_ext = 'zip' pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + if 'Transformer' in model: + # transformer model takes a window, unlike the MLP regressor + freqai_conf.update({"conv_width": 10}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -228,6 +232,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): ("XGBoostRegressor", 2, "freqai_test_strat"), ("CatboostRegressor", 2, "freqai_test_strat"), ("PyTorchMLPRegressor", 2, "freqai_test_strat"), + ("PyTorchTransformerRegressor", 2, "freqai_test_strat"), ("ReinforcementLearner", 3, "freqai_rl_test_strat"), ("XGBoostClassifier", 2, "freqai_test_classifier"), ("LightGBMClassifier", 2, "freqai_test_classifier"), @@ -253,9 +258,12 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) if 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'PyTorchMLP' in model: + if 'PyTorch' in model: pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + if 'Transformer' in model: + # transformer model takes a window, unlike the MLP regressor + freqai_conf.update({"conv_width": 10}) freqai_conf.get("freqai", {}).get("feature_parameters", {}).update( {"indicator_periods_candles": [2]}) 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 142/186] 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 143/186] 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 144/186] 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 145/186] 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 146/186] 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 147/186] 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 148/186] 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 149/186] 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 150/186] 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 151/186] 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 152/186] 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 153/186] 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 154/186] 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 155/186] 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 156/186] 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 157/186] 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 158/186] 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 159/186] 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 160/186] 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 161/186] 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 162/186] 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 163/186] 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 164/186] 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 165/186] 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 166/186] 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) From c2beeb4c790614f1face9fa08709d7d1de9046bb Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 6 May 2023 15:53:58 +0000 Subject: [PATCH 167/186] bug fix backtest feature validation --- freqtrade/freqai/freqai_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 3580963d4..cf4123a5a 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -306,10 +306,11 @@ class IFreqaiModel(ABC): if dk.check_if_backtest_prediction_is_valid(len_backtest_df): if check_features: self.dd.load_metadata(dk) - dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators( + df_fts = self.dk.use_strategy_to_populate_indicators( strategy, prediction_dataframe=dataframe.tail(1), pair=pair ) - dk.find_features(dataframe_dummy_features) + df_fts = dk.remove_special_chars_from_feature_names(df_fts) + dk.find_features(df_fts) self.check_if_feature_list_matches_strategy(dk) check_features = False append_df = dk.get_backtesting_prediction() From 3bbb7e38ead75996f5ba1bf23098067f57ed313b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 6 May 2023 16:12:10 +0000 Subject: [PATCH 168/186] improve transformer architecture, remove 3.10 install constraint, add documentation for torch.compile() --- docs/freqai-configuration.md | 18 +++++++++++ .../freqai/torch/PyTorchTransformerModel.py | 32 ++++++++++--------- requirements-freqai-rl.txt | 2 +- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index e7aca20be..ad7cafd3d 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -395,3 +395,21 @@ Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. T return dataframe ``` To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py). + + +#### Improving performance with `torch.compile()` + +Torch provides a `torch.compile()` method that can be used to improve performance for specific GPU hardware. More details can be found [here](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html). In brief, you simply wrap your `model` in `torch.compile()`: + + +```python + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + model = torch.compile(model) +``` + +Then proceed to use the model as normal. Keep in mind that doing this will remove eager execution, which means errors and tracebacks will not be informative. diff --git a/freqtrade/freqai/torch/PyTorchTransformerModel.py b/freqtrade/freqai/torch/PyTorchTransformerModel.py index 0a252112a..2ab3ea434 100644 --- a/freqtrade/freqai/torch/PyTorchTransformerModel.py +++ b/freqtrade/freqai/torch/PyTorchTransformerModel.py @@ -20,32 +20,35 @@ class PyTorchTransformerModel(nn.Module): """ def __init__(self, input_dim: int = 7, output_dim: int = 7, hidden_dim=1024, - n_layer=2, dropout_percent=0.1, time_window=10): + n_layer=2, dropout_percent=0.1, time_window=10, nhead=8): super().__init__() self.time_window = time_window + # ensure the input dimension to the transformer is divisible by nhead + self.dim_val = input_dim - (input_dim % nhead) self.input_net = nn.Sequential( - nn.Dropout(dropout_percent), nn.Linear(input_dim, hidden_dim) + nn.Dropout(dropout_percent), nn.Linear(input_dim, self.dim_val) ) # Encode the timeseries with Positional encoding - self.positional_encoding = PositionalEncoding(d_model=hidden_dim, max_len=hidden_dim) + self.positional_encoding = PositionalEncoding(d_model=self.dim_val, max_len=self.dim_val) # Define the encoder block of the Transformer self.encoder_layer = nn.TransformerEncoderLayer( - d_model=hidden_dim, nhead=8, dropout=dropout_percent, batch_first=True) + d_model=self.dim_val, nhead=nhead, dropout=dropout_percent, batch_first=True) self.transformer = nn.TransformerEncoder(self.encoder_layer, num_layers=n_layer) - # Pseudo decoder + # the pseudo decoding FC self.output_net = nn.Sequential( - nn.Linear(hidden_dim, hidden_dim), - nn.LayerNorm(hidden_dim), - nn.Tanh(), + nn.Linear(hidden_dim * time_window, int(hidden_dim)), + nn.ReLU(), nn.Dropout(dropout_percent), - ) - - self.output_layer = nn.Sequential( - nn.Linear(hidden_dim * time_window, output_dim), - nn.Tanh() + nn.Linear(int(hidden_dim), int(hidden_dim / 2)), + nn.ReLU(), + nn.Dropout(dropout_percent), + nn.Linear(int(hidden_dim / 2), int(hidden_dim / 4)), + nn.ReLU(), + nn.Dropout(dropout_percent), + nn.Linear(int(hidden_dim / 4), output_dim) ) def forward(self, x, mask=None, add_positional_encoding=True): @@ -60,9 +63,8 @@ class PyTorchTransformerModel(nn.Module): if add_positional_encoding: x = self.positional_encoding(x) x = self.transformer(x, mask=mask) - x = self.output_net(x) x = x.reshape(-1, 1, self.time_window * x.shape[-1]) - x = self.output_layer(x) + x = self.output_net(x) return x diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 525c25229..6b9c1c298 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -2,7 +2,7 @@ -r requirements-freqai.txt # Required for freqai-rl -torch==2.0.0; python_version < '3.11' +torch==2.0.0 #until these branches will be released we can use this gymnasium==0.28.1 stable_baselines3==2.0.0a5 From 36e1e58dad0c63a41cf97792cea248942d590ca4 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 6 May 2023 17:40:04 +0000 Subject: [PATCH 169/186] fix arch --- freqtrade/freqai/torch/PyTorchTransformerModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchTransformerModel.py b/freqtrade/freqai/torch/PyTorchTransformerModel.py index 2ab3ea434..702a7a08b 100644 --- a/freqtrade/freqai/torch/PyTorchTransformerModel.py +++ b/freqtrade/freqai/torch/PyTorchTransformerModel.py @@ -39,7 +39,7 @@ class PyTorchTransformerModel(nn.Module): # the pseudo decoding FC self.output_net = nn.Sequential( - nn.Linear(hidden_dim * time_window, int(hidden_dim)), + nn.Linear(self.dim_val * time_window, int(hidden_dim)), nn.ReLU(), nn.Dropout(dropout_percent), nn.Linear(int(hidden_dim), int(hidden_dim / 2)), From 4b1cb964464e2bb9870de659658f785d9347eb3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:56:59 +0000 Subject: [PATCH 170/186] Bump orjson from 3.8.11 to 3.8.12 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.11 to 3.8.12. - [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.11...3.8.12) --- 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 e3e8a5938..07722c7c7 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.11 +orjson==3.8.12 # Notify systemd sdnotify==0.3.2 From fa40e4e888cbdd026543355c8828041e1c2f5be5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:57:11 +0000 Subject: [PATCH 171/186] Bump websockets from 11.0.2 to 11.0.3 Bumps [websockets](https://github.com/aaugustin/websockets) from 11.0.2 to 11.0.3. - [Release notes](https://github.com/aaugustin/websockets/releases) - [Commits](https://github.com/aaugustin/websockets/compare/11.0.2...11.0.3) --- updated-dependencies: - dependency-name: websockets 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 e3e8a5938..e9d33c394 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,7 +56,7 @@ python-dateutil==2.8.2 schedule==1.2.0 #WS Messages -websockets==11.0.2 +websockets==11.0.3 janus==1.0.0 ast-comments==1.0.1 From 3bb872a5e7a2a601745242c32a2c36a45858d3be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:57:22 +0000 Subject: [PATCH 172/186] Bump mkdocs from 1.4.2 to 1.4.3 Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.4.2...1.4.3) --- updated-dependencies: - dependency-name: mkdocs 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 40f7d9b1c..89ed13549 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ markdown==3.3.7 -mkdocs==1.4.2 +mkdocs==1.4.3 mkdocs-material==9.1.8 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 From 4a911bbe90a481203af1bb3cfcdcb85b54c52127 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:57:44 +0000 Subject: [PATCH 173/186] Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.5 to 1.8.6. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.5...v1.8.6) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52c772bd3..8e7b11672 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.5 + uses: pypa/gh-action-pypi-publish@v1.8.6 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.5 + uses: pypa/gh-action-pypi-publish@v1.8.6 if: (github.event_name == 'release') with: user: __token__ From ecd34ce470afbec09fd18236d74c94c13ac68efd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:57:54 +0000 Subject: [PATCH 174/186] Bump requests from 2.29.0 to 2.30.0 Bumps [requests](https://github.com/psf/requests) from 2.29.0 to 2.30.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.29.0...v2.30.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 e3e8a5938..685b0a44c 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.29.0 +requests==2.30.0 urllib3==1.26.15 jsonschema==4.17.3 TA-Lib==0.4.26 From 68a37cb71b0f81a02bbeafdf461d7acac5838754 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:58:05 +0000 Subject: [PATCH 175/186] Bump tensorboard from 2.12.2 to 2.13.0 Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.12.2 to 2.13.0. - [Release notes](https://github.com/tensorflow/tensorboard/releases) - [Changelog](https://github.com/tensorflow/tensorboard/blob/master/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorboard/compare/2.12.2...2.13.0) --- updated-dependencies: - dependency-name: tensorboard dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 51396ab91..e5bc23d56 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -8,4 +8,4 @@ joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 xgboost==1.7.5 -tensorboard==2.12.2 +tensorboard==2.13.0 From 7bd33be8f7afad92a55d6152eaca1af306f506b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:58:16 +0000 Subject: [PATCH 176/186] Bump python-telegram-bot from 20.2 to 20.3 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 20.2 to 20.3. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v20.2...v20.3) --- updated-dependencies: - dependency-name: python-telegram-bot 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 e3e8a5938..b52ed5fa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==3.0.85 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.12 -python-telegram-bot==20.2 +python-telegram-bot==20.3 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.23.3 arrow==1.2.3 From bf7c52a9eee32e55795033a6fb4b86175e10a28a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:58:37 +0000 Subject: [PATCH 177/186] Bump types-requests from 2.29.0.0 to 2.30.0.0 Bumps [types-requests](https://github.com/python/typeshed) from 2.29.0.0 to 2.30.0.0. - [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 5cc0e78ce..253942e0e 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.29.0.0 +types-requests==2.30.0.0 types-tabulate==0.9.0.2 types-python-dateutil==2.8.19.12 From 75e5f325a9a34087107949f613b42f9625b8e83d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 03:59:08 +0000 Subject: [PATCH 178/186] Bump ruff from 0.0.263 to 0.0.265 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.263 to 0.0.265. - [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.263...v0.0.265) --- 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 5cc0e78ce..e3c4740e8 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.263 +ruff==0.0.265 mypy==1.2.0 pre-commit==3.2.2 pytest==7.3.1 From 10604bf49c9be5637c5a70076bb140989423144e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 May 2023 06:46:30 +0200 Subject: [PATCH 179/186] Run Torch tests on 3.11 --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index ed0910089..95414a83f 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -41,7 +41,7 @@ def can_run_model(model: str) -> None: if is_pytorch_model and is_mac() and not is_arm(): pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.") - if is_pytorch_model and is_py11(): + if is_pytorch_model: pytest.skip("Reinforcement learning / PyTorch currently not available on python 3.11.") From e0c63e12e4ac117638377467d82867b5244add09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 05:15:49 +0000 Subject: [PATCH 180/186] Bump mkdocs-material from 9.1.8 to 9.1.9 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.8 to 9.1.9. - [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.8...9.1.9) --- 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 89ed13549..7aea682ee 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.3 -mkdocs-material==9.1.8 +mkdocs-material==9.1.9 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 jinja2==3.1.2 From 39522322143c89a301336f49a12fa9bdacebeb02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 May 2023 08:59:15 +0200 Subject: [PATCH 181/186] Bump pre-commit requests types --- .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 13216e495..6c3b99de8 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.29.0.0 + - types-requests==2.30.0.0 - types-tabulate==0.9.0.2 - types-python-dateutil==2.8.19.12 - SQLAlchemy==2.0.12 From 9d7c90e9da193618b77b52cdca785f11ef8e41a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 08:11:43 +0000 Subject: [PATCH 182/186] Bump pre-commit from 3.2.2 to 3.3.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.2.2 to 3.3.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.2.2...v3.3.1) --- updated-dependencies: - dependency-name: pre-commit 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 e3c4740e8..29b610868 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.265 mypy==1.2.0 -pre-commit==3.2.2 +pre-commit==3.3.1 pytest==7.3.1 pytest-asyncio==0.21.0 pytest-cov==4.0.0 From 591a51e4bce93240e8b9b08a840c7dfa18b56561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 16:16:45 +0000 Subject: [PATCH 183/186] Bump nbconvert from 7.3.1 to 7.4.0 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.3.1 to 7.4.0. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.3.1...v7.4.0) --- updated-dependencies: - dependency-name: nbconvert 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 ac53306a3..95e7d5bf9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ isort==5.12.0 time-machine==2.9.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.3.1 +nbconvert==7.4.0 # mypy types types-cachetools==5.3.0.5 From 7e31cc4100ba58a6f3301f2dc295b250fc5d51d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 16:17:04 +0000 Subject: [PATCH 184/186] Bump mkdocs-material from 9.1.9 to 9.1.10 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.9 to 9.1.10. - [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.9...9.1.10) --- 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 7aea682ee..de2050335 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.3 -mkdocs-material==9.1.9 +mkdocs-material==9.1.10 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 jinja2==3.1.2 From 3e3945390597533f903a06dd5ffbfd753680845e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 16:17:19 +0000 Subject: [PATCH 185/186] Bump urllib3 from 1.26.15 to 2.0.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.15 to 2.0.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.15...2.0.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 19b4d0082..844ecb41b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ httpx>=0.23.3 arrow==1.2.3 cachetools==4.2.2 requests==2.30.0 -urllib3==1.26.15 +urllib3==2.0.2 jsonschema==4.17.3 TA-Lib==0.4.26 technical==1.4.0 From f2a65437a687dd9595607cf9b2387ed1fb4822c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 16:18:57 +0000 Subject: [PATCH 186/186] Bump ccxt from 3.0.85 to 3.0.97 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.85 to 3.0.97. - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.85...3.0.97) --- 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 19b4d0082..4ac9fe017 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.85 +ccxt==3.0.97 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.12