From 9914ff94573c359542d1098669172bbba55a64da Mon Sep 17 00:00:00 2001 From: hmo Date: Tue, 21 Jul 2026 23:31:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=9B=AF=E7=9B=98):=20=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E5=8C=BA=E5=9F=9F=E7=BD=AE=E9=A1=B6=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=20+=20tag=E4=B8=8EXMPP=E5=8A=A8=E4=BD=9C=E7=BA=A7?= =?UTF-8?q?=E4=BF=A1=E5=8F=B7=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mofin_db: write_holding_strategy 内置tag同步语义——动作级信号 (买入/可买入/可加仓/卖出/止盈)→current_recommend; 信号降级→ 清除current_recommend; active_manual人工标记永不被自动流覆盖/清除 - 新增 sync_recommend_tag() 供裸SQL调用方 - batch_reassess.save_result / per_stock stage-2 调用同步 - 盯盘Tab: '重点推荐'更名'推荐操作', 区域独立琥珀色视觉, 置顶 同步语义: XMPP买入信号(ACTION级告警)的个股=推荐操作区域个股, 信号消失(重评降级)时区域同步消失 --- deploy/profile-scripts/batch_reassess.py | 5 ++- deploy/profile-scripts/per_stock_reassess.py | 10 ++++- mofin_db.py | 46 +++++++++++++++++--- static/index.html | 13 +++--- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/deploy/profile-scripts/batch_reassess.py b/deploy/profile-scripts/batch_reassess.py index 5a464459..1bc1c211 100644 --- a/deploy/profile-scripts/batch_reassess.py +++ b/deploy/profile-scripts/batch_reassess.py @@ -17,7 +17,7 @@ from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, "/home/hmo/MoFin") from llm_client import call_llm, REASSESS_MODEL, gateway_alive, ocg_alive -from mofin_db import snapshot_strategy_history +from mofin_db import snapshot_strategy_history, sync_recommend_tag DB = "/home/hmo/MoFin/data/mofin.db" COOLDOWN_HOURS = 1 @@ -359,6 +359,9 @@ def save_result(code, full_text, parsed): sql = f"UPDATE holding_strategies SET {', '.join(updates)} WHERE code=? AND status='active'" conn.execute(sql, params) conn.commit() + + # ── 推荐操作 tag 同步(与 XMPP 动作级信号同源)── + sync_recommend_tag(conn, code, parsed.get("signal", "")) # 买入信号→推XMPP通知(在conn close前执行) if parsed.get("signal") == "买入": diff --git a/deploy/profile-scripts/per_stock_reassess.py b/deploy/profile-scripts/per_stock_reassess.py index f02b5be4..b84fd8fc 100644 --- a/deploy/profile-scripts/per_stock_reassess.py +++ b/deploy/profile-scripts/per_stock_reassess.py @@ -557,8 +557,14 @@ def main(): if _sig_line: _sig = '买入' if '买入' in _sig_line[0] else '关注' if '关注' in _sig_line[0] else '观望' if '观望' in _sig_line[0] else '卖出' if '卖出' in _sig_line[0] else '' if _sig: - __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute( - "UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", (_sig, code)).connection.commit() + _ts_conn = __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db') + _ts_conn.execute( + "UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", (_sig, code)) + _ts_conn.commit() + # 推荐操作 tag 同步(与 XMPP 动作级信号同源) + from mofin_db import sync_recommend_tag + sync_recommend_tag(_ts_conn, code, _sig) + _ts_conn.close() print(f" ✅ LLM信号={_sig} 已写入") # 买入信号→推XMPP if _sig == "买入": diff --git a/mofin_db.py b/mofin_db.py index 6e4ba6ab..605e5b24 100644 --- a/mofin_db.py +++ b/mofin_db.py @@ -1108,6 +1108,28 @@ def get_prices_batch_from_db(codes: list[str]) -> dict: # 核心写函数 — 替代 json.dump(),强制币种约束 # ═══════════════════════════════════════════════════════════════════ +def sync_recommend_tag(conn, code: str, timing_signal: str): + """裸 SQL 调用方(batch_reassess / per_stock_reassess)的推荐 tag 同步。 + 动作级信号 → current_recommend;信号降级 → 清除 current_recommend; + active_manual(人工标记)永不动。与 XMPP 动作级告警同源(红线#12)。""" + _ACTION_SIGNALS = ("买入", "可买入", "可加仓", "卖出", "止盈") + try: + if timing_signal in _ACTION_SIGNALS: + conn.execute( + "UPDATE holding_strategies SET tag='current_recommend' " + "WHERE code=? AND status='active' AND (tag IS NULL OR tag != 'active_manual')", + (code,)) + conn.commit() + elif timing_signal: + conn.execute( + "UPDATE holding_strategies SET tag='' " + "WHERE code=? AND status='active' AND tag='current_recommend'", + (code,)) + conn.commit() + except Exception as e: + print(f" [TAG SYNC] {code} 失败: {e}", flush=True) + + def snapshot_strategy_history(conn, code: str, source_trigger: str = "write_holding_strategy"): """在修改前快照当前策略到 strategy_history 表。永不抛异常。""" try: @@ -1162,12 +1184,17 @@ def write_holding_strategy(conn, code: str, name: str, data: dict, quality_issues_j = _json.dumps(data.get('quality_issues', {}), ensure_ascii=False) if isinstance(data.get('quality_issues'), dict) else data.get('quality_issues_json', '') signal_factors_j = _json.dumps(data.get('signal_factors', []), ensure_ascii=False) if isinstance(data.get('signal_factors'), list) else data.get('signal_factors_json', '') - # 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空) + # ── 推荐操作 tag 同步语义(与 XMPP 动作级信号同源,红线#12)── + # 动作级信号 → tag=current_recommend(进盯盘"推荐操作"区) + # 信号降级 → 清除 current_recommend(区域同步消失) + # active_manual(人工标记)永远不被自动流程覆盖或清除 + _ACTION_SIGNALS = ("买入", "可买入", "可加仓", "卖出", "止盈") _existing_fa = data.get('full_analysis', '') _existing_ra = data.get('reassessed_at', '') - # tag 语义:'tag' 键缺席=保留旧标签;显式传入(含'')= 按传入值(允许清除标签) _tag_absent = 'tag' not in data - _existing_tag = data.get('tag', '') or '' + _new_sig = data.get('timing_signal', '') or '' + _explicit_tag = data.get('tag', None) + _old_tag = '' if not _existing_fa or _tag_absent: try: _old = conn.execute("SELECT full_analysis, reassessed_at, tag FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone() @@ -1175,10 +1202,19 @@ def write_holding_strategy(conn, code: str, name: str, data: dict, if not _existing_fa: if _old[0]: _existing_fa = _old[0] if _old[1]: _existing_ra = _old[1] - if _tag_absent and _old[2]: - _existing_tag = _old[2] + _old_tag = _old[2] or '' except: pass + if _old_tag == 'active_manual': + _existing_tag = 'active_manual' # 人工标记不可动 + elif _explicit_tag is not None: + _existing_tag = _explicit_tag # 显式传入优先(含''清除) + elif _new_sig in _ACTION_SIGNALS: + _existing_tag = 'current_recommend' # 动作级信号 → 自动推荐 + elif _new_sig and _old_tag == 'current_recommend': + _existing_tag = '' # 信号降级 → 清除自动推荐 + else: + _existing_tag = _old_tag # 其余保留 # ── 类型守卫:shares 必须是数值,防止字符串写入导致下游崩溃 ── _shares = data.get('shares', 0) diff --git a/static/index.html b/static/index.html index 9cab2afc..cf08847b 100644 --- a/static/index.html +++ b/static/index.html @@ -1383,9 +1383,9 @@ async function renderWatch() { return; } - // 分组渲染 + // 分组渲染(推荐操作区域独立于持仓/自选,置顶且与XMPP推荐同步) const groups = [ - { label: '🔥 重点推荐', g: 0 }, + { label: '🔥 推荐操作', g: 0 }, { label: '💼 持仓策略', g: 1 }, { label: '⭐ 自选策略', g: 2 }, ]; @@ -1408,9 +1408,12 @@ async function renderWatch() { const items = stocks.filter(s => s.sort_group === grp.g); if (items.length === 0) continue; - // 分组标题行 - html += '' + - '' + grp.label + ' (' + items.length + ')' + + // 分组标题行(推荐操作区域独立视觉) + const hdrCls = grp.g === 0 + ? 'border-y border-amber-500/50 bg-amber-900/25' + : 'border-b border-slate-700'; + html += '' + + '' + grp.label + ' (' + items.length + ')' + ''; for (const s of items) {