docs: 更新CHANGELOG + 开发日志记录07/13全部变更

- XMPP Bot非阻塞/本地XMPP/MUC修复
- price_monitor推送冷却持久化
- 深套股规则重构+换股规划swap-plan
- 周月线技术分析增强
- 健康Tab写无读清零
- stale_push_wlin/per_stock_reassess/promote_candidates更新
- static/index.html + mofin_health.json + server.py同步
This commit is contained in:
知微
2026-07-13 12:09:25 +08:00
parent 2109289668
commit 9239ab40c0
9 changed files with 645 additions and 358 deletions
+43
View File
@@ -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
+76 -5
View File
@@ -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:43bot加入群聊coregroup就绪,gateway health okHTTP桥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 是内存dictprice_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
+10 -4
View File
@@ -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", ""),
+13
View File
@@ -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 "关注"
+3 -1
View File
@@ -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:
+8 -35
View File
@@ -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
+32
View File
@@ -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():
"""持仓列表"""
+73
View File
@@ -52,6 +52,7 @@ body { background: #0f0f13; color: #e2e8f0; }
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="market">🌐 市场</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="signals">🔍 信号</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="health">🏥 健康</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg blink-tab" data-tab="watch" style="color:#f97316;border-color:#f97316">👁️ 盯盘</button>
<a href="/upload" class="tab-btn px-4 py-2 text-sm rounded-lg" style="text-decoration:none;color:#fbbf24;border-color:#fbbf24">📸 上传</a>
</div>
@@ -68,6 +69,13 @@ body { background: #0f0f13; color: #e2e8f0; }
<div id="tab-health" class="tab-content hidden">
<iframe src="/mofin_health.html" style="width:100%;height:100vh;border:none;background:#0d1117;border-radius:8px;"></iframe>
</div>
<div id="tab-watch" class="tab-content hidden">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-slate-200">👁️ 盯盘 · 有效操作建议</h2>
<span class="text-xs text-slate-500"><span id="watchRefreshTime"></span> <span id="watchCount" class="text-yellow-400 font-mono">0</span></span>
</div>
<div id="watchContent" class="space-y-2"></div>
</div>
</div>
<!-- Stock Detail Modal -->
@@ -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 = `<div class="text-red-400 text-sm">加载失败: ${e.message}</div>`;
}
}
// ── 盯盘 ──
let watchTimer = null;
async function renderWatch() {
const el = document.getElementById('watchContent');
if (!el) return;
try {
// 清除旧定时器,切走时停
if (watchTimer) clearInterval(watchTimer);
const data = await fetchJSON('/api/watch');
const stocks = data.stocks || [];
document.getElementById('watchCount').textContent = stocks.length;
document.getElementById('watchRefreshTime').textContent = new Date().toLocaleTimeString();
if (stocks.length === 0) {
el.innerHTML = '<div class="card p-6 text-center text-slate-500 text-sm">暂无有效操作建议</div>';
return;
}
let html = '';
for (const s of stocks) {
const p = s.price || 0;
const cp = s.change_pct || 0;
const sig = s.timing_signal || '';
const isBuy = sig.includes('买入') || sig.includes('加仓');
const isSell = sig.includes('卖出') || sig.includes('止盈');
const sigColor = isBuy ? 'bg-green-900/50 text-green-300' : isSell ? 'bg-red-900/50 text-red-300' : 'bg-yellow-900/50 text-yellow-300';
const chgColor = cp >= 0 ? 'text-red-400' : 'text-green-400';
const el_ = s.entry_low || 0;
const eh_ = s.entry_high || 0;
const sl_ = s.stop_loss || 0;
const tp_ = s.take_profit || 0;
const rr = s.rr_ratio || 0;
const inZone = (p && el_ && eh_ && p >= el_ && p <= eh_) ? '✅区内' : (p && eh_ && p > eh_) ? '⬆️超出' : '⬇️低于';
html += `
<div class="card p-4 flex items-center gap-3" onclick="openStock('${s.code}')" style="cursor:pointer">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-semibold text-sm text-slate-200">${s.code}</span>
<span class="text-slate-400 text-xs">${s.name || ''}</span>
<span class="text-xs px-1.5 py-0.5 rounded ${sigColor}">${sig}</span>
<span class="text-xs text-slate-500">${s.decision_type==='持仓策略'?'💼':'⭐'}</span>
</div>
<div class="flex items-center gap-3 mt-1 text-xs text-slate-400">
<span class="font-mono">${p.toFixed(2)}</span>
<span class="font-mono ${chgColor}">${cp >= 0 ? '+' : ''}${cp.toFixed(2)}%</span>
<span>区${el_.toFixed(1)}~${eh_.toFixed(1)}</span>
<span>${inZone}</span>
<span>RR ${rr.toFixed(2)}</span>
<span>损${sl_.toFixed(1)}${tp_.toFixed(1)}</span>
</div>
</div>
<div class="text-right text-xs">
<div class="text-slate-400">仓位</div>
<div class="text-yellow-400 font-mono">${s.position_advice || '—'}</div>
</div>
</div>`;
}
el.innerHTML = html;
// 自动刷新:每30秒
watchTimer = setInterval(renderWatch, 30000);
} catch(e) {
el.innerHTML = `<div class="text-red-400 text-sm">加载失败: ${e.message}</div>`;
watchTimer = setInterval(renderWatch, 60000);
}
}
</script>
</body>
</html>
+387 -313
View File
File diff suppressed because it is too large Load Diff