chore: deployed JSON retirement

This commit is contained in:
知微
2026-07-20 18:10:35 +08:00
parent 73520464b6
commit 4383c384be
9 changed files with 1954 additions and 42656 deletions
-40644
View File
File diff suppressed because it is too large Load Diff
+231 -236
View File
@@ -1,236 +1,231 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
mo_config.py — MoFin 统一配置管理(单例模式) mo_config.py — MoFin 统一配置管理(单例模式)
替代 MoFin 中散落在各文件的硬编码路径和常量。 替代 MoFin 中散落在各文件的硬编码路径和常量。
⚠️ 铁律:所有 MoFin 模块必须从此处获取路径和配置,严禁硬编码。 ⚠️ 铁律:所有 MoFin 模块必须从此处获取路径和配置,严禁硬编码。
之前:DATA_DIR = "/home/hmo/web-dashboard/data" (散落在 10+ 文件中) 之前:DATA_DIR = "/home/hmo/web-dashboard/data" (散落在 10+ 文件中)
现在:from mo_config import config; config.data_dir 现在:from mo_config import config; config.data_dir
用法: 用法:
from mo_config import config from mo_config import config
from mo_data import read_portfolio; data = read_portfolio() from mo_data import read_portfolio; data = read_portfolio()
""" """
import os import os
import json import json
from pathlib import Path from pathlib import Path
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List from typing import List
@dataclass @dataclass
class MoConfig: class MoConfig:
"""MoFin 全局配置单例""" """MoFin 全局配置单例"""
# ── 路径 ────────────────────────────────────────────────────── # ── 路径 ──────────────────────────────────────────────────────
# 项目根目录 # 项目根目录
project_dir: Path = field(default_factory=lambda: Path(__file__).parent.resolve()) project_dir: Path = field(default_factory=lambda: Path(__file__).parent.resolve())
# 数据目录(mofin.db 等,所有数据只从 DB 读写) # 数据目录(mofin.db 等,所有数据只从 DB 读写)
data_dir: Path = field(default_factory=lambda: Path( data_dir: Path = field(default_factory=lambda: Path(
os.environ.get("MOFIN_DATA_DIR", "/home/hmo/web-dashboard/data") os.environ.get("MOFIN_DATA_DIR", "/home/hmo/web-dashboard/data")
)) ))
# SQLite 数据库路径 # SQLite 数据库路径
db_path: Path = field(default=None) db_path: Path = field(default=None)
# 缓存目录 # 缓存目录
cache_dir: Path = field(default_factory=lambda: Path.home() / ".cache" / "mofin") cache_dir: Path = field(default_factory=lambda: Path.home() / ".cache" / "mofin")
# Hermes 状态目录 # Hermes 状态目录
hermes_dir: Path = field(default_factory=lambda: Path.home() / ".hermes") hermes_dir: Path = field(default_factory=lambda: Path.home() / ".hermes")
# ── 关键数据文件路径(已废弃,仅保留为检查逻辑。新代码勿用) ────── # ── 关键数据文件路径(已废弃,仅保留为检查逻辑。新代码勿用) ──────
@property @property
def portfolio_path(self) -> Path: def portfolio_path(self) -> Path:
"""⚠️ DEPRECATED: 数据已迁至 mofin.db holdings + portfolio_summary 表。""" """⚠️ DEPRECATED: 数据已迁至 mofin.db holdings + portfolio_summary 表。"""
import warnings import warnings
warnings.warn("portfolio_path is deprecated — use mo_data.read_portfolio() for DB data", DeprecationWarning, stacklevel=2) warnings.warn("portfolio_path is deprecated — use mo_data.read_portfolio() for DB data", DeprecationWarning, stacklevel=2)
return Path() return Path()
@property @property
def decisions_path(self) -> Path: def decisions_path(self) -> Path:
"""⚠️ DEPRECATED: 数据已迁至 mofin.db holding_strategies 表。""" """⚠️ DEPRECATED: 数据已迁至 mofin.db holding_strategies 表。"""
import warnings import warnings
warnings.warn("decisions_path is deprecated — use mo_data.read_decisions() for DB data", DeprecationWarning, stacklevel=2) warnings.warn("decisions_path is deprecated — use mo_data.read_decisions() for DB data", DeprecationWarning, stacklevel=2)
return Path() return Path()
@property @property
def watchlist_path(self) -> Path: def watchlist_path(self) -> Path:
"""⚠️ DEPRECATED: 数据已迁至 mofin.db watchlist_stocks 表。""" """⚠️ DEPRECATED: 数据已迁至 mofin.db watchlist_stocks 表。"""
import warnings import warnings
warnings.warn("watchlist_path is deprecated — use mo_data.read_watchlist() for DB data", DeprecationWarning, stacklevel=2) warnings.warn("watchlist_path is deprecated — use mo_data.read_watchlist() for DB data", DeprecationWarning, stacklevel=2)
return Path() return Path()
@property @property
def price_events_path(self) -> Path: def live_prices_path(self) -> Path:
"""⚠️ 已废弃!数据在 mofin.db price_events 表。""" """⚠️ DEPRECATED: 实时价格已迁移到 mofin_db.live_prices 表。"""
return self.data_dir / "price_events.json" return self.data_dir / "live_prices.json"
@property @property
def live_prices_path(self) -> Path: def evaluation_input_path(self) -> Path:
"""⚠️ DEPRECATED: 实时价格已迁移到 mofin_db.live_prices 表。""" return self.data_dir / "evaluation_input.json"
return self.data_dir / "live_prices.json"
@property
@property def multi_tf_cache_path(self) -> Path:
def evaluation_input_path(self) -> Path: """⚠️ DEPRECATED: 多周期缓存已迁移到 mofin_db.mtf_cache 表。"""
return self.data_dir / "evaluation_input.json" return self.data_dir / "multi_tf_cache.json"
@property @property
def multi_tf_cache_path(self) -> Path: def price_history_path(self) -> Path:
"""⚠️ DEPRECATED: 多周期缓存已迁移到 mofin_db.mtf_cache 表。""" return self.data_dir / "price_history.json"
return self.data_dir / "multi_tf_cache.json"
# ── DB 路径(懒加载) ────────────────────────────────────────
@property
def price_history_path(self) -> Path: def _get_db_path(self) -> Path:
return self.data_dir / "price_history.json" if self.db_path is None:
self.db_path = self.data_dir / "mofin.db"
# ── DB 路径(懒加载) ──────────────────────────────────────── return self.db_path
def _get_db_path(self) -> Path: # ── 汇率 ──────────────────────────────────────────────────────
if self.db_path is None:
self.db_path = self.data_dir / "mofin.db" hk_rate_fallback: float = 0.87 # 港币→人民币 fallback 汇率
return self.db_path
# ── 小果 LLM 端点(用机器名,/etc/hosts 自动解析 LAN/EasyTier)─
# ── 汇率 ────────────────────────────────────────────────────── # node122 = 192.168.1.122 (LAN) / 10.144.144.2 (EasyTier)
xiaoguo_host: str = "node122"
hk_rate_fallback: float = 0.87 # 港币→人民币 fallback 汇率 xiaoguo_port: int = 18003
# ── 小果 LLM 端点(用机器名,/etc/hosts 自动解析 LAN/EasyTier)─ @property
# node122 = 192.168.1.122 (LAN) / 10.144.144.2 (EasyTier) def xiaoguo_url(self) -> str:
xiaoguo_host: str = "node122" return f"http://{self.xiaoguo_host}:{self.xiaoguo_port}"
xiaoguo_port: int = 18003
@property
@property def xiaoguo_api_url(self) -> str:
def xiaoguo_url(self) -> str: return f"{self.xiaoguo_url}/v1/chat/completions"
return f"http://{self.xiaoguo_host}:{self.xiaoguo_port}"
port: int = field(default_factory=lambda: int(os.environ.get("PORT", "8899")))
@property
def xiaoguo_api_url(self) -> str: tdx_relay_url: str = field(
return f"{self.xiaoguo_url}/v1/chat/completions" default_factory=lambda: os.environ.get("TDX_RELAY_URL", "http://localhost:8080")
)
port: int = field(default_factory=lambda: int(os.environ.get("PORT", "8899")))
xmpp_agent_host: str = field(
tdx_relay_url: str = field( default_factory=lambda: os.environ.get("XMPP_AGENT_HOST", "localhost")
default_factory=lambda: os.environ.get("TDX_RELAY_URL", "http://localhost:8080") )
)
xmpp_agent_port: int = field(
xmpp_agent_host: str = field( default_factory=lambda: int(os.environ.get("XMPP_AGENT_PORT", "5801"))
default_factory=lambda: os.environ.get("XMPP_AGENT_HOST", "localhost") )
)
# ── DSA 集成 ──────────────────────────────────────────────────
xmpp_agent_port: int = field(
default_factory=lambda: int(os.environ.get("XMPP_AGENT_PORT", "5801")) dsa_enabled: bool = field(
) default_factory=lambda: os.environ.get("DSA_ENABLED", "false").lower() == "true"
)
# ── DSA 集成 ──────────────────────────────────────────────────
dsa_base_dir: Path = field(default_factory=lambda: Path(
dsa_enabled: bool = field( os.path.normpath(os.path.join(
default_factory=lambda: os.environ.get("DSA_ENABLED", "false").lower() == "true" os.path.dirname(os.path.abspath(__file__)),
) "..", "daily-stock-analysis",
"ZhuLinsen-daily_stock_analysis-a448886"
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 # 盘后最大过期时间(分钟)
# ── 数据新鲜度 ──────────────────────────────────────────────── # ── 验证 ──────────────────────────────────────────────────────
market_hours_max_stale_min: int = 5 # 盘中最大过期时间(分钟) def validate(self) -> List[str]:
off_hours_max_stale_min: int = 120 # 盘后最大过期时间(分钟) """验证配置,返回问题列表"""
issues = []
# ── 验证 ──────────────────────────────────────────────────────
if not self.data_dir.exists():
def validate(self) -> List[str]: issues.append(f"数据目录不存在: {self.data_dir}")
"""验证配置,返回问题列表"""
issues = [] if not self.portfolio_path.exists():
issues.append(f"portfolio_path 不存在(已废弃): {self.portfolio_path}")
if not self.data_dir.exists():
issues.append(f"数据目录不存在: {self.data_dir}") if not self.decisions_path.exists():
issues.append(f"decisions_path 不存在(已废弃): {self.decisions_path}")
if not self.portfolio_path.exists():
issues.append(f"portfolio_path 不存在(已废弃): {self.portfolio_path}") return issues
if not self.decisions_path.exists(): def ensure_dirs(self):
issues.append(f"decisions_path 不存在(已废弃): {self.decisions_path}") """确保必要的目录存在"""
self.data_dir.mkdir(parents=True, exist_ok=True)
return issues self.cache_dir.mkdir(parents=True, exist_ok=True)
self.hermes_dir.mkdir(parents=True, exist_ok=True)
def ensure_dirs(self):
"""确保必要的目录存在""" # ── 输出 ──────────────────────────────────────────────────────
self.data_dir.mkdir(parents=True, exist_ok=True)
self.cache_dir.mkdir(parents=True, exist_ok=True) def summary(self) -> str:
self.hermes_dir.mkdir(parents=True, exist_ok=True) """打印配置摘要"""
lines = [
# ── 输出 ────────────────────────────────────────────────────── "=== MoFin 配置 ===",
f"项目目录: {self.project_dir}",
def summary(self) -> str: f"数据目录: {self.data_dir} (存在: {self.data_dir.exists()})",
"""打印配置摘要""" f"DB路径: {self._get_db_path()} (存在: {self._get_db_path().exists()})",
lines = [ f"端口: {self.port}",
"=== MoFin 配置 ===", f"TDX Relay: {self.tdx_relay_url}",
f"项目目录: {self.project_dir}", f"DSA 集成: {'启用' if self.dsa_enabled else '关闭'}",
f"数据目录: {self.data_dir} (存在: {self.data_dir.exists()})", f"港币汇率 fallback: {self.hk_rate_fallback}",
f"DB路径: {self._get_db_path()} (存在: {self._get_db_path().exists()})", ]
f"端口: {self.port}", issues = self.validate()
f"TDX Relay: {self.tdx_relay_url}", if issues:
f"DSA 集成: {'启用' if self.dsa_enabled else '关闭'}", lines.append(f"\n⚠️ 配置问题 ({len(issues)}):")
f"港币汇率 fallback: {self.hk_rate_fallback}", for i in issues:
] lines.append(f" - {i}")
issues = self.validate() return "\n".join(lines)
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:
"""获取全局配置单例"""
_config_instance: MoConfig | None = None global _config_instance
if _config_instance is None:
_config_instance = MoConfig()
def get_config() -> MoConfig: return _config_instance
"""获取全局配置单例"""
global _config_instance
if _config_instance is None: # 便捷别名
_config_instance = MoConfig() config = property(lambda self: get_config())
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 data_dir() -> Path:
return get_config().data_dir
# ── 向后兼容:导出已废弃的路由常量 ──────────────────────────────────
def ensure_dirs(): # PORTFOLIO_PATH / DECISIONS_PATH / WATCHLIST_PATH 均已废弃(数据在 DB)。
get_config().ensure_dirs()
def _lazy(attr):
"""懒加载属性,首次访问时从 config 获取"""
# ── 向后兼容:导出已废弃的路由常量 ────────────────────────────────── return getattr(get_config(), attr)
# PORTFOLIO_PATH / DECISIONS_PATH / WATCHLIST_PATH 均已废弃(数据在 DB)。
# 为兼容旧代码导出以下变量
def _lazy(attr): PORTFOLIO_PATH = None # 改用 config.portfolio_path
"""懒加载属性,首次访问时从 config 获取""" DECISIONS_PATH = None # 改用 config.decisions_path
return getattr(get_config(), attr) WATCHLIST_PATH = None # 改用 config.watchlist_path
# 为兼容旧代码导出以下变量
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())
# ── 自检 ────────────────────────────────────────────────────────────
if __name__ == "__main__":
cfg = get_config()
print(cfg.summary())
+1 -1
View File
@@ -21,7 +21,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Callable from typing import Optional, Callable
DATA_DIR = Path(__file__).parent / "data" DATA_DIR = Path("/home/hmo/MoFin/data") # 绝对路径:全系统唯一权威数据目录
DB_PATH = DATA_DIR / "mofin.db" DB_PATH = DATA_DIR / "mofin.db"
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -18,7 +18,6 @@ from mo_data import read_decisions
DATA_DIR = Path(__file__).parent.parent / "data" DATA_DIR = Path(__file__).parent.parent / "data"
ACCURACY_PATH = DATA_DIR / "accuracy_stats.json" ACCURACY_PATH = DATA_DIR / "accuracy_stats.json"
EVENTS_PATH = DATA_DIR / "price_events.json"
FEEDBACK_PATH = DATA_DIR / "strategy_feedback.json" FEEDBACK_PATH = DATA_DIR / "strategy_feedback.json"
@@ -174,15 +173,16 @@ def generate_adjustment(decision, phase_check, accuracy_trend):
def run(): def run():
decisions = read_decisions() decisions = read_decisions()
# 优先从 SQLite 读取价格事件 # 价格事件:只从 DB price_events 表读(JSON 已退役)
try: try:
from mofin_db import get_conn, query_price_events from mofin_db import get_conn, query_price_events
conn = get_conn() conn = get_conn()
pe_rows = query_price_events(conn, limit=50000) pe_rows = query_price_events(conn, limit=50000)
conn.close() conn.close()
events = {"events": pe_rows} events = {"events": pe_rows}
except Exception: except Exception as e:
events = load_json(EVENTS_PATH, {"events": []}) print(f"[strategy_feedback] DB价格事件读取失败: {e}", file=sys.stderr)
events = {"events": []}
accuracy_stats = load_json(ACCURACY_PATH, {}) accuracy_stats = load_json(ACCURACY_PATH, {})
accuracy_trend = compute_accuracy_trend(accuracy_stats) accuracy_trend = compute_accuracy_trend(accuracy_stats)
@@ -9,7 +9,6 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
DATA_DIR = Path("/home/hmo/web-dashboard/data") DATA_DIR = Path("/home/hmo/web-dashboard/data")
EVENTS_PATH = DATA_DIR / "price_events.json"
EVALUATION_PATH = DATA_DIR / "evaluation.json" EVALUATION_PATH = DATA_DIR / "evaluation.json"
ACCURACY_PATH = DATA_DIR / "accuracy_stats.json" ACCURACY_PATH = DATA_DIR / "accuracy_stats.json"
CRON_JOBS = "/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json" CRON_JOBS = "/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json"
@@ -104,10 +103,9 @@ def run():
lines.append(check(False, "MoFin DB 数据读取失败")) lines.append(check(False, "MoFin DB 数据读取失败"))
warn_count += 3 warn_count += 3
# 仍为 JSON 文件的检查 # 仍为 JSON 文件的检查price_events 已迁移到 DB 表,不在此列)
files = { files = {
"market.json": DATA_DIR / "market.json", "market.json": DATA_DIR / "market.json",
"price_events.json": EVENTS_PATH,
"evaluation.json": EVALUATION_PATH, "evaluation.json": EVALUATION_PATH,
"accuracy_stats.json": ACCURACY_PATH, "accuracy_stats.json": ACCURACY_PATH,
} }
@@ -121,7 +119,7 @@ def run():
else: else:
ok_count += 1 ok_count += 1
# 4. 价格事件统计 # 4. 价格事件统计(只读 DB price_events 表,JSON 已退役)
lines.append("") lines.append("")
lines.append("【价格事件】") lines.append("【价格事件】")
try: try:
@@ -130,10 +128,12 @@ def run():
ev_list = query_price_events(conn, limit=50000) ev_list = query_price_events(conn, limit=50000)
today_events = query_price_events_by_date(conn, now.strftime("%Y-%m-%d")) today_events = query_price_events_by_date(conn, now.strftime("%Y-%m-%d"))
conn.close() conn.close()
except Exception: except Exception as e:
events = load_json(EVENTS_PATH, {"events": []}) ev_list = []
ev_list = events.get("events", []) today_events = []
today_events = [e for e in ev_list if e.get("date") == now.strftime("%Y-%m-%d")] lines.append(check(False, f"DB价格事件读取失败: {str(e)[:60]}"))
issues.append(f"price_events DB读取失败: {str(e)[:60]}")
warn_count += 1
lines.append(check(len(ev_list) > 0, f"历史事件: {len(ev_list)}")) lines.append(check(len(ev_list) > 0, f"历史事件: {len(ev_list)}"))
lines.append(check(len(today_events) > 0, f"今日事件: {len(today_events)}")) lines.append(check(len(today_events) > 0, f"今日事件: {len(today_events)}"))
if len(ev_list) == 0: if len(ev_list) == 0:
+1 -1
View File
@@ -21,7 +21,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Callable from typing import Optional, Callable
DATA_DIR = Path(__file__).parent / "data" DATA_DIR = Path("/home/hmo/MoFin/data") # 绝对路径:全系统唯一权威数据目录
DB_PATH = DATA_DIR / "mofin.db" DB_PATH = DATA_DIR / "mofin.db"
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
+1 -1
View File
@@ -21,7 +21,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Callable from typing import Optional, Callable
DATA_DIR = Path(__file__).parent / "data" DATA_DIR = Path("/home/hmo/MoFin/data") # 绝对路径:全系统唯一权威数据目录
DB_PATH = DATA_DIR / "mofin.db" DB_PATH = DATA_DIR / "mofin.db"
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════