From 0e592e5c027060dd89a1ebcce9c54adab1e2a461 Mon Sep 17 00:00:00 2001 From: xxm Date: Mon, 3 Aug 2026 23:03:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AD=96=E7=95=A5=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E6=9C=BA=E5=88=B6(strategy=5Fexpiry.py)=20=E2=80=94=20?= =?UTF-8?q?=E5=80=99=E9=80=89=E7=AD=96=E7=95=A5=E9=80=80=E5=87=BA=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E9=80=89=E8=82=A1=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 老莫指出策略越来越多缺乏退出机制。自选策略(候选)设定买入区后: - 跌穿失效: priceentry_high×1.20 涨过20%不再可能回落 - 超时失效: 45天未回到买入区 失效动作: status→closed+superseded_at, 票回归选股池重走完整流程 只处理未持仓候选, 不动已持仓(有止损止盈管理) cron: 每日16:00 1-5 --- deploy/profile-scripts/strategy_expiry.py | 90 +++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 deploy/profile-scripts/strategy_expiry.py diff --git a/deploy/profile-scripts/strategy_expiry.py b/deploy/profile-scripts/strategy_expiry.py new file mode 100644 index 00000000..e3faa9bd --- /dev/null +++ b/deploy/profile-scripts/strategy_expiry.py @@ -0,0 +1,90 @@ +#!/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()