#!/usr/bin/env python3 """watchlist_auto_exit.py — 自选退出机制 每天盘前执行,扫描自选策略(2026-08-17 数据驱动修正): - 信号=卖出 → 自动退出(数据验证唯一正确条件:20日后-2.2%) - RR<1.0 → 自动退出(保底:防负期望交易) - 已废除:死水3连(60%误杀,信号不充分3连71%涨)/ 远离买区 / 零仓位弱信号 - 记录出入日志到 watchlist_log 表 """ import sqlite3, sys, json from datetime import datetime, timedelta # ── 消息通道统一路由(broadcast/xmpp by delivery) ── try: from messenger import install_stdio_hook as _msh _msh() except Exception: pass 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, timeout=30) conn.execute("PRAGMA busy_timeout=30000") 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}") # ── 2026-08-17 淘汰逻辑修正(数据驱动,老莫确认方案A)── # 数据:死水3连/远离买区/RR<2.0 淘汰后20日 60%误杀(详见 docs/淘汰逻辑验证.md) # 只保留:信号=卖出(唯一数据支持的正确条件)+ RR<1.0 保底(防负期望交易) # 条件2(保底): RR<1.0 淘汰(止损距离≥止盈距离的负期望交易) try: _rr = conn.execute( "SELECT rr_ratio FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() if _rr and _rr[0] is not None and 0 < _rr[0] < 1.0: reasons.append(f"RR={_rr[0]}<1.0(保底)") except Exception: pass # 如果有退出理由,执行退出(2026-07-24 老爸:直接删,不降级) 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) ) # 直接删除(2026-07-24 老爸决策:不是降级回候选,不是inactive标记) conn.execute( "DELETE FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'", (code,) ) # 回归选股池(2026-08-03 老莫修正:废除策略后票回归候选池,下次重走完整流程) _stock_name = conn.execute("SELECT name FROM stocks WHERE code=?", (code,)).fetchone() # 2026-08-17 修复:sector 用该股自己的 strategy_name(保留来源策略语义,不再硬编码 v_mr) _strat = conn.execute( "SELECT strategy_name FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() _strat_name = (_strat[0] if _strat and _strat[0] else "unknown") # 2026-08-18 修复:holding.strategy_name 为 unknown 时,回退用 candidates 里旧的 sector # (候选的 sector 是正确策略标签,但 holding 早期 promote 时 source_strategy 可能没继承) if _strat_name == "unknown" or not _strat_name: _old_sec = conn.execute( "SELECT sector FROM candidates WHERE code=?", (code,)).fetchone() _old_sec_v = str(_old_sec[0]).strip() if _old_sec and _old_sec[0] else "" if _old_sec_v and _old_sec_v != "unknown": _strat_name = _old_sec_v conn.execute( "INSERT INTO candidates (code, name, sector, reason, dropped, drop_reason, promoted, " "source_strategy, created_at) " "VALUES (?,?,?,?,0,NULL,0,?,datetime('now','localtime')) " "ON CONFLICT(code) DO UPDATE SET " "name=excluded.name, dropped=0, drop_reason=NULL, promoted=0, " "reason=excluded.reason, source_strategy=excluded.source_strategy, created_at=excluded.created_at", (code, (_stock_name[0] if _stock_name else name) or "", _strat_name, "自选退出回归选股池: " + reason_text, _strat_name) ) print(f" 🔴 删除: {code} {name or ''} | {reason_text}") else: kept.append(code) if not dry_run: conn.commit() # 2026-08-25 老莫监察要求:输出池子规模趋势(观察退出机制是否奏效的核心指标) _pool_now = conn.execute( "SELECT COUNT(*) FROM holding_strategies WHERE status='active' AND decision_type='自选策略'").fetchone()[0] print(f"\n结果: {len(exited)}只退出, {len(kept)}只保留 | 池子规模: {_pool_now + len(exited)}→{_pool_now}") # 生成退出摘要日志 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)