#!/usr/bin/env python3 """ mo_config.py — MoFin 统一配置管理(单例模式) 替代 MoFin 中散落在各文件的硬编码路径和常量。 ⚠️ 铁律:所有 MoFin 模块必须从此处获取路径和配置,严禁硬编码。 之前:DATA_DIR = "/home/hmo/web-dashboard/data" (散落在 10+ 文件中) 现在:from mo_config import config; config.data_dir 用法: from mo_config import config portfolio_path = config.data_dir / "portfolio.json" """ import os import json from pathlib import Path from dataclasses import dataclass, field from typing import List @dataclass class MoConfig: """MoFin 全局配置单例""" # ── 路径 ────────────────────────────────────────────────────── # 项目根目录 project_dir: Path = field(default_factory=lambda: Path(__file__).parent.resolve()) # 数据目录(portfolio.json, decisions.json 等) data_dir: Path = field(default_factory=lambda: Path( os.environ.get("MOFIN_DATA_DIR", "/home/hmo/web-dashboard/data") )) # SQLite 数据库路径 db_path: Path = field(default=None) # 缓存目录 cache_dir: Path = field(default_factory=lambda: Path.home() / ".cache" / "mofin") # Hermes 状态目录 hermes_dir: Path = field(default_factory=lambda: Path.home() / ".hermes") # ── 关键数据文件路径 ────────────────────────────────────────── @property def portfolio_path(self) -> Path: return self.data_dir / "portfolio.json" @property def decisions_path(self) -> Path: return self.data_dir / "decisions.json" @property def watchlist_path(self) -> Path: return self.data_dir / "watchlist.json" @property def price_events_path(self) -> Path: return self.data_dir / "price_events.json" @property def live_prices_path(self) -> Path: return self.data_dir / "live_prices.json" @property def evaluation_input_path(self) -> Path: return self.data_dir / "evaluation_input.json" @property def multi_tf_cache_path(self) -> Path: return self.data_dir / "multi_tf_cache.json" @property def price_history_path(self) -> Path: return self.data_dir / "price_history.json" # ── DB 路径(懒加载) ──────────────────────────────────────── def _get_db_path(self) -> Path: if self.db_path is None: self.db_path = self.data_dir / "mofin.db" return self.db_path # ── 汇率 ────────────────────────────────────────────────────── hk_rate_fallback: float = 0.87 # 港币→人民币 fallback 汇率 # ── 小果 LLM 端点(用机器名,/etc/hosts 自动解析 LAN/EasyTier)─ # node122 = 192.168.1.122 (LAN) / 10.144.144.2 (EasyTier) xiaoguo_host: str = "node122" xiaoguo_port: int = 18003 @property def xiaoguo_url(self) -> str: return f"http://{self.xiaoguo_host}:{self.xiaoguo_port}" @property def xiaoguo_api_url(self) -> str: return f"{self.xiaoguo_url}/v1/chat/completions" port: int = field(default_factory=lambda: int(os.environ.get("PORT", "8899"))) tdx_relay_url: str = field( default_factory=lambda: os.environ.get("TDX_RELAY_URL", "http://localhost:8080") ) xmpp_agent_host: str = field( default_factory=lambda: os.environ.get("XMPP_AGENT_HOST", "localhost") ) xmpp_agent_port: int = field( default_factory=lambda: int(os.environ.get("XMPP_AGENT_PORT", "5801")) ) # ── DSA 集成 ────────────────────────────────────────────────── dsa_enabled: bool = field( default_factory=lambda: os.environ.get("DSA_ENABLED", "false").lower() == "true" ) dsa_base_dir: Path = field(default_factory=lambda: Path( os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), "..", "daily-stock-analysis", "ZhuLinsen-daily_stock_analysis-a448886" )) )) # ── 数据新鲜度 ──────────────────────────────────────────────── market_hours_max_stale_min: int = 5 # 盘中最大过期时间(分钟) off_hours_max_stale_min: int = 120 # 盘后最大过期时间(分钟) # ── 验证 ────────────────────────────────────────────────────── def validate(self) -> List[str]: """验证配置,返回问题列表""" issues = [] if not self.data_dir.exists(): issues.append(f"数据目录不存在: {self.data_dir}") if not self.portfolio_path.exists(): issues.append(f"portfolio.json 不存在: {self.portfolio_path}") if not self.decisions_path.exists(): issues.append(f"decisions.json 不存在: {self.decisions_path}") return issues def ensure_dirs(self): """确保必要的目录存在""" self.data_dir.mkdir(parents=True, exist_ok=True) self.cache_dir.mkdir(parents=True, exist_ok=True) self.hermes_dir.mkdir(parents=True, exist_ok=True) # ── 输出 ────────────────────────────────────────────────────── def summary(self) -> str: """打印配置摘要""" lines = [ "=== MoFin 配置 ===", f"项目目录: {self.project_dir}", f"数据目录: {self.data_dir} (存在: {self.data_dir.exists()})", f"DB路径: {self._get_db_path()} (存在: {self._get_db_path().exists()})", f"端口: {self.port}", f"TDX Relay: {self.tdx_relay_url}", f"DSA 集成: {'启用' if self.dsa_enabled else '关闭'}", f"港币汇率 fallback: {self.hk_rate_fallback}", ] issues = self.validate() if issues: lines.append(f"\n⚠️ 配置问题 ({len(issues)}):") for i in issues: lines.append(f" - {i}") return "\n".join(lines) # ── 单例 ──────────────────────────────────────────────────────────── _config_instance: MoConfig | None = None def get_config() -> MoConfig: """获取全局配置单例""" global _config_instance if _config_instance is None: _config_instance = MoConfig() return _config_instance # 便捷别名 config = property(lambda self: get_config()) # ── 模块级便捷访问 ────────────────────────────────────────────────── def data_dir() -> Path: return get_config().data_dir def ensure_dirs(): get_config().ensure_dirs() # ── 向后兼容:导出常用路径常量 ────────────────────────────────────── # 让旧代码可以通过熟悉的变量名访问路径 def _lazy(attr): """懒加载属性,首次访问时从 config 获取""" return getattr(get_config(), attr) # 为兼容旧代码导出以下变量 PORTFOLIO_PATH = None # 改用 config.portfolio_path DECISIONS_PATH = None # 改用 config.decisions_path WATCHLIST_PATH = None # 改用 config.watchlist_path # ── 自检 ──────────────────────────────────────────────────────────── if __name__ == "__main__": cfg = get_config() print(cfg.summary())