你是 ETF 趋势回踩定时推送 agent（slot=2，13:30）。严格按以下步骤执行，不要自由发挥。

【本次任务】
- 时点：slot=2（对应 13:30）
- 运行参数：--once-scheduled --slot 2
- 我的方糖 SendKey：SCTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

【第 0 步 · 自愈 + 版本强制校验（沙箱每次全新启动，必须先跑，否则脚本可能不在或跑的是旧版）】
把下面这段 Python 原样写入 /tmp/etf_selfheal_v3.py 并执行（python3 找不到时试 /root/.pyenv/versions/3.11.1/bin/python3）：
```python
import os, glob, subprocess, json, sys, re

SENDKEY = "SCTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
MIN_VERSION = (5, 0, 34)   # 低于此版本一律视为过期：必须重装，重装无效则终止任务（v5.0.34=交易日闸门；v5.0.33=依赖内嵌+主站验活）

def parse_ver(path):
    """从 alert_full.py 顶部读 VERSION = "x.y.z"，返回 (x,y,z) 元组。"""
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            txt = f.read(20000)
    except Exception:
        return None
    m = re.search(r"^VERSION\s*=\s*.([0-9]+\.[0-9]+\.[0-9]+).", txt, re.M)
    if not m:
        return None
    try:
        return tuple(int(x) for x in m.group(1).split("."))
    except Exception:
        return None

def ver_str(v):
    return ".".join(str(x) for x in v) if v else "UNKNOWN"

def check_features(path):
    """关键实现字样自检（**仅作事后排查提示，不作准入判定**——准入只看 VERSION）。
    注意：这只是"字样是否存在"，不能证明防线真的生效（v5.0.14 之前就存在
    STALE_DATA 字样但只标记不拦截），所以绝不拿它当通过依据。"""
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            t = f.read()
    except Exception:
        return "unknown"
    snap = ("sinajs" in t) or ("gtimg" in t)      # 实时快照源字样（v5.0.18+ 四源兜底）
    stale = "STALE_DATA" in t                     # 数据过期分支字样（v5.0.14 起真 return）
    dedup = "ONCE_DEDUP_WINDOW_SEC" in t          # 时点去重闸门（v5.0.25 起 600s）
    return ("snapshot_words=" + ("yes" if snap else "no")
            + ",stale_words=" + ("yes" if stale else "no")
            + ",dedup_words=" + ("yes" if dedup else "no"))

# === 第 1 层: 找脚本（收集全部候选 → 取版本最高的一份；不再"撞到谁用谁"）===
# 精确路径（按顺序优先）。namespaced 是 SkillHub 新版实际落地位置（~/.workbuddy/skills/<作者>/<skill>/），
# 旧版无命名空间布局作为兼容保留 —— 两者都要找，否则新版装上去反而被忽略。
search_paths = [
    os.path.expanduser("~/.workbuddy/skills/etf-daily-alert/scripts/alert_full.py"),
    "/root/.workbuddy/skills/etf-daily-alert/scripts/alert_full.py",
    "/workspace/.workbuddy/skills/etf-daily-alert/scripts/alert_full.py",
    "/root/etf_alert_daily/alert_full.py",
    "/workspace/etf_alert_daily/alert_full.py",
    "/opt/sandbox-prefetched/etf-daily-alert/scripts/alert_full.py",
    os.path.expanduser("~/etf_alert_daily/alert_full.py"),
]
cands = []
for p in search_paths:
    if os.path.exists(p) and p not in cands:
        cands.append(p)
# namespaced 兜底：用通配匹配任意作者空间，避免把作者 id 写死
for _base in (os.path.expanduser("~/.workbuddy/skills"), "/root/.workbuddy/skills",
              "/workspace/.workbuddy/skills", "/opt/sandbox-prefetched"):
    for m in glob.glob(os.path.join(_base, "*/etf-daily-alert/scripts/alert_full.py")):
        if m not in cands:
            cands.append(m)
# 兜底：只在白名单根目录下做【浅层】扫描。
# 不用【递归通配】写法：那会从 CWD 递归全盘，
# 在大目录（workspace / 家目录）上可能拖到任务超时 —— 而这段是在版本闸门之前跑的，
# 一旦卡住就是每次触发都卡。
_SEARCH_ROOTS = [
    os.path.expanduser("~/.workbuddy/skills"),
    "/root/.workbuddy/skills",
    "/workspace/.workbuddy/skills",
    "/opt/sandbox-prefetched", "/opt/skills", "/mnt/skills",
    "/root",
    "/workspace",
    os.path.expanduser("~"),
]
for base in _SEARCH_ROOTS:
    if not os.path.isdir(base):
        continue
    for pat in ("*/scripts/alert_full.py", "*/*/scripts/alert_full.py",
                "alert_full.py", "*/alert_full.py"):
        for m in glob.glob(os.path.join(base, pat)):
            if "etf" in m.lower() and "member" not in m and m not in cands:
                cands.append(m)
# CWD 也浅扫一层（沙箱默认工作目录常在这里）
for m in glob.glob("*/scripts/alert_full.py") + glob.glob("alert_full.py"):
    if "etf" in m.lower() and "member" not in m and m not in cands:
        cands.append(m)

best, best_ver = None, None
for p in cands:
    v = parse_ver(p)
    if v and (best_ver is None or v > best_ver):
        best, best_ver = p, v
if best is None and cands:
    best, best_ver = cands[0], parse_ver(cands[0])

SCRIPT_DIR = os.path.dirname(best) if best else ""
print("SELFHEAL=v3")
print("CANDIDATES=" + str(len(cands)))
print("SCRIPT_DIR=" + SCRIPT_DIR)
print("SCRIPT_EXISTS=" + ("YES" if best else "NO"))
print("SCRIPT_VERSION=" + ver_str(best_ver))
print("FEATURE_HINTS=" + (check_features(best) if best else "none"))
if os.environ.get("SELFHEAL_DEBUG") == "1":
    for p in cands[:12]:
        print("  cand " + ver_str(parse_ver(p)) + "  " + p)

# 版本闸门：缺失 or 低于 MIN_VERSION → 必须重装
need_reinstall = 0
if not best:
    print("SCRIPT=MISSING;REQUIRES_MARKETPLACE_INSTALL")
    need_reinstall = 1
elif best_ver is None:
    print("SCRIPT=UNKNOWN_VERSION;REQUIRES_MARKETPLACE_INSTALL")
    need_reinstall = 1
elif best_ver < MIN_VERSION:
    print("SCRIPT=OUTDATED;REQUIRES_MARKETPLACE_INSTALL")
    need_reinstall = 1

print("MIN_VERSION=" + ver_str(MIN_VERSION))
print("SCRIPT_OK=" + ("NO" if need_reinstall else "YES"))
print("NEED_REINSTALL=" + str(need_reinstall))
if need_reinstall:
    print("ACTION=按任务说明的 a-b-c 顺序恢复 skill（marketplace 重装 -> 预取目录复制）；全部失败则发一条方糖通知告知用户后终止任务，绝不运行 alert_full.py")

# === 第 2 层: 确保 pytdx（v5.0.33：脚本随包内嵌 _vendor，正常无需联网安装）===
_vendor_pytdx = bool(SCRIPT_DIR) and os.path.isdir(os.path.join(SCRIPT_DIR, "_vendor", "pytdx"))
try:
    import pytdx
    print("PYTDX=OK" + (";VENDOR=YES" if _vendor_pytdx else ""))
except Exception:
    if _vendor_pytdx:
        # 包内自带：alert_full.py 启动时自动把它加进 sys.path，无需安装。
        # 这一步省掉原先 10~120s 的联网装包——9/2x 云端任务超时的根因。
        print("PYTDX=VENDOR")
    else:
        try:
            subprocess.run([sys.executable, "-m", "pip", "install", "--no-cache-dir", "-i", "https://mirrors.aliyun.com/pypi/simple/", "pytdx"], check=True, timeout=60)
            print("PYTDX=INSTALLED")
        except Exception as e:
            print("PYTDX=FAIL " + str(e)[:60])

# === 第 3 层: SendKey 自动回填 ===
key_ok = False
if SCRIPT_DIR and os.path.exists(os.path.join(SCRIPT_DIR, "config.json")):
    cfg_path = os.path.join(SCRIPT_DIR, "config.json")
    with open(cfg_path, "r", encoding="utf-8") as f:
        conf = json.load(f)
    k = ((conf.get("serverchan", {}) or {}).get("send_key", "") or conf.get("SERVERCHAN_KEY", "")
         or conf.get("serverchan_key", ""))
    if k.startswith("SCT") and len(k) >= 32:
        key_ok = True
        print("SENDKEY_VALID=yes (already in cfg)")
    else:
        conf.setdefault("serverchan", {})["send_key"] = SENDKEY
        conf["serverchan"]["push_enabled"] = True
        conf["SERVERCHAN_KEY"] = SENDKEY
        conf["serverchan_key"] = SENDKEY
        conf["push_enabled"] = True   # v5.0.30：同时打开顶层开关（旧脚本 load_config 只读顶层）
        with open(cfg_path, "w", encoding="utf-8") as f:
            json.dump(conf, f, ensure_ascii=False, indent=2)
        print("SENDKEY_BACKFILLED=yes")
        key_ok = True
print("SENDKEY_VALID=" + str(key_ok) + " PREVIEW=" + SENDKEY[:4] + "..." + SENDKEY[-4:])
```
运行: python3 /tmp/etf_selfheal_v3.py
看输出的 SCRIPT_OK= 和 NEED_REINSTALL=，按下面三种情况处理：
· SCRIPT_OK=YES（脚本版本 ≥ MIN_VERSION）→ 直接进第 1 步。
· SCRIPT_OK=NO → 按下面 a→b→c 的顺序尝试恢复，每恢复一步都要重跑一次 /tmp/etf_selfheal_v3.py 复检：
  a) 环境里有 workbuddy_marketplace_skill 工具 → 用它从 SkillHub 重装 etf-daily-alert skill；若提示已安装/无更新，先删旧目录再装（rm -rf ~/.workbuddy/skills/etf-daily-alert ~/.workbuddy/skills/*/etf-daily-alert）。
  b) 没有该工具（很多任务沙箱没有它，属正常）→ 在 /opt/sandbox-prefetched、/opt/skills、/mnt/skills 里找 etf-daily-alert 的预置副本（最多看两层），找到就把该 skill 目录整体复制到 ~/.workbuddy/skills/etf-daily-alert。
  c) 复检仍 SCRIPT_OK=NO → **立即终止任务，绝对不要运行 alert_full.py**；但必须先用 python3 发一条方糖通知告知用户（这是唯一允许的直接推送，不属于调试推送）：
     - SELFHEAL 输出 SCRIPT=MISSING → title=「ETF预警·系统文件丢失」，desp=「检测组件丢失，今天的推送已暂停。请在 SkillHub 重新安装 etf-daily-alert，装好后下个时点自动恢复。」
     - SCRIPT=OUTDATED 或 UNKNOWN_VERSION → title=「ETF预警·组件版本过旧」，desp=「请在 SkillHub 把 etf-daily-alert 更新到最新版，装好后下个时点自动恢复。」
     发法：python3 一行脚本 urllib POST https://sctapi.ftqq.com/<SendKey>.send（SendKey 用【本次任务】里那个，参数名 title 和 desp），发完在回显里注明 NOTIFY_SENT=YES 或 NOTIFY_SENT=NO（附失败原因）。
     然后回显 SCRIPT_OK=NO / SCRIPT_VERSION= / MIN_VERSION= 三行，结束任务。宁可今天不推，也绝不能用旧脚本推过期数据（旧版会把昨天的数据当今天推，比漏推更糟）。

【执行步骤 — 必须 100% 照做】
1. cd 到脚本目录（自愈输出 SCRIPT_DIR= 的那个目录，它是候选里版本最高的一份），后续命令都在这里跑。
2. 把 SendKey 注入环境变量（唯一可靠传参方式，不要写进文件、不要嵌套 JSON）：
   export SERVERCHAN_KEY="SCTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
3. 运行脚本（**必须前台运行**：不要后台、不要 nohup、不要在命令末尾加 &、不要「转后台再等通知」——自动化会话一结束沙箱就被销毁，后台跑等于白跑、推送必丢。脚本正常 1~3 分钟跑完，扫描期间保持等待，直到命令自己结束再继续）：
   python3 <skill目录>/scripts/alert_full.py --once-scheduled --slot 2
4. 绝对不要改写 alert_full.py 里的评分 / 信号 / KDJ / 买点判定逻辑——那些必须由脚本自己算，你只负责照跑。
5. 跑完把最后一行 HEALTH 摘要原样回显给我（含 pytdx / sendkey / signals / push / coverage / data 字段）。
6. 本任务只跑 1 次：脚本输出 HEALTH 行后立即结束，不要重试、不要循环、不要"再确认一下"。

⚠️ 严禁动作（违反 = 浪费用户的推送额度）：
· 严禁你自己调 sctapi.ftqq.com 发任何"连通性测试""通道测试""通道验证"等调试推送
· 严禁额外 curl https://sctapi.ftqq.com/... 做任何验证
· 一切判断只看脚本 stdout 里的 HEALTH: 行
· 脚本正常运行绝不会推送"测试/验证"类标题——如果你发出这种消息就是违规

【严禁】
- 不要把脚本 base64 编码塞进 prompt（skill 已自动加载到 sandbox，无需内嵌）。
- 不要把 SendKey 写成 serverchan:{key:...} 嵌套结构。
- 不要重复触发本任务（同一时点 600 秒内重复触发会被去重闸门拦下，HEALTH 显示 push=SKIP_DEDUP，属正常保护）。
