• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.98
/freqtrade/configuration/configuration.py
1
"""
2
This module contains the configuration class
3
"""
4
import logging
1✔
5
import warnings
1✔
6
from copy import deepcopy
1✔
7
from pathlib import Path
1✔
8
from typing import Any, Callable, Dict, List, Optional, Tuple
1✔
9

10
from freqtrade import constants
1✔
11
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
1✔
12
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
1✔
13
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
1✔
14
from freqtrade.configuration.load_config import load_file, load_from_files
1✔
15
from freqtrade.constants import Config
1✔
16
from freqtrade.enums import NON_UTIL_MODES, TRADE_MODES, CandleType, RunMode, TradingMode
1✔
17
from freqtrade.exceptions import OperationalException
1✔
18
from freqtrade.loggers import setup_logging
1✔
19
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
1✔
20

21

22
logger = logging.getLogger(__name__)
1✔
23

24

25
class Configuration:
1✔
26
    """
27
    Class to read and init the bot configuration
28
    Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
29
    """
30

31
    def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None:
1✔
32
        self.args = args
1✔
33
        self.config: Optional[Config] = None
1✔
34
        self.runmode = runmode
1✔
35

36
    def get_config(self) -> Config:
1✔
37
        """
38
        Return the config. Use this method to get the bot config
39
        :return: Dict: Bot config
40
        """
41
        if self.config is None:
1✔
42
            self.config = self.load_config()
1✔
43

44
        return self.config
1✔
45

46
    @staticmethod
1✔
47
    def from_files(files: List[str]) -> Dict[str, Any]:
1✔
48
        """
49
        Iterate through the config files passed in, loading all of them
50
        and merging their contents.
51
        Files are loaded in sequence, parameters in later configuration files
52
        override the same parameter from an earlier file (last definition wins).
53
        Runs through the whole Configuration initialization, so all expected config entries
54
        are available to interactive environments.
55
        :param files: List of file paths
56
        :return: configuration dictionary
57
        """
58
        # Keep this method as staticmethod, so it can be used from interactive environments
59
        c = Configuration({'config': files}, RunMode.OTHER)
1✔
60
        return c.get_config()
1✔
61

62
    def load_config(self) -> Dict[str, Any]:
1✔
63
        """
64
        Extract information for sys.argv and load the bot configuration
65
        :return: Configuration dictionary
66
        """
67
        # Load all configs
68
        config: Config = load_from_files(self.args.get("config", []))
1✔
69

70
        # Load environment variables
71
        from freqtrade.commands.arguments import NO_CONF_ALLOWED
1✔
72
        if self.args.get('command') not in NO_CONF_ALLOWED:
1✔
73
            env_data = enironment_vars_to_dict()
1✔
74
            config = deep_merge_dicts(env_data, config)
1✔
75

76
        # Normalize config
77
        if 'internals' not in config:
1✔
78
            config['internals'] = {}
1✔
79

80
        if 'pairlists' not in config:
1✔
81
            config['pairlists'] = []
1✔
82

83
        # Keep a copy of the original configuration file
84
        config['original_config'] = deepcopy(config)
1✔
85

86
        self._process_logging_options(config)
1✔
87

88
        self._process_runmode(config)
1✔
89

90
        self._process_common_options(config)
1✔
91

92
        self._process_trading_options(config)
1✔
93

94
        self._process_optimize_options(config)
1✔
95

96
        self._process_plot_options(config)
1✔
97

98
        self._process_data_options(config)
1✔
99

100
        self._process_analyze_options(config)
1✔
101

102
        self._process_freqai_options(config)
1✔
103

104
        # Import check_exchange here to avoid import cycle problems
105
        from freqtrade.exchange.check_exchange import check_exchange
1✔
106

107
        # Check if the exchange set by the user is supported
108
        check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
1✔
109

110
        self._resolve_pairs_list(config)
1✔
111

112
        process_temporary_deprecated_settings(config)
1✔
113

114
        return config
1✔
115

116
    def _process_logging_options(self, config: Config) -> None:
1✔
117
        """
118
        Extract information for sys.argv and load logging configuration:
119
        the -v/--verbose, --logfile options
120
        """
121
        # Log level
122
        config.update({'verbosity': self.args.get('verbosity', 0)})
1✔
123

124
        if 'logfile' in self.args and self.args['logfile']:
1✔
125
            config.update({'logfile': self.args['logfile']})
1✔
126

127
        setup_logging(config)
1✔
128

129
    def _process_trading_options(self, config: Config) -> None:
1✔
130
        if config['runmode'] not in TRADE_MODES:
1✔
131
            return
1✔
132

133
        if config.get('dry_run', False):
1✔
134
            logger.info('Dry run is enabled')
1✔
135
            if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
1✔
136
                # Default to in-memory db for dry_run if not specified
137
                config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
1✔
138
        else:
139
            if not config.get('db_url'):
1✔
140
                config['db_url'] = constants.DEFAULT_DB_PROD_URL
1✔
141
            logger.info('Dry run is disabled')
1✔
142

143
        logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
1✔
144

145
    def _process_common_options(self, config: Config) -> None:
1✔
146

147
        # Set strategy if not specified in config and or if it's non default
148
        if self.args.get('strategy') or not config.get('strategy'):
1✔
149
            config.update({'strategy': self.args.get('strategy')})
1✔
150

151
        self._args_to_config(config, argname='strategy_path',
1✔
152
                             logstring='Using additional Strategy lookup path: {}')
153

154
        if ('db_url' in self.args and self.args['db_url'] and
1✔
155
                self.args['db_url'] != constants.DEFAULT_DB_PROD_URL):
156
            config.update({'db_url': self.args['db_url']})
1✔
157
            logger.info('Parameter --db-url detected ...')
1✔
158

159
        self._args_to_config(config, argname='db_url_from',
1✔
160
                             logstring='Parameter --db-url-from detected ...')
161

162
        if config.get('force_entry_enable', False):
1✔
163
            logger.warning('`force_entry_enable` RPC message enabled.')
1✔
164

165
        # Support for sd_notify
166
        if 'sd_notify' in self.args and self.args['sd_notify']:
1✔
167
            config['internals'].update({'sd_notify': True})
×
168

169
    def _process_datadir_options(self, config: Config) -> None:
1✔
170
        """
171
        Extract information for sys.argv and load directory configurations
172
        --user-data, --datadir
173
        """
174
        # Check exchange parameter here - otherwise `datadir` might be wrong.
175
        if 'exchange' in self.args and self.args['exchange']:
1✔
176
            config['exchange']['name'] = self.args['exchange']
1✔
177
            logger.info(f"Using exchange {config['exchange']['name']}")
1✔
178

179
        if 'pair_whitelist' not in config['exchange']:
1✔
180
            config['exchange']['pair_whitelist'] = []
×
181

182
        if 'user_data_dir' in self.args and self.args['user_data_dir']:
1✔
183
            config.update({'user_data_dir': self.args['user_data_dir']})
1✔
184
        elif 'user_data_dir' not in config:
1✔
185
            # Default to cwd/user_data (legacy option ...)
186
            config.update({'user_data_dir': str(Path.cwd() / 'user_data')})
1✔
187

188
        # reset to user_data_dir so this contains the absolute path.
189
        config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
1✔
190
        logger.info('Using user-data directory: %s ...', config['user_data_dir'])
1✔
191

192
        config.update({'datadir': create_datadir(config, self.args.get('datadir'))})
1✔
193
        logger.info('Using data directory: %s ...', config.get('datadir'))
1✔
194

195
        if self.args.get('exportfilename'):
1✔
196
            self._args_to_config(config, argname='exportfilename',
1✔
197
                                 logstring='Storing backtest results to {} ...')
198
            config['exportfilename'] = Path(config['exportfilename'])
1✔
199
        else:
200
            config['exportfilename'] = (config['user_data_dir']
1✔
201
                                        / 'backtest_results')
202

203
        if self.args.get('show_sensitive'):
1✔
204
            logger.warning(
1✔
205
                "Sensitive information will be shown in the upcoming output. "
206
                "Please make sure to never share this output without redacting "
207
                "the information yourself.")
208

209
    def _process_optimize_options(self, config: Config) -> None:
1✔
210

211
        # This will override the strategy configuration
212
        self._args_to_config(config, argname='timeframe',
1✔
213
                             logstring='Parameter -i/--timeframe detected ... '
214
                                       'Using timeframe: {} ...')
215

216
        self._args_to_config(config, argname='position_stacking',
1✔
217
                             logstring='Parameter --enable-position-stacking detected ...')
218

219
        self._args_to_config(
1✔
220
            config, argname='enable_protections',
221
            logstring='Parameter --enable-protections detected, enabling Protections. ...')
222

223
        if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
1✔
224
            config.update({'use_max_market_positions': False})
1✔
225
            logger.info('Parameter --disable-max-market-positions detected ...')
1✔
226
            logger.info('max_open_trades set to unlimited ...')
1✔
227
        elif 'max_open_trades' in self.args and self.args['max_open_trades']:
1✔
228
            config.update({'max_open_trades': self.args['max_open_trades']})
1✔
229
            logger.info('Parameter --max-open-trades detected, '
1✔
230
                        'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
231
        elif config['runmode'] in NON_UTIL_MODES:
1✔
232
            logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
1✔
233
        # Setting max_open_trades to infinite if -1
234
        if config.get('max_open_trades') == -1:
1✔
235
            config['max_open_trades'] = float('inf')
1✔
236

237
        if self.args.get('stake_amount'):
1✔
238
            # Convert explicitly to float to support CLI argument for both unlimited and value
239
            try:
1✔
240
                self.args['stake_amount'] = float(self.args['stake_amount'])
1✔
241
            except ValueError:
1✔
242
                pass
1✔
243

244
        configurations = [
1✔
245
            ('timeframe_detail',
246
             'Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...'),
247
            ('backtest_show_pair_list', 'Parameter --show-pair-list detected.'),
248
            ('stake_amount',
249
             'Parameter --stake-amount detected, overriding stake_amount to: {} ...'),
250
            ('dry_run_wallet',
251
             'Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...'),
252
            ('fee', 'Parameter --fee detected, setting fee to: {} ...'),
253
            ('timerange', 'Parameter --timerange detected: {} ...'),
254
            ]
255

256
        self._args_to_config_loop(config, configurations)
1✔
257

258
        self._process_datadir_options(config)
1✔
259

260
        self._args_to_config(config, argname='strategy_list',
1✔
261
                             logstring='Using strategy list of {} strategies', logfun=len)
262

263
        configurations = [
1✔
264
            ('recursive_strategy_search',
265
             'Recursively searching for a strategy in the strategies folder.'),
266
            ('timeframe', 'Overriding timeframe with Command line argument'),
267
            ('export', 'Parameter --export detected: {} ...'),
268
            ('backtest_breakdown', 'Parameter --breakdown detected ...'),
269
            ('backtest_cache', 'Parameter --cache={} detected ...'),
270
            ('disableparamexport', 'Parameter --disableparamexport detected: {} ...'),
271
            ('freqai_backtest_live_models',
272
             'Parameter --freqai-backtest-live-models detected ...'),
273
        ]
274
        self._args_to_config_loop(config, configurations)
1✔
275

276
        # Edge section:
277
        if 'stoploss_range' in self.args and self.args["stoploss_range"]:
1✔
278
            txt_range = eval(self.args["stoploss_range"])
1✔
279
            config['edge'].update({'stoploss_range_min': txt_range[0]})
1✔
280
            config['edge'].update({'stoploss_range_max': txt_range[1]})
1✔
281
            config['edge'].update({'stoploss_range_step': txt_range[2]})
1✔
282
            logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
1✔
283

284
        # Hyperopt section
285

286
        configurations = [
1✔
287
            ('hyperopt', 'Using Hyperopt class name: {}'),
288
            ('hyperopt_path', 'Using additional Hyperopt lookup path: {}'),
289
            ('hyperoptexportfilename', 'Using hyperopt file: {}'),
290
            ('lookahead_analysis_exportfilename', 'Saving lookahead analysis results into {} ...'),
291
            ('epochs', 'Parameter --epochs detected ... Will run Hyperopt with for {} epochs ...'),
292
            ('spaces', 'Parameter -s/--spaces detected: {}'),
293
            ('analyze_per_epoch', 'Parameter --analyze-per-epoch detected.'),
294
            ('print_all', 'Parameter --print-all detected ...'),
295
        ]
296
        self._args_to_config_loop(config, configurations)
1✔
297

298
        if 'print_colorized' in self.args and not self.args["print_colorized"]:
1✔
299
            logger.info('Parameter --no-color detected ...')
1✔
300
            config.update({'print_colorized': False})
1✔
301
        else:
302
            config.update({'print_colorized': True})
1✔
303

304
        configurations = [
1✔
305
            ('print_json', 'Parameter --print-json detected ...'),
306
            ('export_csv', 'Parameter --export-csv detected: {}'),
307
            ('hyperopt_jobs', 'Parameter -j/--job-workers detected: {}'),
308
            ('hyperopt_random_state', 'Parameter --random-state detected: {}'),
309
            ('hyperopt_min_trades', 'Parameter --min-trades detected: {}'),
310
            ('hyperopt_loss', 'Using Hyperopt loss class name: {}'),
311
            ('hyperopt_show_index', 'Parameter -n/--index detected: {}'),
312
            ('hyperopt_list_best', 'Parameter --best detected: {}'),
313
            ('hyperopt_list_profitable', 'Parameter --profitable detected: {}'),
314
            ('hyperopt_list_min_trades', 'Parameter --min-trades detected: {}'),
315
            ('hyperopt_list_max_trades', 'Parameter --max-trades detected: {}'),
316
            ('hyperopt_list_min_avg_time', 'Parameter --min-avg-time detected: {}'),
317
            ('hyperopt_list_max_avg_time', 'Parameter --max-avg-time detected: {}'),
318
            ('hyperopt_list_min_avg_profit', 'Parameter --min-avg-profit detected: {}'),
319
            ('hyperopt_list_max_avg_profit', 'Parameter --max-avg-profit detected: {}'),
320
            ('hyperopt_list_min_total_profit', 'Parameter --min-total-profit detected: {}'),
321
            ('hyperopt_list_max_total_profit', 'Parameter --max-total-profit detected: {}'),
322
            ('hyperopt_list_min_objective', 'Parameter --min-objective detected: {}'),
323
            ('hyperopt_list_max_objective', 'Parameter --max-objective detected: {}'),
324
            ('hyperopt_list_no_details', 'Parameter --no-details detected: {}'),
325
            ('hyperopt_show_no_header', 'Parameter --no-header detected: {}'),
326
            ('hyperopt_ignore_missing_space', 'Paramter --ignore-missing-space detected: {}'),
327
        ]
328

329
        self._args_to_config_loop(config, configurations)
1✔
330

331
    def _process_plot_options(self, config: Config) -> None:
1✔
332

333
        configurations = [
1✔
334
            ('pairs', 'Using pairs {}'),
335
            ('indicators1', 'Using indicators1: {}'),
336
            ('indicators2', 'Using indicators2: {}'),
337
            ('trade_ids', 'Filtering on trade_ids: {}'),
338
            ('plot_limit', 'Limiting plot to: {}'),
339
            ('plot_auto_open', 'Parameter --auto-open detected.'),
340
            ('trade_source', 'Using trades from: {}'),
341
            ('prepend_data', 'Prepend detected. Allowing data prepending.'),
342
            ('erase', 'Erase detected. Deleting existing data.'),
343
            ('no_trades', 'Parameter --no-trades detected.'),
344
            ('timeframes', 'timeframes --timeframes: {}'),
345
            ('days', 'Detected --days: {}'),
346
            ('include_inactive', 'Detected --include-inactive-pairs: {}'),
347
            ('download_trades', 'Detected --dl-trades: {}'),
348
            ('dataformat_ohlcv', 'Using "{}" to store OHLCV data.'),
349
            ('dataformat_trades', 'Using "{}" to store trades data.'),
350
            ('show_timerange', 'Detected --show-timerange'),
351
        ]
352
        self._args_to_config_loop(config, configurations)
1✔
353

354
    def _process_data_options(self, config: Config) -> None:
1✔
355
        self._args_to_config(config, argname='new_pairs_days',
1✔
356
                             logstring='Detected --new-pairs-days: {}')
357
        self._args_to_config(config, argname='trading_mode',
1✔
358
                             logstring='Detected --trading-mode: {}')
359
        config['candle_type_def'] = CandleType.get_default(
1✔
360
            config.get('trading_mode', 'spot') or 'spot')
361
        config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot') or 'spot')
1✔
362
        self._args_to_config(config, argname='candle_types',
1✔
363
                             logstring='Detected --candle-types: {}')
364

365
    def _process_analyze_options(self, config: Config) -> None:
1✔
366
        configurations = [
1✔
367
            ('analysis_groups', 'Analysis reason groups: {}'),
368
            ('enter_reason_list', 'Analysis enter tag list: {}'),
369
            ('exit_reason_list', 'Analysis exit tag list: {}'),
370
            ('indicator_list', 'Analysis indicator list: {}'),
371
            ('timerange', 'Filter trades by timerange: {}'),
372
            ('analysis_rejected', 'Analyse rejected signals: {}'),
373
            ('analysis_to_csv', 'Store analysis tables to CSV: {}'),
374
            ('analysis_csv_path', 'Path to store analysis CSVs: {}'),
375
            # Lookahead analysis results
376
            ('targeted_trade_amount', 'Targeted Trade amount: {}'),
377
            ('minimum_trade_amount', 'Minimum Trade amount: {}'),
378
            ('lookahead_analysis_exportfilename', 'Path to store lookahead-analysis-results: {}'),
379
            ('startup_candle', 'Startup candle to be used on recursive analysis: {}'),
380
        ]
381
        self._args_to_config_loop(config, configurations)
1✔
382

383
    def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
1✔
384

385
        for argname, logstring in configurations:
1✔
386
            self._args_to_config(config, argname=argname, logstring=logstring)
1✔
387

388
    def _process_runmode(self, config: Config) -> None:
1✔
389

390
        self._args_to_config(config, argname='dry_run',
1✔
391
                             logstring='Parameter --dry-run detected, '
392
                             'overriding dry_run to: {} ...')
393

394
        if not self.runmode:
1✔
395
            # Handle real mode, infer dry/live from config
396
            self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
1✔
397
            logger.info(f"Runmode set to {self.runmode.value}.")
1✔
398

399
        config.update({'runmode': self.runmode})
1✔
400

401
    def _process_freqai_options(self, config: Config) -> None:
1✔
402

403
        self._args_to_config(config, argname='freqaimodel',
1✔
404
                             logstring='Using freqaimodel class name: {}')
405

406
        self._args_to_config(config, argname='freqaimodel_path',
1✔
407
                             logstring='Using freqaimodel path: {}')
408

409
        return
1✔
410

411
    def _args_to_config(self, config: Config, argname: str,
1✔
412
                        logstring: str, logfun: Optional[Callable] = None,
413
                        deprecated_msg: Optional[str] = None) -> None:
414
        """
415
        :param config: Configuration dictionary
416
        :param argname: Argumentname in self.args - will be copied to config dict.
417
        :param logstring: Logging String
418
        :param logfun: logfun is applied to the configuration entry before passing
419
                        that entry to the log string using .format().
420
                        sample: logfun=len (prints the length of the found
421
                        configuration instead of the content)
422
        """
423
        if (argname in self.args and self.args[argname] is not None
1✔
424
                and self.args[argname] is not False):
425

426
            config.update({argname: self.args[argname]})
1✔
427
            if logfun:
1✔
428
                logger.info(logstring.format(logfun(config[argname])))
1✔
429
            else:
430
                logger.info(logstring.format(config[argname]))
1✔
431
            if deprecated_msg:
1✔
432
                warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
1✔
433

434
    def _resolve_pairs_list(self, config: Config) -> None:
1✔
435
        """
436
        Helper for download script.
437
        Takes first found:
438
        * -p (pairs argument)
439
        * --pairs-file
440
        * whitelist from config
441
        """
442

443
        if "pairs" in config:
1✔
444
            config['exchange']['pair_whitelist'] = config['pairs']
1✔
445
            return
1✔
446

447
        if "pairs_file" in self.args and self.args["pairs_file"]:
1✔
448
            pairs_file = Path(self.args["pairs_file"])
1✔
449
            logger.info(f'Reading pairs file "{pairs_file}".')
1✔
450
            # Download pairs from the pairs file if no config is specified
451
            # or if pairs file is specified explicitly
452
            if not pairs_file.exists():
1✔
453
                raise OperationalException(f'No pairs file found with path "{pairs_file}".')
1✔
454
            config['pairs'] = load_file(pairs_file)
1✔
455
            if isinstance(config['pairs'], list):
1✔
456
                config['pairs'].sort()
1✔
457
            return
1✔
458

459
        if 'config' in self.args and self.args['config']:
1✔
460
            logger.info("Using pairlist from configuration.")
1✔
461
            config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
1✔
462
        else:
463
            # Fall back to /dl_path/pairs.json
464
            pairs_file = config['datadir'] / 'pairs.json'
1✔
465
            if pairs_file.exists():
1✔
466
                logger.info(f'Reading pairs file "{pairs_file}".')
1✔
467
                config['pairs'] = load_file(pairs_file)
1✔
468
                if 'pairs' in config and isinstance(config['pairs'], list):
1✔
469
                    config['pairs'].sort()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc