""" 配置加载器 支持: 1. YAML 配置文件加载 2. Pydantic Schema 验证 3. 环境变量替换 4. 配置合并(默认值 + 用户配置) """ import os import re import yaml from pathlib import Path from typing import Optional, Dict, Any from framework_v2.config.schemas import StrategyConfig, RotationStrategyConfig class ConfigLoader: """ 配置加载器 用法: loader = ConfigLoader() config = loader.load('config/rotation.yaml') """ def __init__(self, config_dir: str = None): """ 初始化 Args: config_dir: 配置目录(默认 framework_v2/config) """ if config_dir is None: # 默认配置目录 config_dir = Path(__file__).parent self.config_dir = Path(config_dir) def load(self, config_file: str, config_type: str = 'strategy') -> StrategyConfig: """ 加载配置文件 Args: config_file: 配置文件路径(相对路径或绝对路径) config_type: 配置类型('strategy' 或 'rotation') Returns: 验证后的配置对象 示例: >>> loader = ConfigLoader() >>> config = loader.load('rotation_example.yaml') >>> print(config.factor.n_days) 25 """ # 1. 解析文件路径 config_path = self._resolve_path(config_file) # 2. 读取 YAML with open(config_path, 'r', encoding='utf-8') as f: config_dict = yaml.safe_load(f) # 3. 环境变量替换 config_dict = self._substitute_env_vars(config_dict) # 4. Pydantic 验证(根据类型选择 Schema) if config_type == 'rotation': # 兼容旧版本:轮动策略专用配置 config = RotationStrategyConfig(**config_dict) else: # 通用策略配置(推荐) config = StrategyConfig(**config_dict) return config def load_dict(self, config_dict: Dict[str, Any], config_type: str = 'strategy') -> StrategyConfig: """ 从字典加载配置 Args: config_dict: 配置字典 config_type: 配置类型('strategy' 或 'rotation') Returns: 验证后的配置对象 """ # 环境变量替换 config_dict = self._substitute_env_vars(config_dict) # Pydantic 验证(根据类型选择 Schema) if config_type == 'rotation': config = RotationStrategyConfig(**config_dict) else: config = StrategyConfig(**config_dict) return config def _resolve_path(self, config_file: str) -> Path: """ 解析配置文件路径 Args: config_file: 配置文件路径 Returns: 绝对路径 """ path = Path(config_file) # 如果是绝对路径,直接返回 if path.is_absolute(): if not path.exists(): raise FileNotFoundError(f"配置文件不存在: {path}") return path # 相对路径:先在配置目录查找 config_path = self.config_dir / path if config_path.exists(): return config_path # 然后在当前工作目录查找 cwd_path = Path.cwd() / path if cwd_path.exists(): return cwd_path raise FileNotFoundError( f"配置文件未找到: {path}\n" f"搜索路径:\n" f" - {config_path}\n" f" - {cwd_path}" ) def _substitute_env_vars(self, config: Any) -> Any: """ 替换配置中的环境变量 支持格式: - ${VAR_NAME} - ${VAR_NAME:default_value} Args: config: 配置对象(dict/list/str) Returns: 替换后的配置 """ if isinstance(config, dict): return { key: self._substitute_env_vars(value) for key, value in config.items() } elif isinstance(config, list): return [ self._substitute_env_vars(item) for item in config ] elif isinstance(config, str): # 匹配 ${VAR_NAME} 或 ${VAR_NAME:default} pattern = r'\$\{([^}]+)\}' def replace_match(match): var_expr = match.group(1) # 检查是否有默认值 if ':' in var_expr: var_name, default = var_expr.split(':', 1) else: var_name = var_expr default = None # 从环境变量读取 value = os.getenv(var_name) if value is None: if default is not None: return default else: raise ValueError( f"环境变量未设置: {var_name}\n" f"请设置环境变量或在配置中使用默认值: ${{{var_name}:default_value}}" ) return value return re.sub(pattern, replace_match, config) else: return config def get_available_configs(self) -> list: """ 获取可用的配置文件列表 Returns: 配置文件名列表 """ if not self.config_dir.exists(): return [] return [ f.name for f in self.config_dir.glob('*.yaml') if f.is_file() ] # 全局实例 _config_loader: Optional[ConfigLoader] = None def get_config_loader(config_dir: str = None) -> ConfigLoader: """ 获取配置加载器单例 Args: config_dir: 配置目录 Returns: ConfigLoader 实例 """ global _config_loader if _config_loader is None: _config_loader = ConfigLoader(config_dir) return _config_loader def load_config(config_file: str, config_type: str = 'strategy') -> StrategyConfig: """ 快捷函数:加载配置文件 Args: config_file: 配置文件路径 config_type: 配置类型('strategy' 或 'rotation') Returns: 验证后的配置对象 示例: >>> from framework_v2.config import load_config >>> config = load_config('rotation_example.yaml') """ loader = get_config_loader() return loader.load(config_file, config_type=config_type)