#!/usr/bin/env python3
"""
Run **regime-mapped VXX trade ideas** through the VX1/VX3 backtest builder with **daily MTM PnL**.

Builds the **Dynamic VXX Regime Strategy Stack** (Approach B: sum full-size sleeve
``daily_pnl_mtm_usd`` on one account). See ``DYNAMIC_VXX_REGIME_STACK.md``.

Writes per-sleeve ``*_daily_mtm.csv``, ``*_dynamic_vxx_regime_stack_daily_mtm.csv``, and
``*_REGIME_MTM_SUMMARY.csv``.

Example::

    cd /Users/robzingale/trading_bot
    .venv/bin/python RenTech/data_pipeline/download_cboe_vix_futures.py
    PYTHONUNBUFFERED=1 .venv/bin/python RenTech/strategy_stack/run_vxx_regime_mtm_report.py \\
        --start 2016-01-01 --end 2026-12-31 --preload-chains
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import asdict
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.backtest_vxx_long_put_roll_carry import (
    EntryParams,
    SWEET_MAX_ELEV,
    SWEET_MAX_ROLL,
    SWEET_MAX_VXX_VS_MA,
    SWEET_MIN_ELEV,
    SWEET_MIN_ROLL,
    enrich_vix_panel,
    run_long_put_roll_carry,
)
from RenTech.strategy_stack.backtest_vxx_vx1_vx3_strategies import (
    START_CAPITAL,
    TOP_FIVE_DEFAULT,
    TOP_SIX_TO_TEN,
    _CHAIN_CACHE,
    _ensure_vx3_panel,
    _load_chain_cached,
    _load_contango,
    _spec_from_dict,
    daily_equity_metrics,
    daily_mtm_metrics,
    run_one_spec,
)
from RenTech.strategy_stack.explore_vxx_decay_strategies import _load_contango as _load_contango_explore
import numpy as np

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

STACK_NAME = "Dynamic VXX Regime Strategy Stack"
COMBINE_MODE = "stack_full_pnl"  # Approach B — see DYNAMIC_VXX_REGIME_STACK.md

# Regime labels ↔ structures tested (from forward-return + vx1/vx3 backtests).
REGIME_IDEAS: list[dict] = [
    {
        "regime": "R4/R5 steep VX3 contango",
        "name": "SteepContango_ShortCall",
        "source": "vx1_vx3",
        "spec_name": "SteepContango_ShortCall",
    },
    {
        "regime": "R4/R5 steep VX3 contango",
        "name": "SteepContango_IronCondor_Tight",
        "source": "vx1_vx3",
        "spec_name": "SteepContango_IronCondor_Tight",
    },
    {
        "regime": "R4/R5 steep VX3 contango",
        "name": "SteepContango_BearCallCredit",
        "source": "vx1_vx3",
        "spec_name": "SteepContango_BearCallCredit",
    },
    {
        "regime": "R2 mild VX3 contango",
        "name": "MildContango_BearPutCredit",
        "source": "vx1_vx3",
        "spec_name": "MildContango_BearPutCredit",
    },
    {
        "regime": "R3 sweet spot (M1→M2 roll + mild VX1)",
        "name": "SweetSpot_LongPut_OTM",
        "source": "long_put",
    },
    {
        "regime": "R1/R2 contango (VIX3M gate)",
        "name": "BearCall_Contango_VIX3M",
        "source": "bear_call",
    },
]


def _slug(name: str) -> str:
    return name.lower().replace(" ", "_")


# Estimated margin per sleeve when active (non-zero daily PnL = position open).
# Spread strategies use risk_budget (~$3k) as max loss ≈ margin.
# ShortCall has undefined upside on VXX; margin ≈ 3× risk budget.
_VXX_SLEEVE_MARGIN_EST: dict[str, float] = {
    "steepcontango_shortcall":       9000.0,  # undefined risk short call (3× risk budget)
    "steepcontango_ironcondor_tight": 3000.0,  # iron condor, defined risk
    "steepcontango_bearcallcredit":   3000.0,  # bear call credit spread
    "mildcontango_bearputcredit":     3000.0,  # bear put credit spread
    "sweetspot_longput_otm":             0.0,  # long put (debit, no margin)
    "bearcall_contango_vix3m":        3000.0,  # bear call spread
}
_VXX_FALLBACK_MARGIN = 3000.0  # for any unrecognised slug


def build_dynamic_vxx_regime_stack(
    sleeve_daily: dict[str, pd.DataFrame],
    *,
    capital: float,
    start: str | None = None,
    end: str | None = None,
) -> tuple[pd.DataFrame, dict]:
    """Approach B: sum sleeve daily MTM PnL on one capital base."""
    pnl_cols: dict[str, pd.Series] = {}
    for name, df in sleeve_daily.items():
        sub = df.set_index(pd.to_datetime(df["date"]).dt.normalize())
        pnl_cols[f"pnl_{_slug(name)}"] = sub["daily_pnl_mtm_usd"].astype(float)

    panel = pd.DataFrame(pnl_cols).fillna(0.0).sort_index()
    if start:
        panel = panel.loc[pd.Timestamp(start).normalize() :]
    if end:
        panel = panel.loc[: pd.Timestamp(end).normalize()]

    panel["pnl_stack"] = panel.sum(axis=1)
    panel["equity_mtm_usd"] = float(capital) + panel["pnl_stack"].cumsum()

    # Margin estimate: sum margin of all sleeves with an open position (non-zero PnL).
    margin_total = pd.Series(0.0, index=panel.index)
    for col in pnl_cols:
        slug = col.removeprefix("pnl_")
        sleeve_margin = _VXX_SLEEVE_MARGIN_EST.get(slug, _VXX_FALLBACK_MARGIN)
        active = panel[col].ne(0.0).astype(float)
        margin_total += active * sleeve_margin
    panel["margin_total_usd"] = margin_total

    out = panel.reset_index(names="date")

    eq = out.set_index("date")["equity_mtm_usd"]
    mtm_meta, _ = daily_mtm_metrics(
        [
            {
                "date": d,
                "realized_cum_usd": float(eq.loc[d] - capital),
                "unrealized_usd": 0.0,
                "equity_mtm_usd": float(eq.loc[d]),
            }
            for d in eq.index
        ],
        capital=capital,
    )
    meta = {
        "name": STACK_NAME,
        "combine_mode": COMBINE_MODE,
        "capital_usd": capital,
        "n_sleeves": len(sleeve_daily),
        "sleeves": list(sleeve_daily.keys()),
        "first_day": str(out["date"].iloc[0].date()) if len(out) else None,
        "last_day": str(out["date"].iloc[-1].date()) if len(out) else None,
        **mtm_meta,
    }
    return out, meta


def _load_sleeve_daily_from_prefix(out_prefix: Path) -> dict[str, pd.DataFrame]:
    sleeve_daily: dict[str, pd.DataFrame] = {}
    for idea in REGIME_IDEAS:
        path = Path(f"{out_prefix}_{_slug(idea['name'])}_daily_mtm.csv")
        if not path.is_file():
            raise FileNotFoundError(f"Missing sleeve daily MTM: {path}")
        sleeve_daily[idea["name"]] = pd.read_csv(path)
    return sleeve_daily


def _write_stack_artifacts(
    out_prefix: Path,
    sleeve_daily: dict[str, pd.DataFrame],
    *,
    capital: float,
    start: str,
    end: str,
) -> None:
    stack_df, stack_meta = build_dynamic_vxx_regime_stack(
        sleeve_daily, capital=capital, start=start, end=end
    )
    daily_path = Path(f"{out_prefix}_dynamic_vxx_regime_stack_daily_mtm.csv")
    meta_path = Path(f"{out_prefix}_dynamic_vxx_regime_stack_meta.json")
    stack_df.to_csv(daily_path, index=False)
    meta_path.write_text(json.dumps(stack_meta, indent=2))
    print(f"\n=== {STACK_NAME} ({COMBINE_MODE}) ===", flush=True)
    print(
        f"  Return {stack_meta['return_pct']:+.1f}%  Sharpe {stack_meta['sharpe']:.2f}  "
        f"MaxDD {stack_meta['max_dd_pct']:.1f}%  End ${stack_meta['end_equity_mtm_usd']:,.0f}",
        flush=True,
    )
    print(f"  Daily → {daily_path}", flush=True)
    print(f"  Meta  → {meta_path}", flush=True)


def _curated_spec_by_name(name: str):
    for block in TOP_FIVE_DEFAULT + TOP_SIX_TO_TEN:
        if block["name"] == name:
            return _spec_from_dict(block["spec_kwargs"])
    raise KeyError(name)


def _run_vx1_vx3_mtm(
    spec_name: str,
    ct: pd.DataFrame,
    dates: list[pd.Timestamp],
    *,
    capital: float,
    risk_budget: float,
) -> tuple[dict, dict, pd.DataFrame]:
    spec = _curated_spec_by_name(spec_name)
    mtm_rows: list[dict] = []
    trades = run_one_spec(
        spec,
        ct,
        dates,
        risk_budget_usd=risk_budget,
        daily_mtm_records=mtm_rows,
        capital=capital,
    )
    mtm_meta, mtm_df = daily_mtm_metrics(mtm_rows, capital=capital)
    exit_meta = daily_equity_metrics(trades, dates, capital=capital)
    exit_meta["metric_mode"] = "exit_day"
    exit_meta["n_trades"] = len(trades)
    mtm_meta["n_trades"] = len(trades)
    return mtm_meta, exit_meta, mtm_df


def _exit_day_metrics_from_pnls(
    trades: list,
    dates: list[pd.Timestamp],
    capital: float,
    *,
    pnl_attr: str = "pnl_total",
) -> dict:
    pnl_by_day: dict[pd.Timestamp, float] = {}
    for t in trades:
        ed = pd.Timestamp(getattr(t, "exit_date")).normalize()
        pnl_by_day[ed] = pnl_by_day.get(ed, 0.0) + float(getattr(t, pnl_attr))
    idx = pd.DatetimeIndex(dates)
    daily_pnl = pd.Series(0.0, index=idx)
    for d, v in pnl_by_day.items():
        if d in daily_pnl.index:
            daily_pnl.loc[d] = v
    eq = capital + daily_pnl.cumsum()
    dr = eq.diff().fillna(0)
    sd = float(dr.std())
    sharpe = (float(dr.mean()) / sd * np.sqrt(252)) if sd > 1e-12 else 0.0
    peak = eq.cummax()
    dd_pct = float(((eq - peak) / peak.replace(0, np.nan)).fillna(0).min()) * 100
    return {
        "sharpe": round(sharpe, 3),
        "return_pct": round((float(eq.iloc[-1]) / capital - 1) * 100, 2),
        "max_dd_pct": round(dd_pct, 2),
        "n": len(trades),
    }


def _run_long_put_mtm(
    ct: pd.DataFrame,
    dates: list[pd.Timestamp],
    *,
    capital: float,
    risk_budget: float,
) -> tuple[dict, dict, pd.DataFrame]:
    entry = EntryParams(
        min_roll=SWEET_MIN_ROLL,
        max_roll=SWEET_MAX_ROLL,
        min_elev_vs_ma=SWEET_MIN_ELEV,
        max_elev_vs_ma=SWEET_MAX_ELEV,
        max_vxx_vs_ma=SWEET_MAX_VXX_VS_MA,
        ma_window=60,
    )
    trades = run_long_put_roll_carry(
        ct,
        dates,
        entry=entry,
        moneyness=-0.03,
        dte_min=14,
        dte_max=28,
        hold_days=20,
        risk_budget_usd=risk_budget,
    )
    exit_meta = _exit_day_metrics_from_pnls(trades, dates, capital)
    exit_meta["metric_mode"] = "exit_day"
    exit_meta["n_trades"] = len(trades)
    # Exit-day equity (open legs not marked daily on this path yet).
    idx = pd.DatetimeIndex(dates)
    daily_pnl = pd.Series(0.0, index=idx)
    for t in trades:
        ed = pd.Timestamp(t.exit_date).normalize()
        if ed in daily_pnl.index:
            daily_pnl.loc[ed] += float(t.pnl_total)
    eq = capital + daily_pnl.cumsum()
    mtm_df = pd.DataFrame(
        {
            "date": idx,
            "realized_cum_usd": (eq - capital).values,
            "unrealized_usd": 0.0,
            "equity_mtm_usd": eq.values,
            "daily_pnl_mtm_usd": daily_pnl.values,
        }
    )
    mtm_meta, _ = daily_mtm_metrics(
        [
            {
                "date": r["date"],
                "realized_cum_usd": r["realized_cum_usd"],
                "unrealized_usd": 0.0,
                "equity_mtm_usd": r["equity_mtm_usd"],
            }
            for _, r in mtm_df.iterrows()
        ],
        capital=capital,
    )
    mtm_meta["n_trades"] = len(trades)
    mtm_meta["note"] = "exit_day_proxy (long_put builder has no intraday MTM yet)"
    return mtm_meta, exit_meta, mtm_df


def _run_bear_call_mtm(
    start: str,
    end: str,
    *,
    capital: float,
) -> tuple[dict, dict, pd.DataFrame]:
    from RenTech.strategy_stack.backtest_vxx_bear_call_contango import run_backtest

    trades = run_backtest(
        start=start,
        end=end,
        short_moneyness=1.05,
        width_pct=0.15,
        hold_days=20,
        rebalance_every=10,
        contango_mode="vix3m",
        contango_threshold=0.03,
        vix3m_threshold=1.08,
        dte_min=21,
        dte_max=45,
        take_profit_pct=0.5,
        stop_loss_pct=1.0,
    )
    ct = _load_contango_explore()
    dates = [
        pd.Timestamp(d).normalize()
        for d in sorted(ct.index)
        if start <= str(d.date()) <= end
    ]
    exit_meta = _exit_day_metrics_from_pnls(trades, dates, capital)
    exit_meta["metric_mode"] = "exit_day"
    exit_meta["n_trades"] = len(trades)
    idx = pd.DatetimeIndex(dates)
    daily_pnl = pd.Series(0.0, index=idx)
    for t in trades:
        ed = pd.Timestamp(t.exit_date).normalize()
        if ed in daily_pnl.index:
            daily_pnl.loc[ed] += float(t.pnl_total)
    eq = capital + daily_pnl.cumsum()
    mtm_df = pd.DataFrame(
        {
            "date": idx,
            "realized_cum_usd": (eq - capital).values,
            "unrealized_usd": 0.0,
            "equity_mtm_usd": eq.values,
            "daily_pnl_mtm_usd": daily_pnl.values,
        }
    )
    mtm_meta, _ = daily_mtm_metrics(
        [
            {
                "date": r["date"],
                "realized_cum_usd": r["realized_cum_usd"],
                "unrealized_usd": 0.0,
                "equity_mtm_usd": r["equity_mtm_usd"],
            }
            for _, r in mtm_df.iterrows()
        ],
        capital=capital,
    )
    mtm_meta["n_trades"] = len(trades)
    mtm_meta["note"] = "exit_day_proxy (bear_call path)"
    return mtm_meta, exit_meta, mtm_df


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--start", default="2016-01-01")
    ap.add_argument("--end", default="2026-12-31")
    ap.add_argument("--capital", type=float, default=START_CAPITAL)
    ap.add_argument("--risk-budget", type=float, default=3000.0)
    ap.add_argument("--preload-chains", action="store_true")
    ap.add_argument(
        "--out-prefix",
        type=Path,
        default=LOGS / "vxx_regime_mtm_2016_2026",
    )
    ap.add_argument(
        "--combine-only",
        action="store_true",
        help="Skip sleeve backtests; rebuild Dynamic VXX Regime Strategy Stack from existing *_daily_mtm.csv",
    )
    args = ap.parse_args()

    out_prefix = args.out_prefix.expanduser().resolve()
    out_prefix.parent.mkdir(parents=True, exist_ok=True)

    if args.combine_only:
        sleeve_daily = _load_sleeve_daily_from_prefix(out_prefix)
        _write_stack_artifacts(
            out_prefix,
            sleeve_daily,
            capital=args.capital,
            start=args.start,
            end=args.end,
        )
        return

    ct = _ensure_vx3_panel(_load_contango())
    dates = [
        pd.Timestamp(d).normalize()
        for d in sorted(ct.index)
        if args.start <= str(d.date()) <= args.end
    ]
    print(f"Window: {dates[0].date()} → {dates[-1].date()}  ({len(dates)} days)", flush=True)

    if args.preload_chains:
        _CHAIN_CACHE.clear()
        print("Pre-loading VXX option chains …", flush=True)
        for j, d in enumerate(dates):
            _load_chain_cached(d)
            if j and j % 400 == 0:
                print(f"  {j}/{len(dates)}", flush=True)
        print(f"  cached {len(_CHAIN_CACHE)} days", flush=True)

    ct_put = enrich_vix_panel(_load_contango(), args.start, args.end, 60)

    summary_rows: list[dict] = []
    sleeve_daily: dict[str, pd.DataFrame] = {}
    repo = _REPO.resolve()

    for idea in REGIME_IDEAS:
        name = idea["name"]
        print(f"\n=== {name} ({idea['regime']}) ===", flush=True)
        if idea["source"] == "vx1_vx3":
            mtm_m, exit_m, mtm_df = _run_vx1_vx3_mtm(
                idea["spec_name"],
                ct,
                dates,
                capital=args.capital,
                risk_budget=args.risk_budget,
            )
        elif idea["source"] == "long_put":
            mtm_m, exit_m, mtm_df = _run_long_put_mtm(
                ct_put,
                dates,
                capital=args.capital,
                risk_budget=args.risk_budget,
            )
        else:
            mtm_m, exit_m, mtm_df = _run_bear_call_mtm(
                args.start,
                args.end,
                capital=args.capital,
            )

        slug = _slug(name)
        daily_path = Path(f"{out_prefix}_{slug}_daily_mtm.csv")
        mtm_df.to_csv(daily_path, index=False)
        sleeve_daily[name] = mtm_df
        try:
            daily_rel = str(daily_path.relative_to(repo))
        except ValueError:
            daily_rel = str(daily_path)
        row = {
            "regime": idea["regime"],
            "name": name,
            "source": idea["source"],
            "n_trades": mtm_m.get("n_trades", 0),
            "mtm_sharpe": mtm_m.get("sharpe"),
            "mtm_return_pct": mtm_m.get("return_pct"),
            "mtm_max_dd_pct": mtm_m.get("max_dd_pct"),
            "mtm_cagr_pct": mtm_m.get("cagr_pct"),
            "exit_sharpe": exit_m.get("sharpe"),
            "exit_return_pct": exit_m.get("return_pct"),
            "exit_max_dd_pct": exit_m.get("max_dd_pct"),
            "daily_csv": daily_rel,
            "note": mtm_m.get("note", ""),
        }
        summary_rows.append(row)
        print(
            f"  MTM: Sharpe={row['mtm_sharpe']}  Ret={row['mtm_return_pct']}%  "
            f"MaxDD={row['mtm_max_dd_pct']}%  trades={row['n_trades']}",
            flush=True,
        )
        print(
            f"  Exit-day ref: Sharpe={row['exit_sharpe']}  Ret={row['exit_return_pct']}%  "
            f"MaxDD={row['exit_max_dd_pct']}%",
            flush=True,
        )
        print(f"  → {daily_path}", flush=True)

    summary_path = Path(f"{out_prefix}_REGIME_MTM_SUMMARY.csv")
    pd.DataFrame(summary_rows).to_csv(summary_path, index=False)
    meta_path = Path(f"{out_prefix}_REGIME_MTM_SUMMARY.json")
    meta_path.write_text(
        json.dumps(
            {
                "window": [args.start, args.end],
                "capital": args.capital,
                "risk_budget_usd": args.risk_budget,
                "ideas": summary_rows,
            },
            indent=2,
        )
    )
    print(f"\nSummary → {summary_path}", flush=True)
    print(summary_path.read_text(), flush=True)

    _write_stack_artifacts(
        out_prefix,
        sleeve_daily,
        capital=args.capital,
        start=args.start,
        end=args.end,
    )


if __name__ == "__main__":
    main()
