Files
MoFin/deploy/profile-scripts/watchlist_auto_exit.py
T
hmo 481acfb18f feat: 优中选优机制(老爸:阈值太低+存量清洗+直接删)
- enqueue_recommend加严: RR>=2.0+仓位必须明确%+买入区有效+不追高(防今早RR1.49/仓位观望/区缺失的垃圾digest)
- promote: score>=7(原4=91%通过率)+ST排除+RR>=2.0
- watchlist_auto_exit: 新增死水3连退+RR<1.5退; 退出方式改为直接DELETE(非inactive)
- 盯盘可执行门槛1.5→2.0
2026-07-24 11:00:41 +08:00

128 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}")
# ── 优中选优新增(2026-07-24 老爸)──
# 条件4: 死水闸——最近3次重评信号均为 观望/信号不充分/弱势持有 → 退出
_weak = ("观望", "信号不充分", "弱势持有")
try:
_hist = conn.execute(
"SELECT timing_signal FROM strategy_history WHERE code=? "
"ORDER BY snapshotted_at DESC LIMIT 3", (code,)).fetchall()
if len(_hist) >= 3 and all((h[0] or "") in _weak for h in _hist):
reasons.append(f"死水3连({','.join((h[0] or '?') for h in _hist)}")
except Exception:
pass
# 条件5: RR闸——RR<1.5 的平庸标的直接出(老爸:1.5边缘也是阿猫阿狗)
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.5:
reasons.append(f"RR={_rr[0]}<1.5")
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,)
)
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)