126 lines
5.5 KiB
Python
126 lines
5.5 KiB
Python
#!/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
|
|
|
|
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")
|
|
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()
|
|
|
|
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)
|