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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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 "关注"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
"""持仓列表"""
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user