Merge pull request #6665 from freqtrade/config_from_config
Allow recursive loading of configuration files
This commit is contained in:
commit
6ebd30db88
@ -182,6 +182,7 @@
|
|||||||
"disable_dataframe_checks": false,
|
"disable_dataframe_checks": false,
|
||||||
"strategy": "SampleStrategy",
|
"strategy": "SampleStrategy",
|
||||||
"strategy_path": "user_data/strategies/",
|
"strategy_path": "user_data/strategies/",
|
||||||
|
"add_config_files": [],
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "json",
|
||||||
"dataformat_trades": "jsongz"
|
"dataformat_trades": "jsongz"
|
||||||
}
|
}
|
||||||
|
@ -53,14 +53,33 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
|||||||
|
|
||||||
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
||||||
|
|
||||||
|
You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file.
|
||||||
|
This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands.
|
||||||
|
|
||||||
!!! Tip "Use multiple configuration files to keep secrets secret"
|
!!! Tip "Use multiple configuration files to keep secrets secret"
|
||||||
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
|
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
|
||||||
|
|
||||||
|
``` json title="user_data/config.json"
|
||||||
|
"add_config_files": [
|
||||||
|
"config-private.json"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade trade --config user_data/config.json <...>
|
||||||
|
```
|
||||||
|
|
||||||
|
The 2nd file should only specify what you intend to override.
|
||||||
|
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
||||||
|
|
||||||
|
For one-off commands, you can also use the below syntax by specifying multiple "--config" parameters.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
|
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
|
||||||
```
|
```
|
||||||
The 2nd file should only specify what you intend to override.
|
|
||||||
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
This is equivalent to the example above - but `config-private.json` is specified as cli argument.
|
||||||
|
|
||||||
|
|
||||||
## Configuration parameters
|
## Configuration parameters
|
||||||
|
|
||||||
@ -175,6 +194,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
||||||
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
||||||
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
||||||
|
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
||||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
||||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
||||||
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
||||||
|
@ -75,19 +75,41 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def load_from_files(files: List[str]) -> Dict[str, Any]:
|
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Recursively load configuration files if specified.
|
||||||
|
Sub-files are assumed to be relative to the initial config.
|
||||||
|
"""
|
||||||
config: Dict[str, Any] = {}
|
config: Dict[str, Any] = {}
|
||||||
|
if level > 5:
|
||||||
|
raise OperationalException("Config loop detected.")
|
||||||
|
|
||||||
if not files:
|
if not files:
|
||||||
return deepcopy(MINIMAL_CONFIG)
|
return deepcopy(MINIMAL_CONFIG)
|
||||||
|
files_loaded = []
|
||||||
# We expect here a list of config filenames
|
# We expect here a list of config filenames
|
||||||
for path in files:
|
for filename in files:
|
||||||
logger.info(f'Using config: {path} ...')
|
logger.info(f'Using config: {filename} ...')
|
||||||
# Merge config options, overwriting old values
|
if filename == '-':
|
||||||
config = deep_merge_dicts(load_config_file(path), config)
|
# Immediately load stdin and return
|
||||||
|
return load_config_file(filename)
|
||||||
|
file = Path(filename)
|
||||||
|
if base_path:
|
||||||
|
# Prepend basepath to allow for relative assignments
|
||||||
|
file = base_path / file
|
||||||
|
|
||||||
config['config_files'] = files
|
config_tmp = load_config_file(str(file))
|
||||||
|
if 'add_config_files' in config_tmp:
|
||||||
|
config_sub = load_from_files(
|
||||||
|
config_tmp['add_config_files'], file.resolve().parent, level + 1)
|
||||||
|
files_loaded.extend(config_sub.get('config_files', []))
|
||||||
|
deep_merge_dicts(config_sub, config_tmp)
|
||||||
|
|
||||||
|
files_loaded.insert(0, str(file))
|
||||||
|
|
||||||
|
# Merge config options, overwriting prior values
|
||||||
|
config = deep_merge_dicts(config_tmp, config)
|
||||||
|
|
||||||
|
config['config_files'] = files_loaded
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -91,15 +91,14 @@ SUPPORTED_FIAT = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
MINIMAL_CONFIG = {
|
MINIMAL_CONFIG = {
|
||||||
'stake_currency': '',
|
"stake_currency": "",
|
||||||
'dry_run': True,
|
"dry_run": True,
|
||||||
'exchange': {
|
"exchange": {
|
||||||
'name': '',
|
"name": "",
|
||||||
'key': '',
|
"key": "",
|
||||||
'secret': '',
|
"secret": "",
|
||||||
'pair_whitelist': [],
|
"pair_whitelist": [],
|
||||||
'ccxt_async_config': {
|
"ccxt_async_config": {
|
||||||
'enableRateLimit': True,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,8 @@ from freqtrade.configuration.deprecated_settings import (check_conflicting_setti
|
|||||||
process_removed_setting,
|
process_removed_setting,
|
||||||
process_temporary_deprecated_settings)
|
process_temporary_deprecated_settings)
|
||||||
from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict
|
from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict
|
||||||
from freqtrade.configuration.load_config import load_config_file, load_file, log_config_error_range
|
from freqtrade.configuration.load_config import (load_config_file, load_file, load_from_files,
|
||||||
|
log_config_error_range)
|
||||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@ -206,6 +207,32 @@ def test_from_config(default_conf, mocker, caplog) -> None:
|
|||||||
assert isinstance(validated_conf['user_data_dir'], Path)
|
assert isinstance(validated_conf['user_data_dir'], Path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_recursive_files(testdatadir) -> None:
|
||||||
|
files = testdatadir / "testconfigs/testconfig.json"
|
||||||
|
|
||||||
|
conf = Configuration.from_files([files])
|
||||||
|
|
||||||
|
assert conf
|
||||||
|
# Exchange comes from "the first config"
|
||||||
|
assert conf['exchange']
|
||||||
|
# Pricing comes from the 2nd config
|
||||||
|
assert conf['entry_pricing']
|
||||||
|
assert conf['entry_pricing']['price_side'] == "same"
|
||||||
|
assert conf['exit_pricing']
|
||||||
|
# The other key comes from pricing2, which is imported by pricing.json
|
||||||
|
assert conf['exit_pricing']['price_side'] == "other"
|
||||||
|
|
||||||
|
assert len(conf['config_files']) == 4
|
||||||
|
assert 'testconfig.json' in conf['config_files'][0]
|
||||||
|
assert 'test_pricing_conf.json' in conf['config_files'][1]
|
||||||
|
assert 'test_base_config.json' in conf['config_files'][2]
|
||||||
|
assert 'test_pricing2_conf.json' in conf['config_files'][3]
|
||||||
|
|
||||||
|
files = testdatadir / "testconfigs/recursive.json"
|
||||||
|
with pytest.raises(OperationalException, match="Config loop detected."):
|
||||||
|
load_from_files([files])
|
||||||
|
|
||||||
|
|
||||||
def test_print_config(default_conf, mocker, caplog) -> None:
|
def test_print_config(default_conf, mocker, caplog) -> None:
|
||||||
conf1 = deepcopy(default_conf)
|
conf1 = deepcopy(default_conf)
|
||||||
# Delete non-json elements from default_conf
|
# Delete non-json elements from default_conf
|
||||||
|
6
tests/testdata/testconfigs/recursive.json
vendored
Normal file
6
tests/testdata/testconfigs/recursive.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
// This file fails as it's loading itself over and over
|
||||||
|
"add_config_files": [
|
||||||
|
"./recursive.json"
|
||||||
|
]
|
||||||
|
}
|
12
tests/testdata/testconfigs/test_base_config.json
vendored
Normal file
12
tests/testdata/testconfigs/test_base_config.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"stake_currency": "",
|
||||||
|
"dry_run": true,
|
||||||
|
"exchange": {
|
||||||
|
"name": "",
|
||||||
|
"key": "",
|
||||||
|
"secret": "",
|
||||||
|
"pair_whitelist": [],
|
||||||
|
"ccxt_async_config": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
tests/testdata/testconfigs/test_pricing2_conf.json
vendored
Normal file
18
tests/testdata/testconfigs/test_pricing2_conf.json
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing":{
|
||||||
|
"price_side": "other",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0
|
||||||
|
}
|
||||||
|
}
|
21
tests/testdata/testconfigs/test_pricing_conf.json
vendored
Normal file
21
tests/testdata/testconfigs/test_pricing_conf.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing":{
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0
|
||||||
|
},
|
||||||
|
"add_config_files": [
|
||||||
|
"./test_pricing2_conf.json"
|
||||||
|
]
|
||||||
|
}
|
6
tests/testdata/testconfigs/testconfig.json
vendored
Normal file
6
tests/testdata/testconfigs/testconfig.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"add_config_files": [
|
||||||
|
"test_base_config.json",
|
||||||
|
"test_pricing_conf.json"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user