#!/usr/bin/env python3
"""
Backtest 25 SPY overnight improvement ideas on one window; rank by Sharpe / edge.

Example::

    cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 \\
      .venv/bin/python RenTech/strategy_stack/run_spy_overnight_ideas25.py \\
      --start 2016-01-04 --end 2026-06-18 --capital 100000 \\
      --out-prefix RenTech/data/logs/spy_overnight_ideas25
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import pandas as pd

_REPO = Path(__file__).resolve().parents[2]
if str(_REPO) not in sys.path:
    sys.path.insert(0, str(_REPO))

from RenTech.strategy_stack.run_qs_top_ideas_backtest import _align, _fetch_ohlc, _metrics
from RenTech.strategy_stack.spy_overnight_ideas25 import IDEAS, evaluate_idea
from RenTech.strategy_stack.spy_overnight_vix_calm import (
    build_feature_frame,
    next_close_to_close_return,
    signal_to_strategy_returns,
)

LOGS = _REPO / "RenTech" / "data" / "logs"
DEFAULT_OUT = LOGS / "spy_overnight_ideas25"


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--start", default="2016-01-04")
    ap.add_argument("--end", default="2026-06-18")
    ap.add_argument("--yahoo-period", default="max")
    ap.add_argument("--capital", type=float, default=100_000.0)
    ap.add_argument("--out-prefix", type=Path, default=DEFAULT_OUT)
    args = ap.parse_args()

    start = pd.Timestamp(args.start)
    end = pd.Timestamp(args.end) if str(args.end).strip() else None
    cap = float(args.capital)
    fetch_start = start - pd.DateOffset(years=2)

    spy_raw = _fetch_ohlc("SPY", args.yahoo_period)
    vix_raw = _fetch_ohlc("^VIX", args.yahoo_period)
    try:
        vvix_raw = _fetch_ohlc("^VVIX", args.yahoo_period)
    except Exception:
        vvix_raw = None
    try:
        vix3m_raw = _fetch_ohlc("^VIX3M", args.yahoo_period)
    except Exception:
        vix3m_raw = None

    spy_w = _align(spy_raw, fetch_start, end)
    vix_w = _align(vix_raw, fetch_start, end)
    vvix_w = _align(vvix_raw, fetch_start, end) if vvix_raw is not None else None
    vix3m_w = _align(vix3m_raw, fetch_start, end) if vix3m_raw is not None else None

    feats_w = build_feature_frame(spy_w, vix_w, vvix=vvix_w, vix3m=vix3m_w)
    mask = spy_w.index >= start
    if end is not None:
        mask &= spy_w.index <= end
    spy = spy_w.loc[mask]
    feats = feats_w.loc[mask]

    # Benchmarks
    always = pd.Series(True, index=spy.index)
    fwd = next_close_to_close_return(spy)
    always_ret = signal_to_strategy_returns(always, fwd)
    always_m = _metrics(always_ret, cap)

    rows = []
    for idea in IDEAS:
        row = evaluate_idea(idea, spy, feats, capital=cap)
        rows.append(row)
        print(
            f"{row['id']}  Sharpe={row['sharpe']:>5.2f}  "
            f"ret={row['total_return_pct']:>7.1f}%  "
            f"DD={row['max_dd_pct']:>6.1f}%  "
            f"cover={row['coverage_pct']:>5.1f}%  "
            f"edge={row['edge_vs_all_bps']}bps  | {row['name']}",
            flush=True,
        )

    df = pd.DataFrame(rows)
    df["sharpe_vs_always"] = df["sharpe"] - always_m["sharpe"]
    df["beat_always_sharpe"] = df["sharpe"] > always_m["sharpe"]
    # Rank: prefer Sharpe, then edge, then milder DD
    df["rank_score"] = (
        df["sharpe"].fillna(-99)
        + 0.01 * df["edge_vs_all_bps"].fillna(0)
        - 0.005 * df["max_dd_pct"].abs().fillna(99)
    )
    df = df.sort_values("rank_score", ascending=False).reset_index(drop=True)
    df.insert(0, "rank", range(1, len(df) + 1))

    prefix = args.out_prefix.expanduser().resolve()
    prefix.parent.mkdir(parents=True, exist_ok=True)
    ranked_path = Path(f"{prefix}_ranked.csv")
    meta_path = Path(f"{prefix}_meta.json")
    metrics_path = Path(f"{prefix}_metrics.txt")

    df.to_csv(ranked_path, index=False)

    top5 = df.head(5)[["rank", "id", "name", "sharpe", "total_return_pct", "max_dd_pct", "coverage_pct", "edge_vs_all_bps"]]
    beaters = df.loc[df["beat_always_sharpe"], "id"].tolist()

    meta = {
        "window_start": str(start.date()),
        "window_end": str(end.date()) if end is not None else str(spy.index[-1].date()),
        "capital": cap,
        "n_ideas": len(IDEAS),
        "always_overnight": always_m,
        "n_beat_always_sharpe": len(beaters),
        "beat_always_sharpe_ids": beaters,
        "top5": top5.to_dict(orient="records"),
        "ranked_csv": str(ranked_path),
        "caveat": "In-sample Yahoo adj closes; multiple comparisons — treat ranks as exploratory.",
    }
    meta_path.write_text(json.dumps(meta, indent=2))

    lines = [
        "SPY overnight — 25 improvement ideas",
        f"window={meta['window_start']}→{meta['window_end']}  capital={cap:.0f}",
        f"always_overnight: ret={always_m['total_return_pct']}%  Sharpe={always_m['sharpe']}  DD={always_m['max_dd_pct']}%",
        f"ideas beating always Sharpe: {len(beaters)} → {beaters}",
        "",
        "=== TOP 10 by rank_score (Sharpe + edge − |DD|) ===",
    ]
    for _, r in df.head(10).iterrows():
        lines.append(
            f"#{int(r['rank']):02d} {r['id']}  Sharpe={r['sharpe']:.2f}  "
            f"ret={r['total_return_pct']:.1f}%  DD={r['max_dd_pct']:.1f}%  "
            f"cover={r['coverage_pct']:.1f}%  edge={r['edge_vs_all_bps']}bps  | {r['name']}"
        )
    lines.append("")
    lines.append("=== BOTTOM 5 ===")
    for _, r in df.tail(5).iterrows():
        lines.append(
            f"#{int(r['rank']):02d} {r['id']}  Sharpe={r['sharpe']:.2f}  "
            f"ret={r['total_return_pct']:.1f}%  DD={r['max_dd_pct']:.1f}%  "
            f"cover={r['coverage_pct']:.1f}%  edge={r['edge_vs_all_bps']}bps  | {r['name']}"
        )
    lines.append("")
    lines.append(f"wrote {ranked_path}")
    lines.append(f"wrote {meta_path}")
    text = "\n".join(lines) + "\n"
    metrics_path.write_text(text)
    print()
    print(text, end="")


if __name__ == "__main__":
    main()
