Delete configuration.py
This commit is contained in:
		| @@ -1,506 +0,0 @@ | ||||
| """ | ||||
| This module contains the configuration class | ||||
| """ | ||||
| import logging | ||||
| import warnings | ||||
| from copy import deepcopy | ||||
| from pathlib import Path | ||||
| from typing import Any, Callable, Dict, List, Optional | ||||
|  | ||||
| from freqtrade import constants | ||||
| from freqtrade.configuration.check_exchange import check_exchange | ||||
| from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings | ||||
| from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir | ||||
| from freqtrade.configuration.environment_vars import enironment_vars_to_dict | ||||
| from freqtrade.configuration.load_config import load_config_file, load_file | ||||
| from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode | ||||
| from freqtrade.exceptions import OperationalException | ||||
| from freqtrade.loggers import setup_logging | ||||
| from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Configuration: | ||||
|     """ | ||||
|     Class to read and init the bot configuration | ||||
|     Reuse this class for the bot, backtesting, hyperopt and every script that required configuration | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None: | ||||
|         self.args = args | ||||
|         self.config: Optional[Dict[str, Any]] = None | ||||
|         self.runmode = runmode | ||||
|  | ||||
|     def get_config(self) -> Dict[str, Any]: | ||||
|         """ | ||||
|         Return the config. Use this method to get the bot config | ||||
|         :return: Dict: Bot config | ||||
|         """ | ||||
|         if self.config is None: | ||||
|             self.config = self.load_config() | ||||
|  | ||||
|         return self.config | ||||
|  | ||||
|     @staticmethod | ||||
|     def from_files(files: List[str]) -> Dict[str, Any]: | ||||
|         """ | ||||
|         Iterate through the config files passed in, loading all of them | ||||
|         and merging their contents. | ||||
|         Files are loaded in sequence, parameters in later configuration files | ||||
|         override the same parameter from an earlier file (last definition wins). | ||||
|         Runs through the whole Configuration initialization, so all expected config entries | ||||
|         are available to interactive environments. | ||||
|         :param files: List of file paths | ||||
|         :return: configuration dictionary | ||||
|         """ | ||||
|         c = Configuration({'config': files}, RunMode.OTHER) | ||||
|         return c.get_config() | ||||
|  | ||||
|     def load_from_files(self, files: List[str]) -> Dict[str, Any]: | ||||
|  | ||||
|         # Keep this method as staticmethod, so it can be used from interactive environments | ||||
|         config: Dict[str, Any] = {} | ||||
|  | ||||
|         if not files: | ||||
|             return deepcopy(constants.MINIMAL_CONFIG) | ||||
|  | ||||
|         # We expect here a list of config filenames | ||||
|         for path in files: | ||||
|             logger.info(f'Using config: {path} ...') | ||||
|  | ||||
|             # Merge config options, overwriting old values | ||||
|             config = deep_merge_dicts(load_config_file(path), config) | ||||
|  | ||||
|         # Load environment variables | ||||
|         env_data = enironment_vars_to_dict() | ||||
|         config = deep_merge_dicts(env_data, config) | ||||
|  | ||||
|         config['config_files'] = files | ||||
|         # Normalize config | ||||
|         if 'internals' not in config: | ||||
|             config['internals'] = {} | ||||
|         if 'ask_strategy' not in config: | ||||
|             config['ask_strategy'] = {} | ||||
|  | ||||
|         if 'pairlists' not in config: | ||||
|             config['pairlists'] = [] | ||||
|  | ||||
|         return config | ||||
|  | ||||
|     def load_config(self) -> Dict[str, Any]: | ||||
|         """ | ||||
|         Extract information for sys.argv and load the bot configuration | ||||
|         :return: Configuration dictionary | ||||
|         """ | ||||
|         # Load all configs | ||||
|         config: Dict[str, Any] = self.load_from_files(self.args.get("config", [])) | ||||
|  | ||||
|         # Keep a copy of the original configuration file | ||||
|         config['original_config'] = deepcopy(config) | ||||
|  | ||||
|         self._process_logging_options(config) | ||||
|  | ||||
|         self._process_runmode(config) | ||||
|  | ||||
|         self._process_common_options(config) | ||||
|  | ||||
|         self._process_trading_options(config) | ||||
|  | ||||
|         self._process_optimize_options(config) | ||||
|  | ||||
|         self._process_plot_options(config) | ||||
|  | ||||
|         self._process_data_options(config) | ||||
|  | ||||
|         # Check if the exchange set by the user is supported | ||||
|         check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) | ||||
|  | ||||
|         self._resolve_pairs_list(config) | ||||
|  | ||||
|         process_temporary_deprecated_settings(config) | ||||
|  | ||||
|         return config | ||||
|  | ||||
|     def _process_logging_options(self, config: Dict[str, Any]) -> None: | ||||
|         """ | ||||
|         Extract information for sys.argv and load logging configuration: | ||||
|         the -v/--verbose, --logfile options | ||||
|         """ | ||||
|         # Log level | ||||
|         config.update({'verbosity': self.args.get('verbosity', 0)}) | ||||
|  | ||||
|         if 'logfile' in self.args and self.args['logfile']: | ||||
|             config.update({'logfile': self.args['logfile']}) | ||||
|  | ||||
|         setup_logging(config) | ||||
|  | ||||
|     def _process_trading_options(self, config: Dict[str, Any]) -> None: | ||||
|  | ||||
|         # Allow_position_stacking defaults to False | ||||
|         if not config.get('allow_position_stacking'): | ||||
|             config['allow_position_stacking'] = False | ||||
|         logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) | ||||
|  | ||||
|         if config['runmode'] not in TRADING_MODES: | ||||
|             return | ||||
|  | ||||
|         if config.get('dry_run', False): | ||||
|             logger.info('Dry run is enabled') | ||||
|             if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: | ||||
|                 # Default to in-memory db for dry_run if not specified | ||||
|                 config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL | ||||
|         else: | ||||
|             if not config.get('db_url', None): | ||||
|                 config['db_url'] = constants.DEFAULT_DB_PROD_URL | ||||
|             logger.info('Dry run is disabled') | ||||
|  | ||||
|         logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') | ||||
|  | ||||
|     def _process_common_options(self, config: Dict[str, Any]) -> None: | ||||
|  | ||||
|         # Set strategy if not specified in config and or if it's non default | ||||
|         if self.args.get('strategy') or not config.get('strategy'): | ||||
|             config.update({'strategy': self.args.get('strategy')}) | ||||
|  | ||||
|         self._args_to_config(config, argname='strategy_path', | ||||
|                              logstring='Using additional Strategy lookup path: {}') | ||||
|  | ||||
|         if ('db_url' in self.args and self.args['db_url'] and | ||||
|                 self.args['db_url'] != constants.DEFAULT_DB_PROD_URL): | ||||
|             config.update({'db_url': self.args['db_url']}) | ||||
|             logger.info('Parameter --db-url detected ...') | ||||
|  | ||||
|         if config.get('forcebuy_enable', False): | ||||
|             logger.warning('`forcebuy` RPC message enabled.') | ||||
|  | ||||
|         # Support for sd_notify | ||||
|         if 'sd_notify' in self.args and self.args['sd_notify']: | ||||
|             config['internals'].update({'sd_notify': True}) | ||||
|  | ||||
|     def _process_datadir_options(self, config: Dict[str, Any]) -> None: | ||||
|         """ | ||||
|         Extract information for sys.argv and load directory configurations | ||||
|         --user-data, --datadir | ||||
|         """ | ||||
|         # Check exchange parameter here - otherwise `datadir` might be wrong. | ||||
|         if 'exchange' in self.args and self.args['exchange']: | ||||
|             config['exchange']['name'] = self.args['exchange'] | ||||
|             logger.info(f"Using exchange {config['exchange']['name']}") | ||||
|  | ||||
|         if 'pair_whitelist' not in config['exchange']: | ||||
|             config['exchange']['pair_whitelist'] = [] | ||||
|  | ||||
|         if 'user_data_dir' in self.args and self.args['user_data_dir']: | ||||
|             config.update({'user_data_dir': self.args['user_data_dir']}) | ||||
|         elif 'user_data_dir' not in config: | ||||
|             # Default to cwd/user_data (legacy option ...) | ||||
|             config.update({'user_data_dir': str(Path.cwd() / 'user_data')}) | ||||
|  | ||||
|         # reset to user_data_dir so this contains the absolute path. | ||||
|         config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) | ||||
|         logger.info('Using user-data directory: %s ...', config['user_data_dir']) | ||||
|  | ||||
|         config.update({'datadir': create_datadir(config, self.args.get('datadir', None))}) | ||||
|         logger.info('Using data directory: %s ...', config.get('datadir')) | ||||
|  | ||||
|         if self.args.get('exportfilename'): | ||||
|             self._args_to_config(config, argname='exportfilename', | ||||
|                                  logstring='Storing backtest results to {} ...') | ||||
|             config['exportfilename'] = Path(config['exportfilename']) | ||||
|         else: | ||||
|             config['exportfilename'] = (config['user_data_dir'] | ||||
|                                         / 'backtest_results') | ||||
|  | ||||
|     def _process_optimize_options(self, config: Dict[str, Any]) -> None: | ||||
|  | ||||
|         # This will override the strategy configuration | ||||
|         self._args_to_config(config, argname='timeframe', | ||||
|                              logstring='Parameter -i/--timeframe detected ... ' | ||||
|                              'Using timeframe: {} ...') | ||||
|  | ||||
|         self._args_to_config(config, argname='position_stacking', | ||||
|                              logstring='Parameter --enable-position-stacking detected ...') | ||||
|  | ||||
|         self._args_to_config( | ||||
|             config, argname='enable_protections', | ||||
|             logstring='Parameter --enable-protections detected, enabling Protections. ...') | ||||
|  | ||||
|         if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: | ||||
|             config.update({'use_max_market_positions': False}) | ||||
|             logger.info('Parameter --disable-max-market-positions detected ...') | ||||
|             logger.info('max_open_trades set to unlimited ...') | ||||
|         elif 'max_open_trades' in self.args and self.args['max_open_trades']: | ||||
|             config.update({'max_open_trades': self.args['max_open_trades']}) | ||||
|             logger.info('Parameter --max-open-trades detected, ' | ||||
|                         'overriding max_open_trades to: %s ...', config.get('max_open_trades')) | ||||
|         elif config['runmode'] in NON_UTIL_MODES: | ||||
|             logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) | ||||
|         # Setting max_open_trades to infinite if -1 | ||||
|         if config.get('max_open_trades') == -1: | ||||
|             config['max_open_trades'] = float('inf') | ||||
|  | ||||
|         if self.args.get('stake_amount', None): | ||||
|             # Convert explicitly to float to support CLI argument for both unlimited and value | ||||
|             try: | ||||
|                 self.args['stake_amount'] = float(self.args['stake_amount']) | ||||
|             except ValueError: | ||||
|                 pass | ||||
|  | ||||
|         self._args_to_config(config, argname='timeframe_detail', | ||||
|                              logstring='Parameter --timeframe-detail detected, ' | ||||
|                              'using {} for intra-candle backtesting ...') | ||||
|         self._args_to_config(config, argname='stake_amount', | ||||
|                              logstring='Parameter --stake-amount detected, ' | ||||
|                              'overriding stake_amount to: {} ...') | ||||
|         self._args_to_config(config, argname='dry_run_wallet', | ||||
|                              logstring='Parameter --dry-run-wallet detected, ' | ||||
|                              'overriding dry_run_wallet to: {} ...') | ||||
|         self._args_to_config(config, argname='fee', | ||||
|                              logstring='Parameter --fee detected, ' | ||||
|                              'setting fee to: {} ...') | ||||
|  | ||||
|         self._args_to_config(config, argname='timerange', | ||||
|                              logstring='Parameter --timerange detected: {} ...') | ||||
|  | ||||
|         self._process_datadir_options(config) | ||||
|  | ||||
|         self._args_to_config(config, argname='strategy_list', | ||||
|                              logstring='Using strategy list of {} strategies', logfun=len) | ||||
|  | ||||
|         self._args_to_config(config, argname='timeframe', | ||||
|                              logstring='Overriding timeframe with Command line argument') | ||||
|  | ||||
|         self._args_to_config(config, argname='export', | ||||
|                              logstring='Parameter --export detected: {} ...') | ||||
|  | ||||
|         self._args_to_config(config, argname='backtest_breakdown', | ||||
|                              logstring='Parameter --breakdown detected ...') | ||||
|  | ||||
|         self._args_to_config(config, argname='disableparamexport', | ||||
|                              logstring='Parameter --disableparamexport detected: {} ...') | ||||
|  | ||||
|         # Edge section: | ||||
|         if 'stoploss_range' in self.args and self.args["stoploss_range"]: | ||||
|             txt_range = eval(self.args["stoploss_range"]) | ||||
|             config['edge'].update({'stoploss_range_min': txt_range[0]}) | ||||
|             config['edge'].update({'stoploss_range_max': txt_range[1]}) | ||||
|             config['edge'].update({'stoploss_range_step': txt_range[2]}) | ||||
|             logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) | ||||
|  | ||||
|         # Hyperopt section | ||||
|         self._args_to_config(config, argname='hyperopt', | ||||
|                              logstring='Using Hyperopt class name: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_path', | ||||
|                              logstring='Using additional Hyperopt lookup path: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperoptexportfilename', | ||||
|                              logstring='Using hyperopt file: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='epochs', | ||||
|                              logstring='Parameter --epochs detected ... ' | ||||
|                              'Will run Hyperopt with for {} epochs ...' | ||||
|                              ) | ||||
|  | ||||
|         self._args_to_config(config, argname='spaces', | ||||
|                              logstring='Parameter -s/--spaces detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='print_all', | ||||
|                              logstring='Parameter --print-all detected ...') | ||||
|  | ||||
|         if 'print_colorized' in self.args and not self.args["print_colorized"]: | ||||
|             logger.info('Parameter --no-color detected ...') | ||||
|             config.update({'print_colorized': False}) | ||||
|         else: | ||||
|             config.update({'print_colorized': True}) | ||||
|  | ||||
|         self._args_to_config(config, argname='print_json', | ||||
|                              logstring='Parameter --print-json detected ...') | ||||
|  | ||||
|         self._args_to_config(config, argname='export_csv', | ||||
|                              logstring='Parameter --export-csv detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_jobs', | ||||
|                              logstring='Parameter -j/--job-workers detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_random_state', | ||||
|                              logstring='Parameter --random-state detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_min_trades', | ||||
|                              logstring='Parameter --min-trades detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_loss', | ||||
|                              logstring='Using Hyperopt loss class name: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_show_index', | ||||
|                              logstring='Parameter -n/--index detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_best', | ||||
|                              logstring='Parameter --best detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_profitable', | ||||
|                              logstring='Parameter --profitable detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_min_trades', | ||||
|                              logstring='Parameter --min-trades detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_max_trades', | ||||
|                              logstring='Parameter --max-trades detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_min_avg_time', | ||||
|                              logstring='Parameter --min-avg-time detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_max_avg_time', | ||||
|                              logstring='Parameter --max-avg-time detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_min_avg_profit', | ||||
|                              logstring='Parameter --min-avg-profit detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_max_avg_profit', | ||||
|                              logstring='Parameter --max-avg-profit detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_min_total_profit', | ||||
|                              logstring='Parameter --min-total-profit detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_max_total_profit', | ||||
|                              logstring='Parameter --max-total-profit detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_min_objective', | ||||
|                              logstring='Parameter --min-objective detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_max_objective', | ||||
|                              logstring='Parameter --max-objective detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_list_no_details', | ||||
|                              logstring='Parameter --no-details detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='hyperopt_show_no_header', | ||||
|                              logstring='Parameter --no-header detected: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname="hyperopt_ignore_missing_space", | ||||
|                              logstring="Paramter --ignore-missing-space detected: {}") | ||||
|  | ||||
|     def _process_plot_options(self, config: Dict[str, Any]) -> None: | ||||
|  | ||||
|         self._args_to_config(config, argname='pairs', | ||||
|                              logstring='Using pairs {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='indicators1', | ||||
|                              logstring='Using indicators1: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='indicators2', | ||||
|                              logstring='Using indicators2: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='trade_ids', | ||||
|                              logstring='Filtering on trade_ids: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='plot_limit', | ||||
|                              logstring='Limiting plot to: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='plot_auto_open', | ||||
|                              logstring='Parameter --auto-open detected.') | ||||
|  | ||||
|         self._args_to_config(config, argname='trade_source', | ||||
|                              logstring='Using trades from: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='erase', | ||||
|                              logstring='Erase detected. Deleting existing data.') | ||||
|  | ||||
|         self._args_to_config(config, argname='no_trades', | ||||
|                              logstring='Parameter --no-trades detected.') | ||||
|  | ||||
|         self._args_to_config(config, argname='timeframes', | ||||
|                              logstring='timeframes --timeframes: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='days', | ||||
|                              logstring='Detected --days: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='include_inactive', | ||||
|                              logstring='Detected --include-inactive-pairs: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='download_trades', | ||||
|                              logstring='Detected --dl-trades: {}') | ||||
|  | ||||
|         self._args_to_config(config, argname='dataformat_ohlcv', | ||||
|                              logstring='Using "{}" to store OHLCV data.') | ||||
|  | ||||
|         self._args_to_config(config, argname='dataformat_trades', | ||||
|                              logstring='Using "{}" to store trades data.') | ||||
|  | ||||
|     def _process_data_options(self, config: Dict[str, Any]) -> None: | ||||
|  | ||||
|         self._args_to_config(config, argname='new_pairs_days', | ||||
|                              logstring='Detected --new-pairs-days: {}') | ||||
|  | ||||
|     def _process_runmode(self, config: Dict[str, Any]) -> None: | ||||
|  | ||||
|         self._args_to_config(config, argname='dry_run', | ||||
|                              logstring='Parameter --dry-run detected, ' | ||||
|                              'overriding dry_run to: {} ...') | ||||
|  | ||||
|         if not self.runmode: | ||||
|             # Handle real mode, infer dry/live from config | ||||
|             self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE | ||||
|             logger.info(f"Runmode set to {self.runmode.value}.") | ||||
|  | ||||
|         config.update({'runmode': self.runmode}) | ||||
|  | ||||
|     def _args_to_config(self, config: Dict[str, Any], argname: str, | ||||
|                         logstring: str, logfun: Optional[Callable] = None, | ||||
|                         deprecated_msg: Optional[str] = None) -> None: | ||||
|         """ | ||||
|         :param config: Configuration dictionary | ||||
|         :param argname: Argumentname in self.args - will be copied to config dict. | ||||
|         :param logstring: Logging String | ||||
|         :param logfun: logfun is applied to the configuration entry before passing | ||||
|                         that entry to the log string using .format(). | ||||
|                         sample: logfun=len (prints the length of the found | ||||
|                         configuration instead of the content) | ||||
|         """ | ||||
|         if (argname in self.args and self.args[argname] is not None | ||||
|            and self.args[argname] is not False): | ||||
|  | ||||
|             config.update({argname: self.args[argname]}) | ||||
|             if logfun: | ||||
|                 logger.info(logstring.format(logfun(config[argname]))) | ||||
|             else: | ||||
|                 logger.info(logstring.format(config[argname])) | ||||
|             if deprecated_msg: | ||||
|                 warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) | ||||
|  | ||||
|     def _resolve_pairs_list(self, config: Dict[str, Any]) -> None: | ||||
|         """ | ||||
|         Helper for download script. | ||||
|         Takes first found: | ||||
|         * -p (pairs argument) | ||||
|         * --pairs-file | ||||
|         * whitelist from config | ||||
|         """ | ||||
|  | ||||
|         if "pairs" in config: | ||||
|             config['exchange']['pair_whitelist'] = config['pairs'] | ||||
|             return | ||||
|  | ||||
|         if "pairs_file" in self.args and self.args["pairs_file"]: | ||||
|             pairs_file = Path(self.args["pairs_file"]) | ||||
|             logger.info(f'Reading pairs file "{pairs_file}".') | ||||
|             # Download pairs from the pairs file if no config is specified | ||||
|             # or if pairs file is specified explicitly | ||||
|             if not pairs_file.exists(): | ||||
|                 raise OperationalException(f'No pairs file found with path "{pairs_file}".') | ||||
|             config['pairs'] = load_file(pairs_file) | ||||
|             config['pairs'].sort() | ||||
|             return | ||||
|  | ||||
|         if 'config' in self.args and self.args['config']: | ||||
|             logger.info("Using pairlist from configuration.") | ||||
|             config['pairs'] = config.get('exchange', {}).get('pair_whitelist') | ||||
|         else: | ||||
|             # Fall back to /dl_path/pairs.json | ||||
|             pairs_file = config['datadir'] / 'pairs.json' | ||||
|             if pairs_file.exists(): | ||||
|                 config['pairs'] = load_file(pairs_file) | ||||
|                 if 'pairs' in config: | ||||
|                     config['pairs'].sort() | ||||
		Reference in New Issue
	
	Block a user