diff --git a/CHANGELOG.md b/CHANGELOG.md index 075e1840..91090456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -217,3 +217,46 @@ Dad 反馈我的 v4 手动分析(买入区3.62~3.72)与 13:01 cron 报告( - `per_stock_reassess 688981` → 输出 `[DB] holding_strategies 已更新: 688981` - DB holding_strategies 从 1 条 → 17 条 - web-dashboard PID 4003859 正常运行,API 通过 DB 读取 + +## 2026-07-10 — 知微XMPP Bot离线修复 + +### 根因 +Bot被bash包装器手动启动(非systemd),形成孤儿进程。systemd因PID文件冲突僵在`activating (auto-restart)`循环无法接管。孤儿bot在11:40:48断线后卡在重连循环中。 + +### 修复 +1. 杀死孤儿进程 PID 422464 +2. 清理 `/home/hmo/.hermes/zhiwei_bot.pid` 和 `/tmp/xmpp_zhiwei_bot.pid` +3. systemd RestartSec=10s 自动重启接管 +4. 验证:加入coregroup、XMPP就绪、presence available、bridge:5805正常、gateway正常 + +### 预防 +自愈系统新增检查项:systemd状态优先于进程检查——发现进程存在但非systemd管理时自动清理孤儿。 + +## 2026-07-13 — XMPP Bot非阻塞 + 深套规则重构 + 换股规划 + +### XMPP Bot修复 +- gateway调用改为 45s短超时 → "处理中"预回复 → 后台300s轮询,不再阻塞事件循环 +- 远程XMPP服务器不通,改为连本地 127.0.0.1:5222 代理 +- 注册 xep_0045 修复MUC崩溃 +- **涉及文件**: /home/hmo/xmpp_agent_core.py + +### price_monitor推送冷却持久化 +- _push_cooldown 从内存dict改为文件持久化,解决每2分钟重复推操作区间通知 +- **涉及文件**: scripts/price_monitor.py + +### 深套股规则重构 +- 删除全部 `is_deep_loss` 特殊处理(止损/止盈/买入区/质量检查6处guard + action_note覆盖) +- 回归客观判断,该加就加该割就割 +- **涉及文件**: scripts/strategy_lifecycle.py + +### 换股规划功能 +- `python3 strategy_lifecycle.py swap-plan [--need 金额]` +- 五维评分排序,自动计算释放资金方案 +- **涉及文件**: scripts/strategy_lifecycle.py + +### 技术分析增强 +- 周线/月线趋势/MA/支撑阻力注入mtf_context传递到LLM +- 健康Tab写无读警告 3→0 +- 每日汇总只发一次(16:35) +- 健康监控新增数据实体检查 +- **涉及文件**: technical_analysis.py, mofin_health.py, cron_to_xmpp.py, cron_health_monitor.py diff --git a/docs/analyst-knowledge-log.md b/docs/analyst-knowledge-log.md index a4d7afa9..01cddbc7 100644 --- a/docs/analyst-knowledge-log.md +++ b/docs/analyst-knowledge-log.md @@ -13,7 +13,27 @@ ### 系统架构变更 -**自选统一:** +## [2026-07-10 11:42] 知微XMPP Bot离线自愈修复 + +### 发现 +自愈执行器报告知微XMPP Bot离线。调查发现: +- Bot进程(PID 422464)被bash包装器手动启动,非systemd管理 +- systemd xmpp-zhiwei.service 因PID文件冲突僵在 `activating (auto-restart)` +- 孤儿bot在11:40:48断线后无法恢复连接 + +### 修复 +1. 杀死孤儿进程 `kill -TERM 422464` +2. 删除PID文件 `rm -f /home/hmo/.hermes/zhiwei_bot.pid /tmp/xmpp_zhiwei_bot.pid` +3. systemd自动重启,bot干净上线 +4. 验证:presence available、加入coregroup、bridge正常 + +### 教训 +bash包装器启动bot会绕开systemd管理,导致: +- systemd状态监测不准确(显示restarting而非running) +- 孤儿进程断开后无法被systemd接管 +- 已有 `xmpp-bot-infrastructure` skill 记录了孤儿进程陷阱,自愈系统可直接参考执行 + +自选统一:** - 自选数据统一存储在 `holding_strategies(decision_type='自选策略')` - `watchlist_stocks` 表已废弃(is_active=0) - `read_watchlist()` 改为从 `holding_strategies` 读取 @@ -66,10 +86,7 @@ - **调查**: 38条为 xiaoguo 来源信号,属4h时效内积压。当前已完成消费(xiaoguo_signal_consumer每30分运行),积压自愈 - **监控盲区发现**: intraday_health_check.py 和 system_audit.py 均只监控 `source LIKE 'xiaoguo%'`,忽略 divergence_watch(80)、trend(46) 等来源的126条未处理信号 - **修复**: 两处均增加全量未处理查询,区分 xiaoguo 和其他来源分别报告 -- **涉及文件**: - - `scripts/intraday_health_check.py` — check_signal_pipeline: 增加 total_unproc 查询和非xiaoguo预警 - - `scripts/system_audit.py` — audit_signals: 增加 total_unproc 查询和MEDIUM级别溢出预警 - - `CHANGELOG.md` — 追记监控盲区修复 +- **涉及文件**: scripts/intraday_health_check.py, scripts/system_audit.py, CHANGELOG.md ## [2026-07-09 14:xx] 自愈执行器升级:信号积压126条(divergence_watch/trend无consumer) @@ -78,3 +95,57 @@ - **修复**: divergence_detector.py — INSERT时加processed=1(归档性质不需要consumer)。DB — 80条divergence_watch + 46条trend历史标记为processed。同步cron目录MD5一致 - **效果**: signal_news 1077条全量已处理,健康检查PASS,后续不会再次积压 - **涉及文件**: scripts/divergence_detector.py, data/mofin.db, CHANGELOG.md + +## [2026-07-10 10:25] 自愈:知微XMPP Bot离线 — 孤儿进程阻塞systemd + +- **发现**: 盘中自检报警「知微XMPP Bot离线」,TODO 83 created(无fix_action,升级到LLM处理) +- **调查**: + - systemd `xmpp-zhiwei.service` 陷入无限重启循环(重启计数器110+) + - 根因:外部bash wrapper脚本在10:04启动了一个孤儿bot进程(PID 252975),该进程占用了`/tmp/xmpp_zhiwei_bot.pid`和端口5805 + - systemd每次尝试启动都因PID检查`xmpp_zhiwei_bot already running (PID 252975), exiting.`而退出 + - 实际上PID 252975的bot是正常运行的(已加入群聊coregroup,HTTP桥5805正常响应),只是不受systemd管理。wrapper触发源不明(可能自愈系统之前的升级操作) +- **修复**: + 1. `kill -TERM 252975` 杀掉孤儿进程 + 2. 清理旧PID文件 `/tmp/xmpp_zhiwei_bot.pid` 和 `/home/hmo/.hermes/zhiwei_bot.pid` + 3. systemd 10秒RestartSec后自动启动新实例 + 4. TODO 83 标记为completed +- **验证**: systemd active(running) since 10:25:43,bot加入群聊coregroup就绪,gateway health ok,HTTP桥5805正常 +- **效果**: systemd恢复管控bot,重启循环归零 +- **涉及文件**: /home/hmo/xmpp_zhiwei_bot.py (PID_FILE机制) + +## [2026-07-13] XMPP Bot非阻塞修复 + 深套股规则重构 + 换股规划 + +### XMPP Bot修复 +- **问题1**: bot调用gateway同步等600秒,阻塞事件循环 → XMPP心跳发不出 → 被踢下线 → 重试→turn堆积→更慢 +- **修复**: 45s短超时 + "处理中"预回复 + 后台300s轮询 + 不再阻塞心跳 +- **问题2**: 远程XMPP服务器(120.78.123.183:5222)网络不通,bot连不上 +- **修复**: bot.connect(host='127.0.0.1', port=5222) 改连本地XMPP代理 +- **问题3**: xep_0045 MUC插件未注册导致崩溃 +- **修复**: __init__加 self.register_plugin('xep_0045') +- **涉及文件**: /home/hmo/xmpp_agent_core.py + +### price_monitor通知噪音修复 +- **问题**: _push_cooldown 是内存dict,price_monitor每2分钟新进程运行 → cooldown每次重置 → 同股同区间反复推送 +- **修复**: cooldown持久化到 /home/hmo/.hermes/.price_push_cooldown.json +- **涉及文件**: scripts/price_monitor.py + +### 深套股规则重构(Dad铁律确认) +- **删除**: `is_deep_loss` 对止损/止盈/买入区/质量检查的全部6处guard +- **删除**: `action_note="深套持有"` 强制覆盖 +- **删除**: `position_advice="不补不割"` / `time_horizon="长期"` +- **保留**: `stock_category="深套"` 仅作标签 +- **原则**: 一切根据客观判断,该加就加该割就割 +- **涉及文件**: scripts/strategy_lifecycle.py + +### 换股规划功能 +- `python3 strategy_lifecycle.py swap-plan [--need 金额]` +- 五维评分: 亏损程度25% + 仓位20% + 技术趋势25% + 流动性15% + 前景15% +- 按评分排序输出可替换优先级,--need 参数自动计算卖几只够 +- **涉及文件**: scripts/strategy_lifecycle.py + +### 技术分析增强 +- weekly/monthly趋势/MA/支撑阻力注入mtf_context +- 健康Tab写无读警告从3个归零(stock_weekly/monthly/watchlist_log加manual_readers) +- 每日汇总只发一次(16:35 + sent标记防重复) +- cron_health_monitor新增数据实体完整性检查 +- 涉及文件: scripts/technical_analysis.py, scripts/strategy_lifecycle.py, scripts/mofin_health.py, scripts/cron_to_xmpp.py, scripts/cron_health_monitor.py diff --git a/scripts/per_stock_reassess.py b/scripts/per_stock_reassess.py index 59b80a7b..c5c1d172 100644 --- a/scripts/per_stock_reassess.py +++ b/scripts/per_stock_reassess.py @@ -229,11 +229,11 @@ def main(): continue entry = decisions_map.get(code) if not entry: - # 不在 decisions 中的自选股 → 从 DB watchlist_stocks 构建entry + # 不在 decisions 中的自选股 → 从 holding_strategies 构建entry import sqlite3 - _db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') + _db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') _db.row_factory = sqlite3.Row - _wl = _db.execute("SELECT * FROM watchlist_stocks WHERE code=? AND is_active=1", (code,)).fetchone() + _wl = _db.execute("SELECT * FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'", (code,)).fetchone() _db.close() if _wl: entry = { @@ -299,7 +299,13 @@ def main(): skipped += 1 continue # 打印参数调试 - print(f" DEBUG: code={code} name={entry.get('name','')} price={price} cost={entry.get('cost')} shares={entry.get('shares')} action={entry.get('action','')[:30]} is_wl={entry.get('type','') in ('自选策略','watchlist')}", flush=True) + if entry is None: + print(f" DEBUG: code={code} ENTRY=NONE 跳过") + print(f" [SKIP] {code} 策略数据不存在") + skipped += 1 + continue + entry_action = str(entry.get('action') or '') + print(f" DEBUG: code={code} name={entry.get('name','')} price={price} cost={entry.get('cost')} shares={entry.get('shares')} action={entry_action[:30]} is_wl={entry.get('type','') in ('自选策略','watchlist')}", flush=True) result = reassess_strategy( code=code, name=entry.get("name", ""), diff --git a/scripts/promote_candidates.py b/scripts/promote_candidates.py index 33b8549d..83718d80 100644 --- a/scripts/promote_candidates.py +++ b/scripts/promote_candidates.py @@ -56,6 +56,19 @@ def main(): print(f" ⏭ {code} {name} 已在自选中,标记promoted") continue + # 验证实时价格:无有效价格的候选股不入自选(防假数据污染) + try: + import subprocess, json as _jj + _r = subprocess.run(["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/stock_quote.py", code], + capture_output=True, text=True, timeout=10) + _q = _jj.loads(_r.stdout) + if float(_q.get("price", 0)) <= 0: + print(f" ⏭ {code} {name} 无实时价格,跳过") + continue + except Exception as _e: + print(f" ⏭ {code} {name} 价格获取失败({_e}),跳过") + continue + # 构建策略 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") timing_signal = "买入" if score >= 7 else "关注" diff --git a/scripts/stale_detector.py b/scripts/stale_detector.py index 4f71089b..93412551 100644 --- a/scripts/stale_detector.py +++ b/scripts/stale_detector.py @@ -136,7 +136,9 @@ def main(): if not cur_price or cur_price <= 0: continue drift = (cur_price / center - 1) * 100 - if abs(drift) > 15: + # 触发条件:价格偏离>15% 或 买入区明确错误(价格完全在区间外且偏离>50%) + price_outside = cur_price < wl_el or cur_price > wl_eh + if abs(drift) > 15 or (price_outside and abs(drift) > 50): reassess_scripts.append(code) print(f"[AUTO_REASSESS] {name}({code}) 价{cur_price:.2f}偏离买入区中心{center:.2f} {drift:+.0f}% → 触发重评") if reassess_scripts: diff --git a/scripts/stale_push_wlin.py b/scripts/stale_push_wlin.py index 4c76767b..86ac58f7 100644 --- a/scripts/stale_push_wlin.py +++ b/scripts/stale_push_wlin.py @@ -363,39 +363,6 @@ def main(): dec = read_decisions() for e in dec.get("decisions", []): code_data[e["code"]] = e - # 补充watchlist_stocks中不在holding_strategies的自选股 - import sqlite3 as _sq3 - _wl_db = _sq3.connect('/home/hmo/MoFin/data/mofin.db') - _wl_db.row_factory = _sq3.Row - _wl_rows = _wl_db.execute( - "SELECT code, name, entry_low, entry_high, stop_loss, analysis_json " - "FROM watchlist_stocks WHERE is_active=1 AND entry_low > 0" - ).fetchall() - _wl_db.close() - for _w in _wl_rows: - _c = str(_w["code"]) - if _c in code_data: - continue - _aj = json.loads(_w["analysis_json"]) if _w["analysis_json"] else {} - code_data[_c] = { - "code": _c, - "name": _w["name"] or "", - "price": 0, - "entry_low": _w["entry_low"], - "entry_high": _w["entry_high"], - "stop_loss": _w["stop_loss"] or 0, - "take_profit": _aj.get("take_profit", 0), - "rr_ratio": _aj.get("rr", 0), - "tech_snapshot": _aj.get("tech_snapshot", ""), - "timing_signal": _aj.get("action", ""), - "stock_category": "", - "sector_context": "", - "signal_factors": [], - "name": _w["name"] or "", - "shares": 0, - "cost": 0, - "price": 0, - } except Exception as _e: print(f"[DB_LOAD FAIL] {_e}", file=sys.stderr) @@ -498,8 +465,8 @@ def main(): eh = row[5] or 0 if price <= 0 or el <= 0 or eh <= 0: continue - # 价格必须在买入区内或略高于(不超过5%) - if price > eh * 1.05: + # 价格必须在买入区内(硬检查,拒绝偏离仍推买入) + if price < el or price > eh: continue name = row[1] or code all_candidates.append((name, code, price, el, eh, "", False)) @@ -573,6 +540,12 @@ def main(): # 信号必须含买入/加仓才推荐——其他非操作信号跳过 if not any(kw in sig for kw in ["买入", "加仓"]): continue + # 价格必须在买入区内(硬检查,拒绝价格偏离仍推买入) + buy_low, buy_high = s[3], s[4] + price_check = s[2] + if buy_low and buy_high and buy_low > 0: + if not (buy_low <= price_check <= buy_high): + continue # RR完整性检查:买入/加仓信号必须RR>0(策略数据要完整) cd = code_data.get(s[1], {}) rr = cd.get("rr_ratio", 0) or 0 diff --git a/server.py b/server.py index 6f70df04..9e17d844 100644 --- a/server.py +++ b/server.py @@ -83,6 +83,38 @@ def index(): return send_from_directory(app.static_folder, "index.html") +@app.route("/api/watch") +def get_watch(): + """盯盘:当前有操作建议的持仓+自选""" + import sqlite3 + conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db") + conn.row_factory = sqlite3.Row + # 持仓:所有买卖操作都进盯盘;自选:只有买入/加仓信号(没持仓不可能卖) + rows = conn.execute(""" + SELECT hs.code, hs.name, hs.decision_type, lp.price, lp.change_pct, + hs.entry_low, hs.entry_high, hs.stop_loss, hs.take_profit, + hs.rr_ratio, hs.timing_signal, hs.position_advice, hs.action, + hs.full_analysis + FROM holding_strategies hs + LEFT JOIN live_prices lp ON hs.code = lp.code + WHERE hs.status='active' + AND ( + (hs.decision_type='持仓策略' AND hs.timing_signal IN ('买入','可买入','可加仓','卖出','止盈')) + OR + (hs.decision_type='自选策略' AND hs.timing_signal IN ('买入','可买入','可加仓')) + ) + ORDER BY + CASE hs.timing_signal + WHEN '买入' THEN 1 WHEN '可买入' THEN 2 WHEN '可加仓' THEN 3 + WHEN '止盈' THEN 4 WHEN '卖出' THEN 5 + ELSE 9 + END, + lp.change_pct DESC + """).fetchall() + conn.close() + return json.dumps({"stocks": [dict(r) for r in rows], "count": len(rows)}, + ensure_ascii=False) + @app.route("/api/portfolio") def api_portfolio(): """持仓列表""" diff --git a/static/index.html b/static/index.html index 85b876f6..5947e33a 100644 --- a/static/index.html +++ b/static/index.html @@ -52,6 +52,7 @@ body { background: #0f0f13; color: #e2e8f0; } + 📸 上传 @@ -68,6 +69,13 @@ body { background: #0f0f13; color: #e2e8f0; }
+ @@ -151,6 +159,7 @@ function renderTab(name) { else if (name === 'prompts') renderPrompts(); else if (name === 'market') renderMarket(); else if (name === 'signals') renderSignals(); + else if (name === 'watch') renderWatch(); } // ── Data Fetching ── @@ -1266,6 +1275,70 @@ async function renderSignals() { el.innerHTML = `