#!/usr/bin/env python3
"""选股记录跟踪 —— 把每日入选结果并入 records.json，并回填后续表现。

跟踪项：
  入选价、1/3/5/10 日后收盘涨跌、当前涨跌、持有期最大浮盈/最大浮亏、
  是否跌破 5 日线（原策略卖出信号）、状态与提醒

用法：
  python track.py --data ../stock/data            # 合并 latest.json 并刷新跟踪
  python track.py --data ../stock/data --no-merge # 只刷新，不并入今日
"""
from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime
from pathlib import Path

import pandas as pd

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from aq import kline_cached  # noqa: E402

STOP_LOSS_PCT = -8.0


def load(p: Path, default):
    if p.exists():
        try:
            return json.loads(p.read_text("utf-8"))
        except Exception:  # noqa: BLE001
            pass
    return default


def merge_latest(data_dir: Path, rec: dict) -> int:
    latest = load(data_dir / "latest.json", None)
    if not latest:
        return 0
    # 同一交易日只保留最新一版：重跑（改参数、盘中刷新）应当覆盖而不是追加
    td = latest["trade_date"]
    before = len(rec["entries"])
    rec["entries"] = [e for e in rec["entries"] if e["entry_date"] != td]
    dropped = before - len(rec["entries"])
    seen = {(e["entry_date"], e["code"]) for e in rec["entries"]}
    added = 0
    if dropped:
        print(f"[track] 覆盖 {td} 的旧记录 {dropped} 条", file=sys.stderr)
    for p in latest.get("picks", []):
        if (latest["trade_date"], p["code"]) in seen:
            continue
        rec["entries"].append({
            "entry_date": latest["trade_date"],
            "code": p["code"], "name": p["name"],
            "entry_price": p["price"],
            "tier": p["tier"], "signal": p.get("signal", ""),
            "score": p["score"],
            "reasons": p["reasons"], "plan": p["plan"],
            "metrics": p["metrics"],
            "status": "跟踪中", "tracking": {}, "alert": None,
        })
        added += 1
    return added


def refresh(rec: dict) -> None:
    for e in rec["entries"]:
        try:
            k = kline_cached(e["code"], "day", "qfq", 200)
        except Exception:  # noqa: BLE001
            continue
        if k.empty:
            continue
        k = k.reset_index(drop=True)
        k["ma5"] = k["close"].rolling(5).mean()
        after = k[k["date"].astype(str) > e["entry_date"]].reset_index(drop=True)
        entry = float(e["entry_price"])
        tr = {}
        if len(after):
            closes = after["close"].astype(float).tolist()
            for n in (1, 3, 5, 10):
                if len(closes) >= n:
                    tr[f"d{n}"] = round((closes[n - 1] / entry - 1) * 100, 2)
            tr["cur"] = round((closes[-1] / entry - 1) * 100, 2)
            tr["max_gain"] = round((max(closes) / entry - 1) * 100, 2)
            tr["max_dd"] = round((min(closes) / entry - 1) * 100, 2)
            tr["bars"] = len(closes)
            last = after.iloc[-1]
            ma5 = float(last["ma5"]) if pd.notna(last["ma5"]) else None
            below = ma5 is not None and float(last["close"]) < ma5
            tr["ma5"] = round(ma5, 2) if ma5 else None
            tr["below_ma5"] = bool(below)
            # 状态判定
            if below:
                e["status"] = "跌破5日线（卖出信号）"
                e["alert"] = {"level": "exit",
                              "text": f"{e['name']} 收 {float(last['close']):.2f} 跌破 5 日线 {ma5:.2f}，按策略应卖出"}
            elif tr["max_dd"] <= STOP_LOSS_PCT:
                e["status"] = "触发止损"
                e["alert"] = {"level": "stop",
                              "text": f"{e['name']} 持有期最大浮亏 {tr['max_dd']:.1f}%，已触发 {STOP_LOSS_PCT}% 止损"}
            elif e["tier"] == "B" and (float(last["close"]) < float(last["open"])) \
                    and ma5 is not None and float(last["close"]) > ma5:
                e["status"] = "买点已现"
                e["alert"] = {"level": "buy",
                              "text": f"{e['name']} 收上升阴线且站在 5 日线({ma5:.2f})上，具备尾盘买入条件"}
            else:
                e["status"] = "跟踪中"
                e["alert"] = None
        e["tracking"] = tr


def stats(rec: dict) -> dict:
    done = [e for e in rec["entries"] if e.get("tracking")]
    out = {"count": len(rec["entries"]), "tracked": len(done)}
    for n in (1, 3, 5, 10):
        vals = [e["tracking"][f"d{n}"] for e in done if f"d{n}" in e["tracking"]]
        if vals:
            out[f"d{n}"] = {
                "样本": len(vals),
                "均值": round(sum(vals) / len(vals), 2),
                "胜率": round(sum(1 for v in vals if v > 0) / len(vals) * 100, 1),
                "最好": round(max(vals), 2),
                "最差": round(min(vals), 2),
            }
    alerts = [e for e in done if e.get("alert")]
    out["alerts"] = len(alerts)
    return out


def main(argv=None):
    ap = argparse.ArgumentParser(description="选股记录跟踪")
    ap.add_argument("--data", required=True, help="data 目录")
    ap.add_argument("--no-merge", dest="merge", action="store_false")
    a = ap.parse_args(argv)

    d = Path(a.data)
    d.mkdir(parents=True, exist_ok=True)
    rec = load(d / "records.json", {"entries": []})
    rec.setdefault("entries", [])

    added = merge_latest(d, rec) if a.merge else 0
    refresh(rec)
    rec["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    rec["stats"] = stats(rec)
    (d / "records.json").write_text(json.dumps(rec, ensure_ascii=False, indent=2), "utf-8")

    out = {"records": str(d / "records.json"), "新增": added,
           "总记录": len(rec["entries"]), "stats": rec["stats"]}
    print(json.dumps(out, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    sys.exit(main())
