6abc2e45b0
Phase 0 (止血):
- mo_models.py: unified calc_total_assets(), is_hk_stock(), get_hk_rate() — single source of truth
- Fixed 3 files missing frozen_cash: holdings_reconciliation, server, import_holding_xls
- Fixed stale_push_wlin: unified is_hk_stock detection, removed hardcoded 0.866
- Fixed price_monitor: consolidated 2 duplicate total_assets blocks into mo_models calls
- Fixed stock_scorer: replaced broken len()<=5 is_hk_stock heuristic
- Fixed strategy_lifecycle: replaced non-existent currency_utils import with mo_models
Phase 1 (DSA adapter):
- mo_provider.py: wraps DSA DataFetcherManager (16 fetchers, auto-fallback)
- TDX relay as primary, DSA as backup for realtime/kline/news/fundamentals
Phase 2 (Integration):
- mo_bridge.py: injects DSA market review + news context into MoFin analysis prompts
- Graceful degradation if DSA not installed
Infrastructure:
- mo_config.py: centralized Config singleton replacing scattered hardcoded paths
- All 11 changed files pass python compile check
Impact: total_assets now computed in ONE place (mo_models).
is_hk_stock now ONE implementation (no more false negatives).
HK rate now ONE source (hk_rate API → cache → 0.87 fallback).
No more hardcoded 0.866/0.8664/0.8700 divergence.
214 lines
8.0 KiB
Python
214 lines
8.0 KiB
Python
#!/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 汇率
|
|
|
|
# ── 服务配置 ──────────────────────────────────────────────────
|
|
|
|
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())
|