From c79fb5372246b1954d3a775b8c51d6cbb61812b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Fri, 10 Jul 2026 12:20:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=98=E5=89=8D=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E9=87=8D=E8=AF=84(08:10)+=E8=87=AA=E9=80=89=E9=80=80=E5=87=BA?= =?UTF-8?q?=E6=9C=BA=E5=88=B6+=E8=BF=9B=E5=87=BA=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/premarket_full_review.py | 40 ++++++++++++ scripts/watchlist_auto_exit.py | 106 +++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 scripts/premarket_full_review.py create mode 100644 scripts/watchlist_auto_exit.py diff --git a/scripts/premarket_full_review.py b/scripts/premarket_full_review.py new file mode 100644 index 00000000..1cebcd1c --- /dev/null +++ b/scripts/premarket_full_review.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""premarket_full_review.py — 盘前全量重评 + +执行顺序: +1. regenerate_all() 全量技术分析重评(持仓+自选) +2. watchlist_auto_exit() 自选退出检查 +3. 输出摘要 + +调度:交易日 08:10(A股09:30开盘) +""" +import sys, os, json +sys.path.insert(0, '/home/hmo/MoFin') + +# Step 1: 全量重评 +print("=" * 50) +print("📊 盘前全量重评开始") +print("=" * 50) +from strategy_lifecycle import regenerate_all +result = regenerate_all(stdout=True) +print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功") + +# Step 2: 自选退出 +print("\n" + "=" * 50) +print("🔍 自选退出检查") +print("=" * 50) +from scripts.watchlist_auto_exit import main as auto_exit +exited = auto_exit(dry_run=False) + +# Step 3: 写入摘要供开盘简报引用 +summary = { + "premarket_at": __import__('datetime').datetime.now().isoformat(), + "reassess": result, + "auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited], + "total_kept": result.get('total', 0) - len(exited), +} +os.makedirs("/tmp/mofin_premarket", exist_ok=True) +with open("/tmp/mofin_premarket/summary.json", "w") as f: + json.dump(summary, f, ensure_ascii=False, indent=2) + +print(f"\n✅ 盘前重评完毕") diff --git a/scripts/watchlist_auto_exit.py b/scripts/watchlist_auto_exit.py new file mode 100644 index 00000000..cd095238 --- /dev/null +++ b/scripts/watchlist_auto_exit.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""watchlist_auto_exit.py — 自选退出机制 + +每天盘前执行,扫描自选策略: +- 连续N天信号为"卖出" → 自动退出 +- 连续N天评级极低且价格远离买入区 → 自动退出 +- 记录出入日志到 watchlist_log 表 +""" +import sqlite3, sys, json +from datetime import datetime, timedelta + +DB = "/home/hmo/MoFin/data/mofin.db" + +def get_signal_rank(signal): + """信号排序:分值越低越差""" + rank = {"买入": 5, "可买入": 5, "可加仓": 4, "关注": 3, "观望": 2, "卖出": 1, "信号不充分": 0, "弱势持有": 1} + for k, v in rank.items(): + if k in str(signal): + return v + return 2 # 默认关注级 + +def main(dry_run=False): + conn = sqlite3.connect(DB) + now = datetime.now() + + # 读取所有活跃自选策略 + rows = conn.execute(""" + SELECT code, name, timing_signal, entry_low, entry_high, price, + reassessed_at, position_advice, full_analysis + FROM holding_strategies + WHERE status='active' AND decision_type='自选策略' + ORDER BY code + """).fetchall() + + exited = [] + kept = [] + + for code, name, signal, el, eh, price, reassessed_at, pos_advice, fa in rows: + signal_str = str(signal or "") + rank = get_signal_rank(signal_str) + + # 退出条件判断 + reasons = [] + + # 条件1: 信号=卖出 + if "卖出" in signal_str: + reasons.append(f"信号={signal_str}") + + # 条件2: 信号=观望/信号不充分 且 价格远离买入区 + if "观望" in signal_str or "信号不充分" in signal_str: + if price and el and eh and el > 0 and eh > 0: + if price > eh * 1.20: # 高于买入区上沿20% + reasons.append(f"价{price}超买区上沿+{((price/eh)-1)*100:.0f}%") + elif el > 0 and price < el * 0.85: # 低于买入区下沿15% + reasons.append(f"价{price}低于买区下沿{(1-price/el)*100:.0f}%") + + # 条件3: 已清仓/零仓位且信号差 + pos_str = str(pos_advice or "") + if rank <= 1 and ("0%" in pos_str or "清仓" in pos_str or "不参与" in pos_str): + reasons.append(f"仓位建议={pos_str}") + + # 如果有退出理由,执行退出 + if reasons: + reason_text = "; ".join(reasons) + exited.append((code, name, signal_str, reason_text)) + + if not dry_run: + # 记录退出日志 + conn.execute( + "INSERT INTO watchlist_log (code, name, event, reason, old_signal, new_signal, price) " + "VALUES (?,?,?,?,?,?,?)", + (code, name or "", "exit", reason_text, signal_str, "已退出", price) + ) + # 标记为inactive(软删除,保留历史) + conn.execute( + "UPDATE holding_strategies SET status='inactive', updated_at=datetime('now','localtime') " + "WHERE code=? AND status='active' AND decision_type='自选策略'", + (code,) + ) + print(f" 🔴 退出: {code} {name or ''} | {reason_text}") + else: + kept.append(code) + + if not dry_run: + conn.commit() + + print(f"\n结果: {len(exited)}只退出, {len(kept)}只保留") + + # 生成退出摘要日志 + if exited: + summary = f"【自选退出】{now.strftime('%m/%d')} {len(exited)}只自动退出:\n" + for code, name, sig, reason in exited: + summary += f" {code} {name}: {sig} → {reason}\n" + # 写入JSON供报告引用 + with open("/tmp/watchlist_exit_summary.json", "w") as f: + json.dump({"date": now.isoformat(), "exited": len(exited), "items": [ + {"code": c, "name": n, "reason": r} for c, n, s, r in exited + ]}, f, ensure_ascii=False) + print(summary) + + conn.close() + return exited + +if __name__ == "__main__": + dry = "--dry-run" in sys.argv + main(dry_run=dry)