# MoFin 开发规范 > 版本:1.1 | 日期:2026-07-03(1.1 更新 2026-08-10)| 维护:Sisyphus + Zhiwei > 1.1 新增:Cron 单例守卫/600s超时/LLM容错/调度单一(5.3-5.6)+ 部署规范(五·补 D1-D4) --- ## 一、代码结构 ### 1.1 无重复代码 - **一个功能一个文件**。禁止出现两份同名/同功能的 `.py` 文件。 - 公共逻辑抽取到 `mo_models.py` / `mo_data.py` / `mofin_db.py`,各脚本只调不写。 - 每次新增功能前,先搜索是否已有类似实现。 ### 1.2 文件职责 | 文件 | 职责 | 允许做的事 | |------|------|-----------| | `mo_models.py` | 统一数据模型 | `calc_total_assets`, `is_hk_stock`, `to_cny`, `get_hk_rate` | | `mo_data.py` | 统一读取层 | `read_portfolio`, `read_decisions`, `read_watchlist` — 只读 DB | | `mofin_db.py` | DB 层 | 建表、写函数、查询函数 — 所有 SQL 集中在这里 | | `price_monitor.py` | 价格更新 | 从 API 拉价格,写入 holdings + portfolio_summary | | `strategy_lifecycle.py` | 策略生命周期 | `regenerate_all` + quality gates | | `scripts/` | 工具/一次性脚本 | 不通过 cron 调用的辅助工具 | ### 1.3 禁止事项 - ❌ 在业务脚本里直接写 SQL(必须通过 `mofin_db.py`) - ❌ 在业务脚本里直接 `json.load(open(...))` 读数据(必须通过 `mo_data.py`) - ❌ 自己实现 `calc_total_assets` / `is_hk_stock`(必须用 `mo_models.py`) - ❌ 自己实现 `read_portfolio` / `read_decisions` / `read_watchlist`(必须用 `mo_data.py`) - ❌ 新增 JSON 数据文件(所有持久化数据必须走 DB) --- ## 二、数据规范 ### 2.1 币种 | 场景 | 港股 | A股 | |------|------|-----| | 个股 price/cost/stop_loss | HKD | CNY | | `currency` 字段 | `'HKD'` | `'CNY'` | | 总资产/总市值 | CNY(汇总时 × 汇率) | CNY | | 个股 P&L 计算 | 同币种 `(price-cost)/cost` | 同币种 | | API/报告输出 | 标注 `(HKD)` | 无标注 | **铁律**:禁止跨币种比较或加减。`calc_total_assets` / `calc_total_mv` 是唯一可以做币种转换的地方。 ### 2.2 汇率 - 汇率必须通过 `get_hk_rate()` 获取(实时 API + 缓存) - 禁止硬编码 `0.87` / `0.866` 等值(仅 `hk_rate.py` 内部兜底例外) - 汇率 API 不可达时自动使用上次缓存值 ### 2.3 数据源 - **唯一写入源**:`price_monitor.py` 是唯一的价格写入者 - **DB 优先读取**:所有数据读取走 `mo_data.py` → DB - **API 兜底**:DB 无数据时,`price_monitor` 从外部 API 拉取 - **禁止各脚本各自拉 API** 写数据 --- ## 三、DB 规范 ### 3.1 表设计 - 新增表必须在 `mofin_db.py` 的 `init_all_tables()` 中定义 - 新表必须有对应的 `write_*()` 函数和 `query_*()` 函数 - 涉及币种的列必须有 `CHECK(currency IN ('CNY','HKD'))` 约束 - `code` 列建议加 UNIQUE INDEX ### 3.2 迁移 - 加列用 `ALTER TABLE ADD COLUMN`,幂等(`IF NOT EXISTS` 或 try/except) - 不直接改线上 DB 结构,通过 `mofin_db.py` 的 init 函数执行 - 数据修正脚本放到 `scripts/` 目录 --- ## 四、LLM Prompt 规范 ### 4.1 强制要求 | # | 规则 | 检查方法 | |---|------|---------| | S1 | 不引用 JSON 文件名 | grep `\.json` | | S2 | 港股价格标注 `(HKD)` | 检查 prompt 模板 | | S3 | 不指示跨币种直接比较 | 人工审查 | | S4 | 数据源标注为 DB 表名 | `strategy_evaluations` 不是 `evaluation.json` | | S5 | 不在 prompt 里硬编码路径 | grep `/home/hmo/` | ### 4.2 新增/修改 Prompt - 在 `prompt_manager/init_registry.py` 注册新版本 - 旧版本标记 `status="deprecated"`,保留历史记录 - 更新后运行测试脚本验证无 JSON 引用 --- ## 五、Cron 规范 ### 5.1 调度 - Cron 脚本必须能独立运行(`python3 xxx.py` 不报错) - 处理异常,不因单个 API 失败而崩溃 - 关键步骤加 try/except + `print(..., file=sys.stderr)` 日志 ### 5.2 幂等性 - `price_monitor` 价格不变时不写入 DB - `regenerate_all` 可重复运行不产生副作用 - 所有写操作支持重复执行 ### 5.3 单例守卫(2026-08-10 新增,铁律) **所有定时/常驻脚本必须加单例守卫,防止 cron 重复触发导致多个实例并发。** - 实测教训:`refresh_mtf_cache.py` 无守卫,cron 重复触发两个实例 → SQLite `database is locked` 长锁,拖垮候选管道 - 必须用 `fcntl.flock(LOCK_EX | LOCK_NB)` 实现(进程级,进程死自动释放) - 示例: ```python import fcntl lock_f = open("/tmp/xxx.lock", "w") try: fcntl.flock(lock_f, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: print("[XXX] 已有实例在运行,退出") return ``` - 守卫在脚本 `main()` 开头执行,持有到脚本结束 ### 5.4 600s 超时限制(2026-08-10 新增,铁律) **hermes cron `child_timeout_seconds=600`,脚本运行超 600s 会被 SIGTERM 杀掉(exit code -15)。** - 实测教训:`promote_candidates.py` 处理 240 候选 × 18s/个 >> 600s 被杀 - **重型脚本必须控制单次运行时长**: - 分批处理(每批限 N 个,剩余留待下批) - 内部子进程超时缩短(如重评 480s→120s) - 总时长护栏(如 500s,留 100s 余量) - 循环中定期检查 `time.time() - START_TIME > BUDGET`,超时 break ### 5.5 LLM 依赖容错(2026-08-10 新增) **依赖 LLM API 的 cron(简报/洞察/推荐)必须处理"LLM 失败"情况:** - 实测教训:开盘简报 LLM 返回空流 → 静默失败无输出,状态未记录 - 要求: - LLM 调用失败时输出明确错误标记(非静默) - jobs.json 状态正确记录失败(不显示 ok) - 有重试机制(3次内重试) - 失败时仍输出已生成的模板部分(不整体丢弃) ### 5.6 调度通道单一(2026-08-10 新增) **同一脚本只保留一个调度入口,避免重复触发:** - 实测教训:cron_to_xmpp 有主 profile + position-analyst + 系统 crontab 三处调度,可能重复推送 - 规范:一个脚本一个调度(选择最优的 profile/crontab),废弃的调度显式移除或禁用 --- ## 五·补、部署规范(2026-08-10 新增,实战踩坑教训) ### D1. deploy_guard 机制(必须遵守) **deploy_guard 监控 CODE_PATHS(deploy/profile-scripts 等),发现未入库改动即回滚到 git HEAD。** - ❌ 禁止:直接改 246 上 deploy/profile-scripts 的脚本而不 commit → 会被守卫回滚 - ✅ 正确流程: ``` 1. 修改脚本(246 或本地) 2. 立即 commit:GIT_ALLOW_COMMIT=1 git commit -m 'fix: 说明' 3. push:git push https://hmo@git.yoin.fun/hmo/MoFin.git master 4. 守卫下次运行看到 CODE_PATHS 干净 → 不回滚 ``` ### D2. 提交白名单 - pre-commit 钩子要求 `GIT_ALLOW_COMMIT=1` 才放行(知微无此令牌 = 无法提交) - **小小莫本人操作显式携带该令牌**(有 git 写权限) - 知微的修改路径:kanban 提单给小小莫评审执行 ### D3. push 身份 - **必须用 hmo 身份 push**(`git push https://hmo@git.yoin.fun/...`),store 凭据 - ❌ 禁止改 `git config user.name` 为 xxm/mohe(凭据匹配错身份 → push 被拒) ### D4. 修改后验证 - 每次修改必须验证:`python3 -u xxx.py` 实际运行(看是否报错/超时/锁冲突) - 验证结果记录到 CHANGELOG 或部署文档 --- ## 六、开发流程 ### 6.1 日常开发步骤 ``` 1. 理解需求 → 检查现有代码 → 确定改动范围 2. 本地修改代码 3. 运行 scripts/run_all_tests.py(必须全部通过) 4. 如有新增功能,补充 TEST_PLAN.md 测试用例 5. scp 部署到服务器 6. 在服务器上再次运行 run_all_tests.py 7. git commit + push 8. 如涉及 server.py 修改,重启 Flask 9. 更新 CHANGELOG.md ``` ### 6.2 测试要求 **每次代码变更后必须:** ```bash # 在服务器上运行全面测试 cd /home/hmo/MoFin python3 scripts/run_all_tests.py ``` **输出必须满足:** - 0 个 `❌` - 最多 1-2 个 `⚠️`(已知问题,已记录在案) **如测试不通过:** 1. 修复问题 2. 重新测试 3. 不通过不提交 ### 6.3 文档同步 **哪些文档需要同步更新:** | 改动内容 | 需更新的文档 | |---------|------------| | 新增/修改表 | `docs/DATABASE_ARCHITECTURE.md` | | 新增/修改 API | `docs/SYSTEM_ARCHITECTURE.md` | | 架构变更 | `docs/SYSTEM_ARCHITECTURE.md` | | 数据模型变更 | `docs/portfolio-data-model.md` | | 任何功能变更 | `CHANGELOG.md`(追加顶部) | | 新增测试用例 | `docs/TEST_PLAN.md` | ### 6.4 提交规范 - Commit message 格式:`type: 简短描述` - 类型:`fix:` / `feat:` / `refactor:` / `docs:` / `test:` / `migrate:` - 一次 commit 一个逻辑变更,不要混搭 --- ## 七、快速检查清单 **每次提交前自检:** ``` □ run_all_tests.py 全部通过 □ 无新增 json.load / json.dump 数据文件 □ 港股 currency='HKD'、A股 currency='CNY' □ 无硬编码汇率 □ 无重复代码 □ CHANGELOG.md 已更新 □ 无残留 print 调试语句 □ git status 确认只改了应改的文件 ``` --- ## 八、紧急联系人 | 角色 | 负责 | |------|------| | 小小莫 | 架构、DB、重构、price_monitor | | 知微 | 策略、LLM prompt、cron、日常运营 |