feat: 自选退出回归选股池(watchlist_auto_exit改造) — 废除策略后票回候选池
老莫指出策略需退出机制且回归选股池。发现现役watchlist_auto_exit.py已是完整退出机制 (卖出信号/超买区20%/跌破买区15%/死水3连/RR<1.5), 但DELETE后票直接消失不回归。 改造: DELETE前 INSERT candidates(清除dropped/promoted), 票回归选股池重走完整流程。 删除重复的strategy_expiry.py(与现役机制冲突)
This commit is contained in:
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""strategy_expiry.py — 策略失效机制(2026-08-03 新增)
|
|
||||||
|
|
||||||
背景:老莫指出策略越来越多、缺乏退出机制。自选策略(候选)设定买入区后,
|
|
||||||
如果价格跌穿买入区太远、或超时未回到买入区、或涨过头不再可能回落,
|
|
||||||
原策略应废除,票回归选股池,下次重新走完整流程。
|
|
||||||
|
|
||||||
只处理"候选/自选策略"(未持仓),不动已持仓策略(有止损止盈管理)。
|
|
||||||
|
|
||||||
失效规则(对齐老莫 2026-08-03 描述):
|
|
||||||
1. 跌穿失效: price < entry_low × 0.85 (跌破买入区下沿15%——跌太远,原买入逻辑失效)
|
|
||||||
2. 涨过头失效: price > entry_high × 1.20 (超过买入区上沿20%——不可能回落到买入区)
|
|
||||||
3. 超时失效: updated_at 距今 > 30交易日(约45自然日) 且 期间价格未回到买入区
|
|
||||||
(简化: 用当前价 vs 买入区 + 时间, 后续可细化区间内价格轨迹)
|
|
||||||
|
|
||||||
失效动作:
|
|
||||||
- status → 'closed', superseded_at → now, reason 记录
|
|
||||||
- 票回归选股池(下次扫描重新走 Stage1→Stage2 完整流程)
|
|
||||||
|
|
||||||
用法: python3 strategy_expiry.py [--dry-run]
|
|
||||||
"""
|
|
||||||
import sys, sqlite3, json
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
DB = Path("/home/hmo/MoFin/data/mofin.db")
|
|
||||||
DRY = "--dry-run" in sys.argv
|
|
||||||
|
|
||||||
# ── 参数(可调)──
|
|
||||||
DOWN_DEV = 0.85 # 跌破买入区下沿至 85% 即失效(跌 15%)
|
|
||||||
UP_DEV = 1.20 # 超过买入区上沿 120% 即失效(涨 20%)
|
|
||||||
STALE_DAYS = 45 # 超时:45 自然日(约30交易日)未回到买入区
|
|
||||||
|
|
||||||
|
|
||||||
def check_expiry(conn):
|
|
||||||
"""扫描 active 自选策略(未持仓候选),返回失效清单"""
|
|
||||||
rows = conn.execute("""
|
|
||||||
SELECT id, code, name, entry_low, entry_high, price, updated_at, decision_type
|
|
||||||
FROM holding_strategies
|
|
||||||
WHERE status='active' AND (shares IS NULL OR shares=0) AND (cost IS NULL OR cost=0)
|
|
||||||
""").fetchall()
|
|
||||||
expired = []
|
|
||||||
now = datetime.now()
|
|
||||||
for rid, code, name, el, eh, price, upd, dtype in rows:
|
|
||||||
if not el or not eh or not price:
|
|
||||||
continue
|
|
||||||
reason = None
|
|
||||||
# 1. 跌穿失效
|
|
||||||
if price < el * DOWN_DEV:
|
|
||||||
reason = f"跌穿买入区: price={price} < entry_low×{DOWN_DEV}={el*DOWN_DEV:.2f} (跌{ (1-price/el)*100:.0f}%)"
|
|
||||||
# 2. 涨过头失效
|
|
||||||
elif price > eh * UP_DEV:
|
|
||||||
reason = f"涨过头: price={price} > entry_high×{UP_DEV}={eh*UP_DEV:.2f} (涨{ (price/eh-1)*100:.0f}%)"
|
|
||||||
# 3. 超时失效(用 updated_at,created_at 基本为空)
|
|
||||||
elif upd:
|
|
||||||
try:
|
|
||||||
upd_dt = datetime.strptime(str(upd)[:19], "%Y-%m-%d %H:%M:%S")
|
|
||||||
if (now - upd_dt).days > STALE_DAYS:
|
|
||||||
# 且当前价不在买入区(若在买入区说明还在等回调,不失效)
|
|
||||||
if not (el <= price <= eh):
|
|
||||||
reason = f"超时{ (now-upd_dt).days }天未回到买入区(当前{price} 区间{el}~{eh})"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if reason:
|
|
||||||
expired.append({"id": rid, "code": code, "name": name, "reason": reason, "dtype": dtype})
|
|
||||||
return expired
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
conn = sqlite3.connect(str(DB))
|
|
||||||
expired = check_expiry(conn)
|
|
||||||
print(f"扫描 active 候选策略: {len(expired)} 条需失效")
|
|
||||||
if DRY:
|
|
||||||
print("(--dry-run 不执行)")
|
|
||||||
for e in expired:
|
|
||||||
print(f" ⛔ {e['code']} {e['name']} [{e['dtype']}]: {e['reason']}")
|
|
||||||
if not DRY and expired:
|
|
||||||
for e in expired:
|
|
||||||
conn.execute("""
|
|
||||||
UPDATE holding_strategies
|
|
||||||
SET status='closed', superseded_at=datetime('now','localtime'), reason=?
|
|
||||||
WHERE id=?
|
|
||||||
""", (f"策略失效: {e['reason']}", e["id"]))
|
|
||||||
conn.commit()
|
|
||||||
print(f"✅ 已失效 {len(expired)} 条策略,票已回归选股池")
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -98,6 +98,17 @@ def main(dry_run=False):
|
|||||||
"DELETE FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'",
|
"DELETE FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'",
|
||||||
(code,)
|
(code,)
|
||||||
)
|
)
|
||||||
|
# 回归选股池(2026-08-03 老莫修正:废除策略后票回归候选池,下次重走完整流程)
|
||||||
|
_stock_name = conn.execute("SELECT name FROM stocks WHERE code=?", (code,)).fetchone()
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO candidates (code, name, sector, reason, dropped, drop_reason, promoted, 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, created_at=excluded.created_at",
|
||||||
|
(code, (_stock_name[0] if _stock_name else name) or "", "v_mr",
|
||||||
|
"自选退出回归选股池: " + reason_text)
|
||||||
|
)
|
||||||
print(f" 🔴 删除: {code} {name or ''} | {reason_text}")
|
print(f" 🔴 删除: {code} {name or ''} | {reason_text}")
|
||||||
else:
|
else:
|
||||||
kept.append(code)
|
kept.append(code)
|
||||||
|
|||||||
Reference in New Issue
Block a user