Config Management
Manage multi-environment configs for trading systems — pydantic-settings, YAML/TOML files, and environment-based overrides.
Configuration Management
1. Overview
Configuration management facilitates the centralization of application settings, strategy parameters, exchange credentials, and database configurations within a structured system.
Objectives
- Elimination of hardcoded values
- Secure handling of sensitive credentials
- Support for multiple operational environments
- Streamlined maintenance processes
2. Configuration Files
Configuration parameters are segregated into distinct file types based on their sensitivity and purpose.
| File | Purpose |
|---|---|
config.yaml | Non-sensitive application settings |
.env | Sensitive credentials and secrets |
2.1. Example YAML Structure
YAML (YAML Ain't Markup Language) is utilized for structured, human-readable configuration data. It supports hierarchical data representation.
strategy:
fast_window: 10
slow_window: 30
exchange:
name: bybit
testnet: true
2.2. Example .env File
The .env file contains environment-specific variables, particularly sensitive data that must not be committed to version control. This file is typically excluded via .gitignore.
BYBIT_API_KEY=abc123
BYBIT_API_SECRET=secret123
DB_PASSWORD=password
3. Dependency Installation
Required third-party libraries for configuration management are installed to ensure notebook functionality.
!pip install pyyaml python-dotenv pandasRequirement already satisfied: pyyaml in /usr/local/lib/python3.12/dist-packages (6.0.3) Requirement already satisfied: python-dotenv in /usr/local/lib/python3.12/dist-packages (1.2.2) Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy>=1.26.0 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.0.2) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
4. Library Imports
Standard library modules and third-party packages are imported to support various functionalities, including file system operations, logging, YAML parsing, and environment variable management.
# Standard library imports
import os # Access operating system environment variables
import json # Serialize Python dictionaries into formatted JSON strings
import logging # Logging framework for runtime events
from pathlib import Path # OS-independent file path handling
# Third-party library imports
import yaml # Read and write YAML configuration files
from dotenv import load_dotenv # Load .env files into environment variables
# Warning management
import warnings
warnings.filterwarnings("ignore") # Suppresses non-critical runtime warnings for cleaner output
# Logger Configuration
logging.basicConfig(
level=logging.INFO, # Sets the logging level to INFO
format="%(levelname)s | %(message)s" # Defines the log message format
)
# Module-level logger instance
logger = logging.getLogger(__name__)5. Default Configuration Definition
The default configuration serves as a foundational layer, establishing a comprehensive set of parameters with fallback values. It also acts as a schema and provides intrinsic documentation for all available settings.
def get_default_config() -> dict:
"""
Retrieves the complete default configuration dictionary.
The default configuration fulfills three primary roles:
- Serves as a fallback configuration for unset parameters.
- Defines the expected configuration schema.
- Acts as implicit runtime documentation for all settings.
Returns
-------
dict
A nested dictionary representing the default application configuration.
"""
return {
"strategy": {
"fast_window": 10, # Defines the period for the fast moving average.
"slow_window": 30, # Defines the period for the slow moving average.
"initial_capital": 10000.0, # Starting balance for simulated trading.
},
"exchange": {
"name": "bybit", # Specifies the cryptocurrency exchange platform.
"testnet": True, # Boolean flag to indicate connection to a test environment.
},
}6. Strategy Configuration and Logic
The strategy section within the configuration defines all operational parameters utilized by the trading engine. These parameters directly influence the behavior and decision-making processes of the automated trading strategy.
6.1. Key Strategy Parameters
| Parameter | Purpose |
|---|---|
fast_window | Period for the fast moving average, influencing sensitivity. |
slow_window | Period for the slow moving average, indicating trend. |
initial_capital | The starting portfolio balance for the strategy. |
6.2. Example Signal Generation Logic
Configuration parameters are directly integrated into the signal generation logic, allowing for modification of strategy behavior without requiring code changes.
IF fast_moving_average > slow_moving_average
→ Generate BUY signal
IF fast_moving_average < slow_moving_average
→ Generate SELL signal
Adjustments to fast_window and slow_window in the configuration directly alter the crossover conditions, thereby changing the trading signals generated by the strategy.
7. YAML Configuration File Generation
This section outlines the process for automatically generating an initial config.yaml file. This action prevents overwriting existing user modifications.
def write_config(path: str = "config.yaml") -> Path:
"""
Writes the default configuration to a specified YAML file.
Existing files are not overwritten to preserve user-defined settings.
Parameters
----------
path : str
The destination file path for the YAML configuration.
Returns
-------
Path
The absolute path to the written configuration file.
"""
config_path = Path(path).resolve() # Resolve absolute file path
if config_path.exists():
logger.info("Configuration file already exists at %s. Skipping write operation.", config_path)
return config_path
default_cfg = get_default_config() # Retrieve the default configuration
with open(config_path, "w") as f:
yaml.dump(
default_cfg,
f,
default_flow_style=False, # Use human-readable block formatting
sort_keys=False # Preserve insertion order of keys
)
logger.info("Default configuration written to %s.", config_path)
return config_path
write_config()PosixPath('/content/config.yaml')8. YAML Configuration Loading
This section details the loading of user-defined configuration values from config.yaml. The process involves merging these values with the default configuration, ensuring that user overrides are applied while preserving any defaults not explicitly specified.
def load_config(path: str = "config.yaml") -> dict:
"""
Loads a YAML configuration file and merges its contents with the default configuration.
Parameters
----------
path : str
The file path to the YAML configuration file.
Returns
-------
dict
The final merged configuration dictionary.
"""
config = get_default_config() # Initialize with default configuration
config_path = Path(path) # Create a Path object for the configuration file
if not config_path.exists():
logger.warning("Configuration file not found at %s. Proceeding with default settings.", config_path)
return config
with open(config_path, "r") as f:
yaml_data = yaml.safe_load(f) # Safely load YAML data to prevent arbitrary code execution
if yaml_data is None:
logger.warning("Configuration file at %s is empty. Using default settings.", config_path)
return config
# Merge YAML overrides into defaults
for section, values in yaml_data.items():
if section in config and isinstance(values, dict):
config[section].update(values) # Update only the keys provided in the YAML file
elif section in config and not isinstance(values, dict):
logger.warning("Type mismatch for section '%s' in %s. Expected dictionary, got %s. Ignoring section.", section, config_path, type(values).__name__)
logger.info("Configuration loaded from %s.", config_path)
return config
loaded_config = load_config()
print("Loaded configuration:\n%s" % json.dumps(loaded_config, indent=2))Loaded configuration:
{
"strategy": {
"fast_window": 10,
"slow_window": 30,
"initial_capital": 10000.0
},
"exchange": {
"name": "bybit",
"testnet": true
}
}
8.1. Importance of yaml.safe_load()
The yaml.safe_load() function is crucial for security when processing YAML files, especially those sourced from untrusted inputs. It restricts the types of Python objects that can be constructed from the YAML data, mitigating risks associated with arbitrary code execution.
| Function | Security Implications |
|---|---|
yaml.safe_load() | Secure: Loads only standard YAML types, preventing arbitrary code execution. |
yaml.load() | Insecure: Capable of executing arbitrary Python objects, posing a security risk with untrusted input. |
Recommendation: Always utilize yaml.safe_load() for parsing user-provided or external YAML configuration files.
9. Secret Injection from Environment Variables
Sensitive credentials, such as API keys and database passwords, must be isolated from source code and configuration files. The .env file and environment variables provide a secure mechanism for managing these secrets. This section describes the process of injecting these secrets into the application's runtime configuration.
def inject_secrets(config: dict, env_file: str = ".env") -> dict:
"""
Injects sensitive information from environment variables into the configuration dictionary.
Environment variables are loaded from the specified `.env` file, with existing system
environment variables taking precedence. Secrets include API keys, API secrets, and database passwords.
Parameters
----------
config : dict
The current configuration dictionary.
env_file : str
The path to the `.env` file. No error is raised if the file is absent.
Returns
-------
dict
The configuration dictionary with secrets injected.
"""
# Load .env file variables into os.environ. Existing OS variables take precedence.
load_dotenv(dotenv_path=env_file, override=False)
# Dynamically determine the exchange name for credential lookup
exchange_name = config["exchange"].get("name", "bybit").upper()
# Inject exchange API credentials
config["exchange"]["api_key"] = os.getenv(
f"{exchange_name}_API_KEY", "" # Default to empty string if not set
)
config["exchange"]["api_secret"] = os.getenv(
f"{exchange_name}_API_SECRET", ""
)
# Inject database password. Ensure 'database' section exists.
if "database" not in config:
config["database"] = {} # Create database section if it doesn't exist
config["database"]["password"] = os.getenv("DB_PASSWORD", "")
logger.info("Secrets successfully injected from environment variables.")
return config
# Create a dummy .env file for demonstration purposes
# In a real application, this file would be managed separately and not committed to version control.
with open(".env", "w") as f:
f.write("BYBIT_API_KEY=your_bybit_api_key\n")
f.write("BYBIT_API_SECRET=your_bybit_api_secret\n")
f.write("DB_PASSWORD=your_db_password\n")
# Inject secrets into the loaded configuration
config_with_secrets = inject_secrets(loaded_config)
def mask_secrets(config: dict) -> dict:
"""
Creates a deep copy of the configuration and masks sensitive values with '***'.
This function is intended exclusively for logging and display purposes to prevent
accidental exposure of credentials. It does not modify the original configuration object.
Parameters
----------
config : dict
The configuration dictionary containing potentially sensitive data.
Returns
-------
dict
A new configuration dictionary with sensitive values masked.
"""
import copy
masked = copy.deepcopy(config) # Create a deep copy to prevent mutation of the original
secret_keys = {"api_key", "api_secret", "password"} # Define keys to be masked
for section_name, section_values in masked.items():
if isinstance(section_values, dict):
for key in section_values:
if key in secret_keys:
# Replace the value with asterisks or indicate if not set
section_values[key] = "***" if section_values[key] else "(not set)"
return masked
print("Configuration with masked secrets:\n%s" % json.dumps(mask_secrets(config_with_secrets), indent=2))Configuration with masked secrets:
{
"strategy": {
"fast_window": 10,
"slow_window": 30,
"initial_capital": 10000.0
},
"exchange": {
"name": "bybit",
"testnet": true,
"api_key": "***",
"api_secret": "***"
},
"database": {
"password": "***"
}
}
10. Configuration Validation
Configuration validation is a critical step to ensure the integrity and correctness of application settings. This process involves checking data types, value ranges, and logical relationships between parameters, as well as verifying the presence of all mandatory fields.
Validation helps prevent runtime errors, provides early feedback on invalid configurations, and enhances overall application stability.
def validate_config(config: dict) -> list:
"""
Validates the provided configuration dictionary against defined rules.
Checks include structural integrity, value ranges, and inter-parameter dependencies.
Parameters
----------
config : dict
The complete application configuration dictionary to be validated.
Returns
-------
list of str
A list of validation error messages. An empty list indicates a valid configuration.
"""
errors = [] # List to accumulate all detected errors
# Extract relevant sections for validation
strat = config.get("strategy", {})
exch = config.get("exchange", {})
# --- Strategy Parameter Validation ---
if strat.get("fast_window", 0) >= strat.get("slow_window", 1):
errors.append("Strategy validation error: `fast_window` must be strictly smaller than `slow_window`.")
# --- Exchange Configuration Validation ---
# If not in testnet mode, API credentials are mandatory
if not exch.get("testnet", True):
if not exch.get("api_key"):
errors.append("Exchange validation error: `api_key` is required when `testnet` is set to `False`.")
if not exch.get("api_secret"):
errors.append("Exchange validation error: `api_secret` is required when `testnet` is set to `False`.")
return errors
# Perform validation on the configuration with injected secrets
validation_errors = validate_config(config_with_secrets)
# Report validation results
if validation_errors:
print("Configuration validation failed with the following errors:")
for error in validation_errors:
print(f"- {error}")
else:
print("Configuration is valid.")Configuration is valid.
11. Full Configuration Pipeline
The full configuration pipeline consolidates all stages of configuration loading, secret injection, and validation into a single, robust function. This approach ensures consistency and simplifies the application startup sequence by providing a fully resolved and validated configuration object.
11.1. Full Configuration Pipeline Function
The load_full_config function encapsulates the entire configuration process, ensuring that the application always starts with a complete, validated, and securely populated configuration. It orchestrates the loading of defaults, YAML overrides, secret injection, and final validation.
def load_full_config(
yaml_path: str = "config.yaml",
env_file: str = ".env",
) -> dict:
"""
Executes the comprehensive configuration loading and validation pipeline.
The pipeline consists of the following sequential steps:
1. Initialization with default values.
2. Application of overrides from the YAML configuration file.
3. Injection of secrets from environment variables (e.g., from an .env file).
4. Validation of the complete configuration to ensure integrity.
Parameters
----------
yaml_path : str
The file path to the YAML configuration file.
env_file : str
The file path to the .env file containing sensitive credentials.
Returns
-------
dict
The fully resolved, validated, and immutable configuration dictionary.
Raises
------
ValueError
If configuration validation fails, detailing all encountered errors.
"""
# Step 1: Initialize with default values.
cfg = get_default_config()
logger.info("Configuration initialized with default settings.")
# Step 2: Apply YAML overrides. `load_config` already incorporates `get_default_config`,
# so we reassign the result to `cfg`.
cfg = load_config(yaml_path)
# Step 3: Inject secrets from the environment.
cfg = inject_secrets(cfg, env_file)
# Step 4: Validate the complete configuration.
errors = validate_config(cfg)
if errors:
error_message = (
f"Configuration validation failed with {len(errors)} error(s):\n" +
"\n".join(f" - {e}" for e in errors)
)
logger.error(error_message)
raise ValueError(error_message)
logger.info("Full configuration loaded and validated successfully.")
return cfg
# Demonstrate the full configuration pipeline
try:
final_config = load_full_config("config.yaml", ".env")
print("\n--- Final Runtime Configuration (masked secrets):\n%s" % json.dumps(mask_secrets(final_config), indent=2))
except ValueError as e:
print(f"\nError loading configuration: {e}")
--- Final Runtime Configuration (masked secrets):
{
"strategy": {
"fast_window": 10,
"slow_window": 30,
"initial_capital": 10000.0
},
"exchange": {
"name": "bybit",
"testnet": true,
"api_key": "***",
"api_secret": "***"
},
"database": {
"password": "***"
}
}
Defaults
↓
config.yaml
↓
.env Secrets
↓
Validation
↓
Final Runtime Config
12. Summary of Configuration Features
This table summarizes the primary features and purposes of the configuration system components. \
| Feature | Purpose |
|---|---|
| YAML Configuration | Storage of non-sensitive application settings in a structured format. |
.env Files | Secure storage and injection of sensitive credentials. |
| Validation Logic | Detection of invalid configurations and ensuring data integrity. |
| Default Values | Provision of fallback parameters and schema definition. |
| Environment Variables | Mechanism for providing secure, environment-specific credentials. |