#!/usr/bin/env python3
"""
Standalone **inverse SPY bear hedge** (SPY regime + SH).

Firmed criteria (defaults):
  * Enter when SPY dual slope is bearish **OR** SPY is below SMA200
  * Hold **SH** (−1x) while the bear regime persists
  * Exit when SPY regime turns off (not on inverse-ETF bounce whipsaws)

Example::

    cd /Users/robzingale/trading_bot
    PYTHONUNBUFFERED=1 .venv/bin/python \\
        RenTech/strategy_stack/run_ma_slope_inverse_spy_standard.py \\
        --start 2016-01-04

Sweep regime / exit rules::

    PYTHONUNBUFFERED=1 .venv/bin/python \\
        RenTech/strategy_stack/run_ma_slope_inverse_spy_sweep.py \\
        --start 2016-01-04
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path

import numpy as np
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.data_loader import DataLoader
from RenTech.strategy_stack.ma_slope_inverse_sleeve import (
    INVERSE_SPY_ETFS,
    MaSlopeInverseConfig,
    MaSlopeInverseSleeve,
)
from RenTech.strategy_stack.main import _compute_daily_backtest_features

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


def _load_etf_dict(tickers: list[str], period: str) -> dict[str, pd.DataFrame]:
    loader = DataLoader()
    out: dict[str, pd.DataFrame] = {}
    for t in tickers:
        raw = loader.fetch_daily(t, period=period)
        if raw is None or raw.empty:
            print(f"  skip {t}: no data", flush=True)
            continue
        df = _compute_daily_backtest_features(raw)
        df.index = pd.to_datetime(df.index).tz_localize(None)
        out[t] = df.sort_index()
    return out


def _metrics(r: pd.Series, spy_r: pd.Series, cap: float) -> dict:
    r = r.astype(np.float64).dropna()
    eq = cap * (1.0 + r).cumprod()
    years = len(r) / 252.0
    end = float(eq.iloc[-1])
    tot = end / cap - 1.0
    cagr = (end / cap) ** (1.0 / years) - 1.0 if years > 0 else float("nan")
    dd = float((eq / eq.cummax() - 1.0).min())
    sd = float(r.std(ddof=1))
    sharpe = float(r.mean() / sd * np.sqrt(252.0)) if sd > 1e-12 else float("nan")
    rho = float(pd.DataFrame({"h": r, "s": spy_r.reindex(r.index)}).dropna().corr().iloc[0, 1])
    y22 = r.loc["2022-01-01":"2022-12-31"]
    ret_2022 = float((1 + y22).prod() - 1) * 100 if len(y22) > 1 else float("nan")
    return {
        "total_return_pct": tot * 100,
        "cagr_pct": cagr * 100,
        "max_dd_pct": dd * 100,
        "sharpe": sharpe,
        "corr_vs_spy": rho,
        "ret_2022_pct": ret_2022,
        "end_equity": end,
    }


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="")
    ap.add_argument("--yahoo-period", default="max")
    ap.add_argument("--capital", type=float, default=100_000.0)
    ap.add_argument("--tickers", default="SH", help="Inverse ETFs (default SH only)")
    ap.add_argument("--selection", default="top_n", choices=("all_active", "top_n"))
    ap.add_argument("--top-n", type=int, default=1)
    ap.add_argument(
        "--spy-regime",
        default="bear_dual_or_sma200",
        choices=("none", "bear_dual_slope", "below_sma200", "bear_dual_or_sma200"),
    )
    ap.add_argument(
        "--spy-exit",
        default="spy_slope_or_sma200",
        choices=(
            "inverse_signal",
            "spy_fast_slope_positive",
            "spy_regime_off",
            "spy_slope_or_sma200",
            "spy_bull_both",
        ),
    )
    ap.add_argument(
        "--no-require-inverse-momentum",
        action="store_true",
        help="Hold SH on SPY bear regime only (skip inverse-ETF slope confirmation)",
    )
    ap.add_argument("--fast-period", type=int, default=10)
    ap.add_argument("--slow-period", type=int, default=50)
    ap.add_argument("--fast-lookback", type=int, default=10)
    ap.add_argument("--slow-lookback", type=int, default=5)
    ap.add_argument("--spy-sma-window", type=int, default=200)
    ap.add_argument("--cash-yield", type=float, default=0.04)
    ap.add_argument("--out-prefix", type=Path, default=DEFAULT_OUT)
    args = ap.parse_args()

    tickers = [t.strip().upper() for t in args.tickers.split(",") if t.strip()]
    etf_dict = _load_etf_dict(tickers, args.yahoo_period)
    spy_df = _compute_daily_backtest_features(
        DataLoader().fetch_daily("SPY", period=args.yahoo_period)
    )
    spy_df.index = pd.to_datetime(spy_df.index).tz_localize(None)

    if not etf_dict:
        raise SystemExit("No inverse ETF data loaded")

    cfg = MaSlopeInverseConfig(
        fast_period=int(args.fast_period),
        slow_period=int(args.slow_period),
        fast_lookback=int(args.fast_lookback),
        slow_lookback=int(args.slow_lookback),
        spy_sma_window=int(args.spy_sma_window),
        spy_regime=args.spy_regime,
        spy_exit=args.spy_exit,
        require_inverse_momentum=not bool(args.no_require_inverse_momentum),
        selection=args.selection,
        top_n=int(args.top_n),
        tickers_preferred=tuple(tickers),
        cash_annual_yield=float(args.cash_yield),
    )
    eng = MaSlopeInverseSleeve(config=cfg)
    daily_ret = eng.generate_returns(etf_dict, spy_df, verbose=True)
    pos_log = eng.generate_position_log(etf_dict, spy_df)

    daily_ret.index = pd.to_datetime(daily_ret.index).tz_localize(None)
    mask = daily_ret.index >= pd.Timestamp(args.start)
    if args.end.strip():
        mask &= daily_ret.index <= pd.Timestamp(args.end)
    r = daily_ret.loc[mask].fillna(0.0)

    spy_r = spy_df["ret"].astype(float)
    m = _metrics(r, spy_r, float(args.capital))

    prefix = args.out_prefix.expanduser().resolve()
    prefix.parent.mkdir(parents=True, exist_ok=True)
    slug = cfg.slug()
    daily_path = Path(f"{prefix}_{slug}_daily.csv")
    pos_path = Path(f"{prefix}_{slug}_positions.csv")
    meta_path = Path(f"{prefix}_{slug}_meta.json")
    metrics_path = Path(f"{prefix}_{slug}_metrics.txt")

    cap = float(args.capital)
    eq_usd = cap * (1.0 + r).cumprod()
    pd.DataFrame(
        {
            "date": r.index.strftime("%Y-%m-%d"),
            "daily_ret": r.values,
            "daily_pnl_usd": (r * cap).values,
            "equity_usd": eq_usd.values,
        }
    ).to_csv(daily_path, index=False)

    if len(pos_log):
        pm = pd.to_datetime(pos_log["date"]) >= pd.Timestamp(args.start)
        if args.end.strip():
            pm &= pd.to_datetime(pos_log["date"]) <= pd.Timestamp(args.end)
        pos_log.loc[pm].to_csv(pos_path, index=False)

    cmd = (
        f"cd {_REPO} && PYTHONUNBUFFERED=1 .venv/bin/python "
        f"RenTech/strategy_stack/run_ma_slope_inverse_spy_standard.py "
        f"--start {args.start} --tickers {','.join(tickers)} "
        f"--spy-regime {args.spy_regime} --spy-exit {args.spy_exit}"
    )

    meta = {
        "strategy": "MaSlopeInverseSleeve",
        "token": "ma_slope_inverse_spy",
        "etf_universe": {t: INVERSE_SPY_ETFS.get(t, "") for t in tickers},
        "config": asdict(cfg),
        **m,
        "command": cmd,
        "daily_csv": str(daily_path),
        "positions_csv": str(pos_path),
    }
    meta_path.write_text(json.dumps(meta, indent=2, default=str) + "\n")

    metrics_path.write_text(
        f"""# MA slope bear hedge — {slug}
Window: {r.index.min().date()} -> {r.index.max().date()}

Command:
{cmd}

Rules:
  SPY regime: {args.spy_regime}
  SPY exit: {args.spy_exit}
  Tickers: {', '.join(tickers)}
  Require inverse momentum: {not args.no_require_inverse_momentum}

Headline (${cap:,.0f}):
  Total return: {m['total_return_pct']:.2f}%
  CAGR: {m['cagr_pct']:.2f}%
  Sharpe: {m['sharpe']:.3f}
  Max DD: {m['max_dd_pct']:.2f}%
  Corr vs SPY: {m['corr_vs_spy']:.3f}
  2022 return: {m['ret_2022_pct']:.2f}%

Artifacts:
  {daily_path}
  {pos_path}
"""
    )

    print(metrics_path.read_text(), flush=True)
    print(f"Wrote {daily_path}", flush=True)


if __name__ == "__main__":
    main()
