freqtrade_origin/freqtrade/configuration/configuration.py

515 lines
21 KiB
Python
Raw Normal View History

"""
This module contains the configuration class
"""
2024-05-12 14:29:24 +00:00
2024-06-08 07:31:50 +00:00
import ast
2018-03-25 19:37:14 +00:00
import logging
import warnings
from copy import deepcopy
from pathlib import Path
from typing import Any, Callable, Optional
2018-07-04 07:31:35 +00:00
from freqtrade import constants
2019-10-08 23:37:29 +00:00
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
2020-09-28 17:39:41 +00:00
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
2021-07-31 15:43:10 +00:00
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
2022-04-07 18:13:52 +00:00
from freqtrade.configuration.load_config import load_file, load_from_files
2022-09-18 11:20:36 +00:00
from freqtrade.constants import Config
from freqtrade.enums import (
NON_UTIL_MODES,
TRADE_MODES,
CandleType,
MarginMode,
RunMode,
TradingMode,
)
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging
2021-07-12 12:08:01 +00:00
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
2019-02-19 12:14:47 +00:00
2020-09-28 17:39:41 +00:00
logger = logging.getLogger(__name__)
2018-07-19 19:12:27 +00:00
2019-09-12 01:39:52 +00:00
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: Optional[RunMode] = None) -> None:
self.args = args
2022-09-18 11:20:36 +00:00
self.config: Optional[Config] = None
self.runmode = runmode
2022-09-18 11:20:36 +00:00
def get_config(self) -> Config:
"""
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
"""
2022-04-07 18:13:52 +00:00
# Keep this method as staticmethod, so it can be used from interactive environments
2024-05-12 14:29:24 +00:00
c = Configuration({"config": files}, RunMode.OTHER)
return c.get_config()
def load_config(self) -> dict[str, Any]:
2019-07-15 19:17:57 +00:00
"""
Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary
"""
# Load all configs
2022-09-18 11:20:36 +00:00
config: Config = load_from_files(self.args.get("config", []))
2021-07-31 15:43:10 +00:00
# Load environment variables
2024-01-22 15:26:47 +00:00
from freqtrade.commands.arguments import NO_CONF_ALLOWED
2024-05-12 14:29:24 +00:00
if self.args.get("command") not in NO_CONF_ALLOWED:
2024-01-22 15:26:47 +00:00
env_data = enironment_vars_to_dict()
config = deep_merge_dicts(env_data, config)
2021-07-31 15:43:10 +00:00
# Normalize config
2024-05-12 14:29:24 +00:00
if "internals" not in config:
config["internals"] = {}
2019-07-15 19:17:57 +00:00
2024-05-12 14:29:24 +00:00
if "pairlists" not in config:
config["pairlists"] = []
2019-11-09 13:15:47 +00:00
# Keep a copy of the original configuration file
2024-05-12 14:29:24 +00:00
config["original_config"] = deepcopy(config)
2020-02-27 06:01:00 +00:00
self._process_logging_options(config)
self._process_runmode(config)
self._process_common_options(config)
2019-07-15 19:17:57 +00:00
self._process_trading_options(config)
self._process_optimize_options(config)
2019-07-15 19:17:57 +00:00
self._process_plot_options(config)
2019-07-15 19:17:57 +00:00
self._process_data_options(config)
self._process_analyze_options(config)
self._process_freqai_options(config)
# Import check_exchange here to avoid import cycle problems
from freqtrade.exchange.check_exchange import check_exchange
2019-08-16 12:56:57 +00:00
# Check if the exchange set by the user is supported
2024-05-12 14:29:24 +00:00
check_exchange(config, config.get("experimental", {}).get("block_bad_exchanges", True))
2019-08-16 12:56:57 +00:00
self._resolve_pairs_list(config)
2019-10-08 23:37:29 +00:00
process_temporary_deprecated_settings(config)
2019-07-15 19:17:57 +00:00
return config
2022-09-18 11:20:36 +00:00
def _process_logging_options(self, config: Config) -> None:
"""
2019-05-29 18:57:14 +00:00
Extract information for sys.argv and load logging configuration:
the -v/--verbose, --logfile options
"""
# Log level
2024-05-12 14:29:24 +00:00
config.update({"verbosity": self.args.get("verbosity", 0)})
2019-03-29 19:12:44 +00:00
2024-05-12 14:29:24 +00:00
if "logfile" in self.args and self.args["logfile"]:
config.update({"logfile": self.args["logfile"]})
2019-03-29 19:12:44 +00:00
setup_logging(config)
2022-09-18 11:20:36 +00:00
def _process_trading_options(self, config: Config) -> None:
2024-05-12 14:29:24 +00:00
if config["runmode"] not in TRADE_MODES:
return
2024-05-12 14:29:24 +00:00
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
2024-05-12 14:29:24 +00:00
config["db_url"] = constants.DEFAULT_DB_DRYRUN_URL
else:
2024-05-12 14:29:24 +00:00
if not config.get("db_url"):
config["db_url"] = constants.DEFAULT_DB_PROD_URL
logger.info("Dry run is disabled")
2021-07-12 12:08:01 +00:00
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
2022-09-18 11:20:36 +00:00
def _process_common_options(self, config: Config) -> None:
2019-07-15 19:17:57 +00:00
# Set strategy if not specified in config and or if it's non default
2024-05-12 14:29:24 +00:00
if self.args.get("strategy") or not config.get("strategy"):
config.update({"strategy": self.args.get("strategy")})
2019-07-15 19:17:57 +00:00
2024-05-12 14:29:24 +00:00
self._args_to_config(
config, argname="strategy_path", logstring="Using additional Strategy lookup path: {}"
)
2019-07-15 19:17:57 +00:00
2024-05-12 14:29:24 +00:00
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 ...")
2024-05-12 14:29:24 +00:00
self._args_to_config(
config, argname="db_url_from", logstring="Parameter --db-url-from detected ..."
)
2024-05-12 14:29:24 +00:00
if config.get("force_entry_enable", False):
logger.warning("`force_entry_enable` RPC message enabled.")
2019-07-15 19:17:57 +00:00
# Support for sd_notify
2024-05-12 14:29:24 +00:00
if "sd_notify" in self.args and self.args["sd_notify"]:
config["internals"].update({"sd_notify": True})
2022-09-18 11:20:36 +00:00
def _process_datadir_options(self, config: Config) -> None:
2019-05-29 18:57:14 +00:00
"""
Extract information for sys.argv and load directory configurations
--user-data, --datadir
2019-05-29 18:57:14 +00:00
"""
2019-08-29 04:42:56 +00:00
# Check exchange parameter here - otherwise `datadir` might be wrong.
2024-05-12 14:29:24 +00:00
if "exchange" in self.args and self.args["exchange"]:
config["exchange"]["name"] = self.args["exchange"]
2019-08-29 04:42:56 +00:00
logger.info(f"Using exchange {config['exchange']['name']}")
2024-05-12 14:29:24 +00:00
if "pair_whitelist" not in config["exchange"]:
config["exchange"]["pair_whitelist"] = []
2024-05-12 14:29:24 +00:00
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:
2019-07-21 12:32:29 +00:00
# Default to cwd/user_data (legacy option ...)
2024-05-12 14:29:24 +00:00
config.update({"user_data_dir": str(Path.cwd() / "user_data")})
2019-07-28 13:11:41 +00:00
2019-07-21 12:32:29 +00:00
# reset to user_data_dir so this contains the absolute path.
2024-05-12 14:29:24 +00:00
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"])
2024-05-12 14:29:24 +00:00
config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
logger.info("Using data directory: %s ...", config.get("datadir"))
2019-05-29 18:57:14 +00:00
2024-05-12 14:29:24 +00:00
if self.args.get("exportfilename"):
self._args_to_config(
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
)
config["exportfilename"] = Path(config["exportfilename"])
else:
2024-05-12 14:29:24 +00:00
config["exportfilename"] = config["user_data_dir"] / "backtest_results"
2024-05-12 14:29:24 +00:00
if self.args.get("show_sensitive"):
logger.warning(
2024-04-18 20:51:25 +00:00
"Sensitive information will be shown in the upcoming output. "
"Please make sure to never share this output without redacting "
2024-05-12 14:29:24 +00:00
"the information yourself."
)
2022-09-18 11:20:36 +00:00
def _process_optimize_options(self, config: Config) -> None:
# This will override the strategy configuration
2024-05-12 14:29:24 +00:00
self._args_to_config(
config,
argname="timeframe",
2024-05-12 15:51:21 +00:00
logstring="Parameter -i/--timeframe detected ... Using timeframe: {} ...",
2024-05-12 14:29:24 +00:00
)
2024-05-12 14:29:24 +00:00
self._args_to_config(
config,
argname="position_stacking",
logstring="Parameter --enable-position-stacking detected ...",
)
self._args_to_config(
2024-05-12 14:29:24 +00:00
config,
argname="enable_protections",
logstring="Parameter --enable-protections detected, enabling Protections. ...",
)
2024-11-02 15:49:26 +00:00
if "max_open_trades" in self.args and self.args["max_open_trades"]:
2024-05-12 14:29:24 +00:00
config.update({"max_open_trades": self.args["max_open_trades"]})
logger.info(
2024-05-12 15:51:21 +00:00
"Parameter --max-open-trades detected, overriding max_open_trades to: %s ...",
2024-05-12 14:29:24 +00:00
config.get("max_open_trades"),
)
elif config["runmode"] in NON_UTIL_MODES:
logger.info("Using max_open_trades: %s ...", config.get("max_open_trades"))
2021-02-26 18:48:06 +00:00
# Setting max_open_trades to infinite if -1
2024-05-12 14:29:24 +00:00
if config.get("max_open_trades") == -1:
config["max_open_trades"] = float("inf")
2024-05-12 14:29:24 +00:00
if self.args.get("stake_amount"):
2021-02-25 19:14:33 +00:00
# Convert explicitly to float to support CLI argument for both unlimited and value
try:
2024-05-12 14:29:24 +00:00
self.args["stake_amount"] = float(self.args["stake_amount"])
2021-02-25 19:14:33 +00:00
except ValueError:
pass
2024-01-21 18:56:00 +00:00
configurations = [
2024-05-12 14:29:24 +00:00
(
"timeframe_detail",
"Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...",
),
("backtest_show_pair_list", "Parameter --show-pair-list detected."),
(
"stake_amount",
"Parameter --stake-amount detected, overriding stake_amount to: {} ...",
),
(
"dry_run_wallet",
"Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...",
),
("fee", "Parameter --fee detected, setting fee to: {} ..."),
("timerange", "Parameter --timerange detected: {} ..."),
]
2024-01-21 18:56:00 +00:00
self._args_to_config_loop(config, configurations)
self._process_datadir_options(config)
2024-05-12 14:29:24 +00:00
self._args_to_config(
config,
argname="strategy_list",
logstring="Using strategy list of {} strategies",
logfun=len,
)
2018-07-27 21:00:50 +00:00
2024-01-21 18:56:00 +00:00
configurations = [
2024-05-12 14:29:24 +00:00
(
"recursive_strategy_search",
"Recursively searching for a strategy in the strategies folder.",
),
("timeframe", "Overriding timeframe with Command line argument"),
("export", "Parameter --export detected: {} ..."),
("backtest_breakdown", "Parameter --breakdown detected ..."),
("backtest_cache", "Parameter --cache={} detected ..."),
("disableparamexport", "Parameter --disableparamexport detected: {} ..."),
("freqai_backtest_live_models", "Parameter --freqai-backtest-live-models detected ..."),
2024-01-21 18:56:00 +00:00
]
self._args_to_config_loop(config, configurations)
# Edge section:
2024-05-12 14:29:24 +00:00
if "stoploss_range" in self.args and self.args["stoploss_range"]:
2024-06-08 07:31:50 +00:00
txt_range = ast.literal_eval(self.args["stoploss_range"])
2024-05-12 14:29:24 +00:00
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"])
2018-11-14 15:31:23 +00:00
# Hyperopt section
2024-01-21 18:56:00 +00:00
configurations = [
2024-05-12 14:29:24 +00:00
("hyperopt", "Using Hyperopt class name: {}"),
("hyperopt_path", "Using additional Hyperopt lookup path: {}"),
("hyperoptexportfilename", "Using hyperopt file: {}"),
("lookahead_analysis_exportfilename", "Saving lookahead analysis results into {} ..."),
("epochs", "Parameter --epochs detected ... Will run Hyperopt with for {} epochs ..."),
("spaces", "Parameter -s/--spaces detected: {}"),
("analyze_per_epoch", "Parameter --analyze-per-epoch detected."),
("print_all", "Parameter --print-all detected ..."),
2024-01-21 18:56:00 +00:00
]
self._args_to_config_loop(config, configurations)
2024-05-12 14:29:24 +00:00
if "print_colorized" in self.args and not self.args["print_colorized"]:
logger.info("Parameter --no-color detected ...")
config.update({"print_colorized": False})
else:
2024-05-12 14:29:24 +00:00
config.update({"print_colorized": True})
2019-08-03 16:09:42 +00:00
2024-01-21 18:56:00 +00:00
configurations = [
2024-05-12 14:29:24 +00:00
("print_json", "Parameter --print-json detected ..."),
("export_csv", "Parameter --export-csv detected: {}"),
("hyperopt_jobs", "Parameter -j/--job-workers detected: {}"),
("hyperopt_random_state", "Parameter --random-state detected: {}"),
("hyperopt_min_trades", "Parameter --min-trades detected: {}"),
("hyperopt_loss", "Using Hyperopt loss class name: {}"),
("hyperopt_show_index", "Parameter -n/--index detected: {}"),
("hyperopt_list_best", "Parameter --best detected: {}"),
("hyperopt_list_profitable", "Parameter --profitable detected: {}"),
("hyperopt_list_min_trades", "Parameter --min-trades detected: {}"),
("hyperopt_list_max_trades", "Parameter --max-trades detected: {}"),
("hyperopt_list_min_avg_time", "Parameter --min-avg-time detected: {}"),
("hyperopt_list_max_avg_time", "Parameter --max-avg-time detected: {}"),
("hyperopt_list_min_avg_profit", "Parameter --min-avg-profit detected: {}"),
("hyperopt_list_max_avg_profit", "Parameter --max-avg-profit detected: {}"),
("hyperopt_list_min_total_profit", "Parameter --min-total-profit detected: {}"),
("hyperopt_list_max_total_profit", "Parameter --max-total-profit detected: {}"),
("hyperopt_list_min_objective", "Parameter --min-objective detected: {}"),
("hyperopt_list_max_objective", "Parameter --max-objective detected: {}"),
("hyperopt_list_no_details", "Parameter --no-details detected: {}"),
("hyperopt_show_no_header", "Parameter --no-header detected: {}"),
("hyperopt_ignore_missing_space", "Parameter --ignore-missing-space detected: {}"),
2024-01-21 18:56:00 +00:00
]
self._args_to_config_loop(config, configurations)
2022-09-18 11:20:36 +00:00
def _process_plot_options(self, config: Config) -> None:
2024-01-21 18:56:00 +00:00
configurations = [
2024-05-12 14:29:24 +00:00
("pairs", "Using pairs {}"),
("indicators1", "Using indicators1: {}"),
("indicators2", "Using indicators2: {}"),
("trade_ids", "Filtering on trade_ids: {}"),
("plot_limit", "Limiting plot to: {}"),
("plot_auto_open", "Parameter --auto-open detected."),
("trade_source", "Using trades from: {}"),
("prepend_data", "Prepend detected. Allowing data prepending."),
("erase", "Erase detected. Deleting existing data."),
("no_trades", "Parameter --no-trades detected."),
("timeframes", "timeframes --timeframes: {}"),
("days", "Detected --days: {}"),
("include_inactive", "Detected --include-inactive-pairs: {}"),
("download_trades", "Detected --dl-trades: {}"),
2024-05-18 18:20:58 +00:00
("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
2024-05-12 14:29:24 +00:00
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
("dataformat_trades", 'Using "{}" to store trades data.'),
("show_timerange", "Detected --show-timerange"),
2024-01-21 18:56:00 +00:00
]
self._args_to_config_loop(config, configurations)
2022-08-19 11:44:31 +00:00
2022-09-18 11:20:36 +00:00
def _process_data_options(self, config: Config) -> None:
2024-05-12 14:29:24 +00:00
self._args_to_config(
config, argname="new_pairs_days", logstring="Detected --new-pairs-days: {}"
)
self._args_to_config(
config, argname="trading_mode", logstring="Detected --trading-mode: {}"
)
config["candle_type_def"] = CandleType.get_default(
config.get("trading_mode", "spot") or "spot"
)
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
config["margin_mode"] = MarginMode(config.get("margin_mode", "") or "")
2024-05-12 14:29:24 +00:00
self._args_to_config(
config, argname="candle_types", logstring="Detected --candle-types: {}"
)
2022-09-18 11:20:36 +00:00
def _process_analyze_options(self, config: Config) -> None:
2024-01-21 18:56:00 +00:00
configurations = [
2024-05-12 14:29:24 +00:00
("analysis_groups", "Analysis reason groups: {}"),
("enter_reason_list", "Analysis enter tag list: {}"),
("exit_reason_list", "Analysis exit tag list: {}"),
("indicator_list", "Analysis indicator list: {}"),
("entry_only", "Only analyze entry signals: {}"),
("exit_only", "Only analyze exit signals: {}"),
2024-05-12 14:29:24 +00:00
("timerange", "Filter trades by timerange: {}"),
("analysis_rejected", "Analyse rejected signals: {}"),
("analysis_to_csv", "Store analysis tables to CSV: {}"),
("analysis_csv_path", "Path to store analysis CSVs: {}"),
2024-01-21 18:56:00 +00:00
# Lookahead analysis results
2024-05-12 14:29:24 +00:00
("targeted_trade_amount", "Targeted Trade amount: {}"),
("minimum_trade_amount", "Minimum Trade amount: {}"),
("lookahead_analysis_exportfilename", "Path to store lookahead-analysis-results: {}"),
("startup_candle", "Startup candle to be used on recursive analysis: {}"),
2024-01-21 18:56:00 +00:00
]
self._args_to_config_loop(config, configurations)
def _args_to_config_loop(self, config, configurations: list[tuple[str, str]]) -> None:
2024-01-21 18:56:00 +00:00
for argname, logstring in configurations:
self._args_to_config(config, argname=argname, logstring=logstring)
2023-09-12 10:50:39 +00:00
2022-09-18 11:20:36 +00:00
def _process_runmode(self, config: Config) -> None:
2024-05-12 14:29:24 +00:00
self._args_to_config(
config,
argname="dry_run",
2024-05-12 15:51:21 +00:00
logstring="Parameter --dry-run detected, overriding dry_run to: {} ...",
2024-05-12 14:29:24 +00:00
)
2019-07-15 19:17:57 +00:00
if not self.runmode:
# Handle real mode, infer dry/live from config
2024-05-12 14:29:24 +00:00
self.runmode = RunMode.DRY_RUN if config.get("dry_run", True) else RunMode.LIVE
2020-02-27 06:01:00 +00:00
logger.info(f"Runmode set to {self.runmode.value}.")
2019-07-15 19:17:57 +00:00
2024-05-12 14:29:24 +00:00
config.update({"runmode": self.runmode})
2019-06-16 11:31:24 +00:00
2022-09-18 11:20:36 +00:00
def _process_freqai_options(self, config: Config) -> None:
2024-05-12 14:29:24 +00:00
self._args_to_config(
config, argname="freqaimodel", logstring="Using freqaimodel class name: {}"
)
2024-05-12 14:29:24 +00:00
self._args_to_config(
config, argname="freqaimodel_path", logstring="Using freqaimodel path: {}"
)
return
2024-05-12 14:29:24 +00:00
def _args_to_config(
self,
config: Config,
argname: str,
logstring: str,
logfun: Optional[Callable] = None,
deprecated_msg: Optional[str] = None,
) -> None:
2019-07-15 19:17:57 +00:00
"""
: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)
"""
2024-05-12 14:29:24 +00:00
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]})
2019-07-15 19:17:57 +00:00
if logfun:
logger.info(logstring.format(logfun(config[argname])))
else:
logger.info(logstring.format(config[argname]))
if deprecated_msg:
2024-09-01 06:32:42 +00:00
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning, stacklevel=1)
2019-08-16 12:56:57 +00:00
2022-09-18 11:20:36 +00:00
def _resolve_pairs_list(self, config: Config) -> None:
2019-08-16 12:56:57 +00:00
"""
Helper for download script.
Takes first found:
* -p (pairs argument)
* --pairs-file
* whitelist from config
"""
if "pairs" in config:
2024-05-12 14:29:24 +00:00
config["exchange"]["pair_whitelist"] = config["pairs"]
2019-08-16 12:56:57 +00:00
return
if "pairs_file" in self.args and self.args["pairs_file"]:
pairs_file = Path(self.args["pairs_file"])
2019-08-16 12:56:57 +00:00
logger.info(f'Reading pairs file "{pairs_file}".')
# Download pairs from the pairs file if no config is specified
2021-06-25 13:45:49 +00:00
# or if pairs file is specified explicitly
2019-08-16 12:56:57 +00:00
if not pairs_file.exists():
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
2024-05-12 14:29:24 +00:00
config["pairs"] = load_file(pairs_file)
if isinstance(config["pairs"], list):
config["pairs"].sort()
return
2019-08-16 12:56:57 +00:00
2024-05-12 14:29:24 +00:00
if "config" in self.args and self.args["config"]:
2019-08-16 12:56:57 +00:00
logger.info("Using pairlist from configuration.")
2024-05-12 14:29:24 +00:00
config["pairs"] = config.get("exchange", {}).get("pair_whitelist")
else:
# Fall back to /dl_path/pairs.json
2024-05-12 14:29:24 +00:00
pairs_file = config["datadir"] / "pairs.json"
if pairs_file.exists():
2023-06-19 16:29:37 +00:00
logger.info(f'Reading pairs file "{pairs_file}".')
2024-05-12 14:29:24 +00:00
config["pairs"] = load_file(pairs_file)
if "pairs" in config and isinstance(config["pairs"], list):
config["pairs"].sort()