From 529ffaa33a52b3f3ed1e7cebc27fc93d25769934 Mon Sep 17 00:00:00 2001 From: xxm Date: Tue, 18 Aug 2026 10:26:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E6=8F=90=E6=8B=94=E8=87=AA?= =?UTF-8?q?=E9=80=89=E9=87=8D=E8=AF=84=E8=A1=A5=E6=8E=A8=E2=80=94=E2=80=94?= =?UTF-8?q?promote=E5=90=8Edetach=E5=90=AF=E5=8A=A8backfill,90s=E5=90=8E?= =?UTF-8?q?=E8=A1=A5=E8=B7=91=E6=9C=AA=E5=B0=B1=E7=BB=AA=E9=87=8D=E8=AF=84?= =?UTF-8?q?=E5=B9=B6XMPP=E8=A1=A5=E6=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/promote_candidates.py | 9 ++ .../promote_reassess_backfill.py | 93 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 deploy/profile-scripts/promote_reassess_backfill.py diff --git a/deploy/profile-scripts/promote_candidates.py b/deploy/profile-scripts/promote_candidates.py index 7a2a4e8d..9694fbf1 100644 --- a/deploy/profile-scripts/promote_candidates.py +++ b/deploy/profile-scripts/promote_candidates.py @@ -247,6 +247,15 @@ def main(): print(f"[PROMOTE] XMPP 通知已发送({promoted}只含完整重评)", flush=True) except Exception as e: print(f"[PROMOTE] XMPP 通知失败: {e}", flush=True) + # 2026-08-18 老莫:重评未就绪的必须补推——后台 detach 补跑重评+补推 + if promoted > 0: + try: + _bf = subprocess.Popen( + ["/home/hmo/MoFin/venv/bin/python", "/home/hmo/MoFin/deploy/profile-scripts/promote_reassess_backfill.py", "90"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) + print(f"[PROMOTE] 后台补推已启动(PID={_bf.pid}, 90s后补跑未就绪重评并补推)", flush=True) + except Exception as e: + print(f"[PROMOTE] 后台补推启动失败: {e}", flush=True) conn.close() diff --git a/deploy/profile-scripts/promote_reassess_backfill.py b/deploy/profile-scripts/promote_reassess_backfill.py new file mode 100644 index 00000000..4b1a6185 --- /dev/null +++ b/deploy/profile-scripts/promote_reassess_backfill.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""promote_reassess_backfill.py — 新提拔自选的重评补推(老莫:未就绪的要补推) +promote 结束后 detach 启动: +1. 读 promote 记录的新提拔候选(holding_strategies 里 full_analysis 为空/短的 active 自选) +2. 对未就绪的补跑 per_stock_reassess(300s 超时) +3. 收集就绪的完整重评,通过 XMPP 补推给老莫 +""" +import sys, os, json, time, subprocess, sqlite3, urllib.request +from datetime import datetime + +DB = "/home/hmo/MoFin/data/mofin.db" +PY = "/home/hmo/MoFin/venv/bin/python" +REASSESS = "/home/hmo/MoFin/deploy/profile-scripts/per_stock_reassess.py" +XMPP = "http://127.0.0.1:5805/" +TIMEOUT = 300 +MAX_RETRY = 2 + +def xmpp_push(body): + try: + req = urllib.request.Request( + XMPP, data=json.dumps({"to": "hmo@yoin.fun", "body": body, "type": "chat"}).encode(), + headers={"Content-Type": "application/json"}) + urllib.request.urlopen(req, timeout=10) + return True + except Exception as e: + print(f" ⚠️ XMPP 补推失败: {e}", flush=True) + return False + +def main(wait_sec=90): + # 给首次重评完成时间(promote 触发的 REASSESS 可能还在跑) + print(f"[BACKFILL] 等待 {wait_sec}s 后检查未就绪重评...", flush=True) + time.sleep(wait_sec) + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + # 最近 30 分钟新提拔的自选,full_analysis 为空/短(未就绪) + rows = conn.execute(""" + SELECT code, name, strategy_name, rr_ratio, LENGTH(full_analysis) as fa_len, created_at + FROM holding_strategies + WHERE status='active' AND decision_type='自选策略' + AND created_at >= datetime('now', 'localtime', '-30 minutes') + AND (full_analysis IS NULL OR LENGTH(full_analysis) < 50) + ORDER BY created_at DESC + """).fetchall() + conn.close() + if not rows: + print("[BACKFILL] 无未就绪重评", flush=True) + return + print(f"[BACKFILL] 发现 {len(rows)} 只未就绪重评,逐一补跑...", flush=True) + backfilled = [] + for code, name, strat, rr, fa_len, created in rows: + ok = False + for attempt in range(MAX_RETRY): + try: + r = subprocess.run([PY, REASSESS, code], capture_output=True, text=True, timeout=TIMEOUT) + if r.returncode == 0: + ok = True + break + else: + print(f" ⚠️ {code} 重评失败(尝试{attempt+1}): {r.stderr.strip()[:100]}", flush=True) + except Exception as e: + print(f" ⚠️ {code} 重评异常(尝试{attempt+1}): {e}", flush=True) + time.sleep(5) + if ok: + backfilled.append((code, name, strat, rr)) + if not backfilled: + print("[BACKFILL] 补跑后仍无就绪报告,跳过补推", flush=True) + return + # 收集就绪的完整报告 + conn = sqlite3.connect(DB, timeout=30) + codes = ",".join("?" * len(backfilled)) + ready = conn.execute( + f"SELECT code, name, strategy_name, rr_ratio, full_analysis FROM holding_strategies " + f"WHERE status='active' AND code IN ({codes})", + tuple(c[0] for c in backfilled)).fetchall() + conn.close() + body = f"📤 重评报告补推({len(ready)}只新入自选的重评已就绪)\n" + for i, (code, name, strat, rr, fa) in enumerate(ready, 1): + nm = name or code + body += f"\n━━━ [{i}] {code} {nm} ━━━\n" + body += f"策略: {strat or 'unknown'} | RR: {rr or 'N/A'}\n" + if fa and len(str(fa)) > 50: + body += f"【12维重评】\n{fa}\n" + else: + body += "(仍未能生成,已放弃补推)\n" + if len(body) > 6000: + body = body[:6000] + "\n…(内容过长已截断)" + ok = xmpp_push(body) + print(f"[BACKFILL] 补推{'成功' if ok else '失败'}: {len(ready)} 只", flush=True) + +if __name__ == "__main__": + wait = int(sys.argv[1]) if len(sys.argv) > 1 else 90 + main(wait) \ No newline at end of file