From c91d4c33f042997c9392aa25f2c83f1f48cee970 Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 22 Jul 2026 10:29:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=A1=E5=8F=B7-=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=EF=BC=88reconcile=E4=BB=A5=E5=B7=B2=E5=AD=98?= =?UTF-8?q?=E5=88=86=E6=9E=90=E4=B8=BA=E5=94=AF=E4=B8=80=E4=BA=8B=E5=AE=9E?= =?UTF-8?q?=E6=BA=90=EF=BC=89=20+=20prompt=E6=8C=81=E4=BB=93=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=EF=BC=88=E5=B7=B2=E6=8C=81=E6=9C=89/?= =?UTF-8?q?=E6=9C=AA=E6=8C=81=E6=9C=89=E5=88=86=E5=BC=80=E7=BB=99=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=EF=BC=8C=E7=A6=81=E6=AD=A2=E4=B8=A4=E5=A4=B4=E5=86=99?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/batch_reassess.py | 30 ++++++++++++++-- deploy/profile-scripts/per_stock_reassess.py | 36 +++++--------------- mofin_db.py | 36 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/deploy/profile-scripts/batch_reassess.py b/deploy/profile-scripts/batch_reassess.py index cf7d1964..7d5d5710 100644 --- a/deploy/profile-scripts/batch_reassess.py +++ b/deploy/profile-scripts/batch_reassess.py @@ -95,6 +95,14 @@ def collect_data(code): data["changelog_json"] = r[12] or "" data["reassessed_at"] = r[13] or "" data["position_advice"] = r[14] or "" + # 持仓状态(2026-07-22 老爸要求:LLM 必须知道是否持有/成本/股数) + hr = conn.execute("SELECT shares, cost, price FROM holdings WHERE code=? AND is_active=1 AND shares>0", (code,)).fetchone() + if hr and hr[0]: + data["held"] = True + data["held_shares"] = hr[0] + data["held_cost"] = hr[1] or 0 + else: + data["held"] = False conn.close() # 从腾讯API拉最新价和基本面 @@ -205,6 +213,20 @@ def build_prompt(data): # 完整分析原文(不截断) _full_analysis = data.get('full_analysis', '') or '' _fa_display = _full_analysis if _full_analysis else '(首次分析,无历史)' + + # ── 持仓上下文(2026-07-22 老爸要求:LLM 必须知道持有状态,建议不得两头都写)── + if data.get('held'): + _sh = data.get('held_shares', 0) + _cost = data.get('held_cost', 0) + _px = data.get('price', 0) or 0 + _pnl = ((_px - _cost) / _cost * 100) if _cost else 0 + _position_context = (f"⚠️ 我当前【已持有】{data['code']}:{_sh}股,成本{_cost:.2f}元," + f"现价{_px}元(盈亏{_pnl:+.1f}%)。你的建议必须基于「已持有」状态给出" + f"(加减仓/止损止盈/持有观察),禁止给「未持有者」的建仓建议。") + else: + _position_context = (f"⚠️ 我当前【未持有】{data['code']}。你的建议必须基于「未持有」状态给出" + f"(是否建仓/什么价位建仓/仓位多大),禁止假设我有浮盈、" + f"禁止出现「已持仓者」视角的建议。") _orig_strategy_section = f"""当前策略参数: {_params_str} @@ -235,6 +257,7 @@ PE={data.get('pe','?')}(最新财报) 市值={data.get('mcap','?')}亿 当前信号:{data.get('timing_signal','?')} 分类:{data.get('stock_category','?')} 我的总资产={total}元,可用现金={cash}元。 +{_position_context} 请严格按以下格式输出(注意节标题不可省略): @@ -373,8 +396,11 @@ def save_result(code, full_text, parsed): conn.execute(sql, params) conn.commit() - # ── 推荐操作 tag 同步(与 XMPP 动作级信号同源)── - sync_recommend_tag(conn, code, parsed.get("signal", "")) + # ── 信号以分析为唯一事实源(防信号/分析脱节)── + from mofin_db import reconcile_signal_from_analysis + final_sig = reconcile_signal_from_analysis(conn, code) + # ── 推荐操作 tag 同步(跟随对齐后的信号)── + sync_recommend_tag(conn, code, final_sig) # 买入信号推送已统一收拢到 sync_recommend_tag 的转场推送(防双重告警)。 # 本路径只负责写库+tag,推送由 mofin_db.push_recommend_alert 在 tag 转场时触发。 diff --git a/deploy/profile-scripts/per_stock_reassess.py b/deploy/profile-scripts/per_stock_reassess.py index b84fd8fc..49e0a123 100644 --- a/deploy/profile-scripts/per_stock_reassess.py +++ b/deploy/profile-scripts/per_stock_reassess.py @@ -550,34 +550,14 @@ def main(): else: print(f" ⚠️ 12维分析未完成,跳过保存") print(f" [DB] holding_strategies 已更新: {code}") - # 从LLM输出提取信号 - if _full_analysis_text and '【综合结论】' in _full_analysis_text: - try: - _sig_line = [l for l in _full_analysis_text.split('\n') if '综合结论' in l] - 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: - _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 == "买入": - try: - _nr2 = __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute( - "SELECT name, price, entry_low, entry_high, stop_loss, take_profit, position_advice FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() - if _nr2: - _xm = f"📈 {_nr2[0] or code}({code}) 价{_nr2[1]}→12维买入信号!区间{_nr2[2]}~{_nr2[3]} 损{_nr2[4]} 盈{_nr2[5]} 仓位{_nr2[6] or '-'}" - from alert_helper import notify as _notify2, ACTION as _ACT2 - _notify2("买入信号", _xm, _ACT2) - print(f" 📨 XMPP推送买入信号") - except: pass - except: pass + # 信号以已存分析为唯一事实源(防信号/分析脱节) + # 推荐推送统一走 reconcile→tag→摘要队列(batch 结束统一发,不再单只推送) + from mofin_db import reconcile_signal_from_analysis + _rc_conn = __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db') + _sig = reconcile_signal_from_analysis(_rc_conn, code) + _rc_conn.close() + if _sig: + print(f" ✅ LLM信号={_sig} 已对齐") # 冷却期已更新(reassessed_at写入) except Exception as _dbe: print(f" [DB FAIL] holding_strategies 写入失败: {_dbe}", file=sys.stderr) diff --git a/mofin_db.py b/mofin_db.py index a949831d..5b9bd385 100644 --- a/mofin_db.py +++ b/mofin_db.py @@ -1108,6 +1108,42 @@ def get_prices_batch_from_db(codes: list[str]) -> dict: # 核心写函数 — 替代 json.dump(),强制币种约束 # ═══════════════════════════════════════════════════════════════════ +def reconcile_signal_from_analysis(conn, code: str) -> str: + """以已存 full_analysis 为唯一事实源,重算 timing_signal 并写回。 + 根治"信号与分析脱节"(per_stock 分开写信号/分析导致的 信号=买入但分析=观望)。 + 返回最终信号。无裁决行 → 清空动作级信号(防陈旧买入残留)。""" + try: + row = conn.execute("SELECT full_analysis, timing_signal FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() + if not row: + return "" + fa, old_sig = row[0] or "", row[1] or "" + verdict = "" + for line in fa.split("\n"): + if "【综合结论】" in line: + for s in ("买入", "可买入", "可加仓", "卖出", "止盈", "关注", "观望", "持有", "弱势持有"): + if s in line: + verdict = s + break + break + if verdict: + new_sig = verdict + elif old_sig in ("买入", "可买入", "可加仓"): + new_sig = "" # 无裁决且陈旧动作信号 → 清除 + else: + new_sig = old_sig + if new_sig != old_sig: + conn.execute("UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", + (new_sig, code)) + conn.commit() + # 信号变了 → tag 跟着对齐 + sync_recommend_tag(conn, code, new_sig) + print(f" [RECONCILE] {code} 信号 {old_sig}→{new_sig}(以分析为准)", flush=True) + return new_sig + except Exception as e: + print(f" [RECONCILE] {code} 异常: {e}", flush=True) + return "" + + def sync_recommend_tag(conn, code: str, timing_signal: str): """裸 SQL 调用方(batch_reassess / per_stock_reassess)的推荐 tag 同步。 动作级信号 → current_recommend;信号降级 → 清除 current_recommend;