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 += '