#!/usr/bin/env python3
"""
Export **fund-mode** Best Ideas book as two reports:

1. **Daily fund state** — one row per session with three namespaces:
   ``fund_*`` (PnL that sums to NAV), ``standalone_*`` ($100k sleeve reference),
   ``activity_*`` (positions/tickers; not $ attribution).
2. **All trades** — union of closed trades from SPY margin sim, equity dip, VXX long call;
   optional VXX regime sleeves (slow).

Example::

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

Requires ``best_ideas_stack_plus_*_fund_mtm_daily.csv`` (run combine with ``--mtm --fund-mode`` first).
"""

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.combine_best_ideas_stack import (
    DEFAULT_EQUITY_DIP_DAILY,
    DEFAULT_LIT_MTM_DAILY,
    DEFAULT_RUSSELL3000_DIP_DAILY,
    DEFAULT_RUSSELL3000_DIP_TOP_N,
    DEFAULT_SP500_DIP_TOP_N,
    DEFAULT_VXX_DAILY,
    DEFAULT_VXX_LONG_CALL_DAILY,
    EQUITY_DIP_SHARPE_RUSSELL3000,
    EQUITY_DIP_SHARPE_SP500,
    _resolve_equity_dip_weights,
    _resolve_fund_weights,
)
from RenTech.strategy_stack.data_loader import DataLoader
from RenTech.strategy_stack.equity_universe_loaders import load_equity_panel_dict
from RenTech.strategy_stack.main import _compute_daily_backtest_features
from RenTech.strategy_stack.multi_strategy_manager import BuyTheDipSleeve
from RenTech.strategy_stack.run_sp500_dip_standard import _filter_equity_by_history

LOGS = _REPO / "RenTech" / "data" / "logs"
DEFAULT_FUND_DAILY = (
    LOGS
    / "best_ideas_stack_plus_equity_dip_plus_vxx_long_call_plus_macro_aw_plus_sector_momentum_plus_tactical_aw_plus_fund_mtm_daily.csv"
)
DEFAULT_TACTICAL_AW_ALLOCATIONS = LOGS / "tactical_aw_standard_allocations.csv"
DEFAULT_TACTICAL_AW_DAILY = LOGS / "tactical_aw_standard_daily.csv"
DEFAULT_MACRO_AW_TRADES = LOGS / "macro_aw_options_portfolio_eq_trades.csv"
DEFAULT_SECTOR_MOMENTUM_REBALANCES = LOGS / "sector_momentum_standard_rebalances.csv"
DEFAULT_SECTOR_MOMENTUM_DAILY = LOGS / "sector_momentum_standard_daily.csv"
DEFAULT_SPY_TRADES = LOGS / "lit_stack_vrp_margin_trades.csv"
DEFAULT_LONG_CALL_JSONL = (
    LOGS
    / "overall_portfolio_trade_audits"
    / "engine_vxx_1pct_2016_2026"
    / "vxx_portfolio_long_call.jsonl"
)

# Keys match ``combine_best_ideas_stack.DEFAULT_FUND_WEIGHTS``.
FUND_SLEEVE_KEYS = (
    "spy_theta",
    "vxx_regime",
    "equity_dip",
    "vxx_long_call",
    "macro_aw",
    "sector_momentum",
    "tactical_aw",
    "tsmom",
)

DAILY_STATE_COLUMN_GLOSSARY: dict[str, str] = {
    "fund_nav_usd": "Single fund-mode NAV (compounds daily weighted sleeve returns).",
    "fund_daily_pnl_usd": "Change in fund_nav_usd; equals sum of fund_pnl_*_usd sleeves (≈0 residual).",
    "fund_daily_return": "fund_daily_pnl_usd / prior fund_nav.",
    "fund_pnl_*_usd": "Sleeve contribution to fund NAV: fund_weight × sleeve standalone return × prior fund_nav.",
    "fund_ret_*": "Standalone sleeve daily return (from each sleeve's own $100k backtest), not fund return.",
    "fund_weight_*": "Capital budget share among active sleeves (sums to 1).",
    "fund_allocated_*_usd": "fund_nav_usd × fund_weight (notional budget label, not broker cash).",
    "fund_pnl_residual_usd": "fund_daily_pnl_usd − sum(fund_pnl_*); should be ~0.",
    "standalone_pnl_*_usd": "That sleeve's daily $ PnL on its own $100k backtest — does NOT add to fund NAV.",
    "standalone_equity_*_usd": "Cumulative equity from the sleeve's standalone backtest ($100k start).",
    "standalone_margin_spy_theta_usd": "SPY θ margin from lit_stack_vrp_margin sim on standalone $100k book.",
    "fund_margin_spy_theta_est_usd": "Linear estimate: standalone_margin × (fund_allocated_spy / standalone_spy_equity). Not broker fund margin.",
    "fund_bp_util_spy_theta_est": "fund_margin_spy_theta_est_usd / fund_nav_usd.",
    "activity_*": "Positions / tickers / flags — descriptive only, not fund $ attribution.",
}


def _norm_idx(s: pd.Series | pd.DatetimeIndex) -> pd.DatetimeIndex:
    return pd.to_datetime(s).dt.normalize() if isinstance(s, pd.Series) else pd.to_datetime(s).normalize()


def _trade_row_base(
    *,
    fund_book: str,
    sleeve: str,
    entry_date: str,
    exit_date: str,
    pnl_usd: float,
    **extra,
) -> dict:
    row = {
        "fund_book": fund_book,
        "sleeve": sleeve,
        "entry_date": entry_date,
        "exit_date": exit_date,
        "pnl_usd": float(pnl_usd),
    }
    row.update(extra)
    return row


def load_spy_margin_trades(path: Path, fund_weight_spy: float) -> pd.DataFrame:
    df = pd.read_csv(path)
    rows = []
    for r in df.itertuples(index=False):
        d = r._asdict()
        rows.append(
            _trade_row_base(
                fund_book="spy_theta",
                sleeve=str(d.get("sid", "")),
                entry_date=str(d["entry_date"]),
                exit_date=str(d["exit_date"]),
                pnl_usd=float(d["realized_pnl_usd"]),
                qty=int(d.get("qty", 1)),
                margin_reserved_usd=float(d.get("margin_reserved_usd", 0)),
                trade_kind=str(d.get("trade_kind", "")),
                regime=str(d.get("regime", "")),
                trade_params_json=str(d.get("trade_params_json", "")),
                legs_json=str(d.get("legs_json", "")),
                fund_weight_spy=fund_weight_spy,
                source_file=str(path.name),
            )
        )
    return pd.DataFrame(rows)


def load_macro_aw_trades(path: Path, fund_weight: float) -> pd.DataFrame:
    df = pd.read_csv(path)
    rows = []
    for r in df.itertuples(index=False):
        d = r._asdict()
        raw_qty = d.get("qty", d.get("contracts", 1))
        qty = 1
        if pd.notna(raw_qty):
            try:
                qty = int(raw_qty)
            except (TypeError, ValueError):
                qty = 1
        rows.append(
            _trade_row_base(
                fund_book="macro_aw",
                sleeve=str(d.get("sleeve", "")),
                entry_date=str(d.get("entry_date", ""))[:10],
                exit_date=str(d.get("exit_date", ""))[:10],
                pnl_usd=float(d.get("pnl_usd", 0)),
                qty=qty,
                trade_kind=str(d.get("strategy", d.get("trade_kind", ""))),
                legs_json=str(d.get("legs_json", "")),
                fund_weight=fund_weight,
                source_file=str(path.name),
            )
        )
    return pd.DataFrame(rows)


def load_tactical_aw_allocation_trades(
    alloc_path: Path,
    daily_path: Path,
    capital: float,
    fund_weight: float,
) -> pd.DataFrame:
    """Weight-change rows from tactical AW allocation audit."""
    alloc = pd.read_csv(alloc_path)
    if alloc.empty:
        return pd.DataFrame()

    daily = pd.read_csv(daily_path, parse_dates=["date"])
    daily["date"] = pd.to_datetime(daily["date"]).dt.normalize()
    daily = daily.set_index("date").sort_index()
    ret = daily["daily_ret"].astype(float)

    rows: list[dict] = []
    for dt, grp in alloc.groupby("date", sort=True):
        dt_ts = pd.Timestamp(dt).normalize()
        after = ret.index[ret.index > dt_ts]
        exit_ts = pd.Timestamp(after[0]).normalize() if len(after) else dt_ts
        period_ret = (
            float((1.0 + ret.loc[(ret.index >= dt_ts) & (ret.index < exit_ts)]).prod() - 1.0)
            if (ret.index >= dt_ts).any()
            else 0.0
        )
        tickers = grp["ticker"].astype(str).tolist()
        weights = {str(r["ticker"]): float(r["weight"]) for _, r in grp.iterrows()}
        rows.append(
            _trade_row_base(
                fund_book="tactical_aw",
                sleeve="tactical_aw",
                entry_date=str(dt_ts.date()),
                exit_date=str(exit_ts.date()),
                pnl_usd=float(capital) * period_ret,
                trade_kind="allocation_change",
                trade_params_json=json.dumps(
                    {
                        "tickers": tickers,
                        "weights": weights,
                        "cash_weight": float(grp["cash_weight"].iloc[0]),
                        "total_invested_weight": float(grp["total_invested_weight"].iloc[0]),
                        "period_return": period_ret,
                    }
                ),
                fund_weight=fund_weight,
                source_file=str(alloc_path.name),
            )
        )
        for _, r in grp.iterrows():
            rows.append(
                _trade_row_base(
                    fund_book="tactical_aw",
                    sleeve=f"tactical_aw_{r['ticker']}",
                    entry_date=str(dt_ts.date()),
                    exit_date=str(exit_ts.date()),
                    pnl_usd=0.0,
                    trade_kind="sleeve_weight",
                    trade_params_json=json.dumps(
                        {
                            "weight": float(r["weight"]),
                            "baseline_weight": float(r["baseline_weight"]),
                        }
                    ),
                    ticker=str(r["ticker"]),
                    fund_weight=fund_weight,
                    source_file=str(alloc_path.name),
                )
            )
    return pd.DataFrame(rows)


def load_sector_momentum_rebalance_trades(
    rebal_path: Path,
    daily_path: Path,
    capital: float,
    fund_weight: float,
) -> pd.DataFrame:
    """Monthly rebalance rows + per-ticker hold detail for fund ALL_TRADES."""
    rebal = pd.read_csv(rebal_path)
    if rebal.empty:
        return pd.DataFrame()

    daily = pd.read_csv(daily_path, parse_dates=["date"])
    daily["date"] = pd.to_datetime(daily["date"]).dt.normalize()
    daily = daily.set_index("date").sort_index()
    ret = daily["daily_ret"].astype(float)

    rows: list[dict] = []
    held_only = rebal.loc[rebal["weight"].astype(float) > 0].copy()
    for eff, grp in held_only.groupby("effective_date", sort=True):
        eff_ts = pd.Timestamp(eff).normalize()
        period_end = pd.Timestamp(grp["period_end_date"].iloc[0]).normalize()
        mask = (ret.index >= eff_ts) & (ret.index <= period_end)
        period_ret = float((1.0 + ret.loc[mask]).prod() - 1.0) if mask.any() else 0.0
        pnl_usd = float(capital) * period_ret

        tickers = grp.sort_values("rank")["ticker"].astype(str).tolist()
        mom_map = {
            str(r["ticker"]): float(r["aqr_mom"])
            for _, r in grp.iterrows()
            if pd.notna(r["aqr_mom"])
        }
        entries = [str(r["ticker"]) for _, r in grp.iterrows() if bool(r["is_new_entry"])]
        month_rebal = rebal.loc[rebal["effective_date"] == eff]
        exits_prior = month_rebal.loc[
            (month_rebal["weight"].astype(float) == 0.0)
            & month_rebal["is_exit_from_prior"].astype(bool)
        ]["ticker"].astype(str).tolist()

        rows.append(
            _trade_row_base(
                fund_book="sector_momentum",
                sleeve="sector_momentum",
                entry_date=str(eff_ts.date()),
                exit_date=str(period_end.date()),
                pnl_usd=pnl_usd,
                trade_kind="monthly_rebalance",
                trade_params_json=json.dumps(
                    {
                        "signal_date": str(grp["signal_date"].iloc[0]),
                        "top_k": int(grp["top_k"].iloc[0]),
                        "tickers_held": tickers,
                        "aqr_mom": mom_map,
                        "new_entries": entries,
                        "exits_from_prior": exits_prior,
                        "period_return": period_ret,
                    }
                ),
                fund_weight=fund_weight,
                source_file=str(rebal_path.name),
            )
        )

        for _, r in grp.iterrows():
            rows.append(
                _trade_row_base(
                    fund_book="sector_momentum",
                    sleeve=f"sector_momentum_{r['ticker']}",
                    entry_date=str(eff_ts.date()),
                    exit_date=str(period_end.date()),
                    pnl_usd=0.0,
                    trade_kind="sector_hold",
                    trade_params_json=json.dumps(
                        {
                            "weight": float(r["weight"]),
                            "aqr_mom": float(r["aqr_mom"]) if pd.notna(r["aqr_mom"]) else None,
                            "rank": int(r["rank"]),
                            "is_new_entry": bool(r["is_new_entry"]),
                        }
                    ),
                    ticker=str(r["ticker"]),
                    fund_weight=fund_weight,
                    source_file=str(rebal_path.name),
                )
            )

    return pd.DataFrame(rows)


def load_long_call_jsonl_trades(path: Path, fund_weight: float) -> pd.DataFrame:
    rows = []
    with path.open(encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            d = json.loads(line)
            rows.append(
                _trade_row_base(
                    fund_book="vxx_long_call",
                    sleeve="vxx_long_call",
                    entry_date=str(d.get("entry_date", ""))[:10],
                    exit_date=str(d.get("exit_date", ""))[:10],
                    pnl_usd=float(d.get("pnl_total", d.get("pnl_usd", 0))),
                    contracts=int(d.get("contracts", 1)),
                    broker_risk_usd=float(d.get("broker_risk_usd", 0)),
                    legs_json=str(d.get("entry_legs_json", d.get("legs_json", ""))),
                    exit_reason=str(d.get("exit_reason", "")),
                    fund_weight=fund_weight,
                    source_file=str(path.name),
                )
            )
    return pd.DataFrame(rows)


def collect_buy_the_dip_trades_and_daily(
    *,
    universe: str,
    start: str,
    end: str,
    yahoo_period: str,
    top_n: int,
    hold_days: int,
    pct_drop_min: float,
    refresh_cache: bool,
) -> tuple[pd.DataFrame, pd.DataFrame]:
    """
    Mirror ``BuyTheDipSleeve`` pct_drop logic; return (daily_state, trades) for one universe.
    """
    equity_dict = load_equity_panel_dict(
        universe,
        daily_period=yahoo_period,
        refresh_cache=refresh_cache,
    )
    min_first = pd.Timestamp(start).normalize() - pd.Timedelta(days=400)
    equity_dict = _filter_equity_by_history(equity_dict, min_first)
    spy_df = _compute_daily_backtest_features(
        DataLoader().fetch_daily("SPY", period=yahoo_period)
    )
    eng = BuyTheDipSleeve(
        signal_mode="pct_drop",
        pct_drop_min=pct_drop_min,
        hold_trading_days=hold_days,
    )
    # Reuse engine internals via generate_returns for calendar, then custom track
    daily_ret = eng.generate_returns(
        equity_dict,
        top_n=top_n,
        spy_df=spy_df,
        verbose=False,
    )
    master_index = pd.DatetimeIndex(pd.to_datetime(daily_ret.index).tz_localize(None))
    close_df = pd.DataFrame(
        {t: df["close"].astype(float) for t, df in equity_dict.items()}
    ).reindex(master_index).ffill()
    tickers = list(close_df.columns)
    N = len(tickers)
    D = len(master_index)
    hold = hold_days
    ret_chg = close_df.pct_change().fillna(0.0)
    sma200 = close_df.rolling(200, min_periods=200).mean()
    atr = (
        (close_df - close_df.shift(1)).abs()
        .rolling(5, min_periods=5)
        .mean()
        .div(close_df)
    )

    active_entry = np.full(N, -1, dtype=np.int32)
    trade_rows: list[dict] = []
    daily_rows: list[dict] = []

    for d in range(D):
        dt = master_index[d]
        expired = (active_entry >= 0) & (d > active_entry + hold)
        for j in np.flatnonzero(expired):
            entry_d = int(active_entry[j])
            entry_dt = master_index[entry_d]
            exit_dt = dt
            # PnL over hold using close-to-close from entry+1 through d
            pnl_frac = 0.0
            for k in range(entry_d + 1, d + 1):
                r = ret_chg.iloc[k, j]
                if np.isfinite(r):
                    pnl_frac += float(r)
            trade_rows.append(
                _trade_row_base(
                    fund_book="equity_dip",
                    sleeve=f"equity_dip_{universe}",
                    entry_date=entry_dt.strftime("%Y-%m-%d"),
                    exit_date=exit_dt.strftime("%Y-%m-%d"),
                    pnl_usd=pnl_frac * 100_000.0,
                    ticker=str(tickers[j]),
                    hold_sessions=int(d - entry_d),
                    universe=universe,
                    source_file="export_buy_the_dip_sim",
                )
            )
        active_entry[expired] = -1

        in_trade = (active_entry >= 0) & (active_entry < d) & (d <= active_entry + hold)
        held = np.flatnonzero(in_trade)
        n_held = int(held.size)
        tickers_held = ",".join(tickers[j] for j in held[:20])
        if held.size > 20:
            tickers_held += f",+{held.size - 20}_more"

        dip_sig = (
            (ret_chg.iloc[d] <= -pct_drop_min)
            & (close_df.iloc[d] > sma200.iloc[d])
            & np.isfinite(atr.iloc[d])
        )
        flat = active_entry < 0
        dip_ok = dip_sig.fillna(False).to_numpy(dtype=bool)
        cand = np.flatnonzero(flat & dip_ok)
        new_entries = 0
        if cand.size > 0:
            sc = atr.iloc[d, cand].astype(float)
            key = np.where(np.isfinite(sc), sc, -np.inf)
            order = np.argsort(-key, kind="stable")
            take_n = min(top_n, cand.size)
            chosen = cand[order[:take_n]]
            for j in chosen:
                active_entry[j] = d
            new_entries = int(take_n)

        daily_rows.append(
            {
                "date": dt,
                "universe": universe,
                "n_equity_positions_held": n_held,
                "n_new_dip_entries": new_entries,
                "tickers_held_sample": tickers_held,
                "gross_equal_weight": float(n_held) / float(top_n) if top_n else 0.0,
            }
        )

    daily = pd.DataFrame(daily_rows)
    trades = pd.DataFrame(trade_rows)
    daily = daily[(daily["date"] >= pd.Timestamp(start)) & (daily["date"] <= pd.Timestamp(end))]
    return daily, trades


def _ordered_daily_state_columns(cols: list[str]) -> list[str]:
    """Stable column order: fund → standalone → activity."""
    prefix_order = ("date", "fund_", "standalone_", "activity_", "fund_start_")
    ranked: list[tuple[int, int, str]] = []
    for i, c in enumerate(cols):
        rank = len(prefix_order)
        for j, p in enumerate(prefix_order):
            if c == p.rstrip("_") or c.startswith(p):
                rank = j
                break
        ranked.append((rank, i, c))
    ranked.sort()
    return [c for _, _, c in ranked]


def build_daily_fund_state(
    fund_daily: pd.DataFrame,
    spy_daily: pd.DataFrame,
    vxx_daily: pd.DataFrame,
    dip_daily: pd.DataFrame,
    fund_weights: dict[str, float],
    capital: float,
    *,
    equity_dip_split: tuple[float, float] = (0.5, 0.5),
) -> pd.DataFrame:
    """
    Daily fund state with explicit namespaces:

    * **fund_*** — sleeve $ that sums to ``fund_daily_pnl_usd`` (fund-mode combine).
    * **standalone_*** — each sleeve's own $100k backtest (reference; do not sum to fund NAV).
    * **activity_*** — open counts / tickers (no $ attribution).
    """
    fd = fund_daily.copy()
    fd["date"] = _norm_idx(fd["date"])
    fd = fd.set_index("date").sort_index()

    spy = spy_daily.copy()
    spy["date"] = _norm_idx(spy["date"])
    spy = spy.set_index("date").sort_index()

    vxx = vxx_daily.copy()
    vxx["date"] = _norm_idx(vxx["date"])
    vxx = vxx.set_index("date").sort_index()

    dip = dip_daily.copy()
    if len(dip):
        dip["date"] = _norm_idx(dip["date"])
        dip = dip.set_index("date").sort_index()

    idx = fd.index
    nav = fd["equity_mtm_usd"]
    out = pd.DataFrame(index=idx)

    # --- Fund NAV (single account) ---
    out["fund_nav_usd"] = nav
    out["fund_daily_pnl_usd"] = fd["pnl_best_ideas_mtm"]
    out["fund_daily_return"] = fd.get("portfolio_return_fund", fd.get("daily_return_mtm", 0))

    fund_pnl_cols: list[str] = []
    for key in FUND_SLEEVE_KEYS:
        w = float(fund_weights.get(key, 0))
        out[f"fund_weight_{key}"] = w
        out[f"fund_allocated_{key}_usd"] = nav * w
        pnl_col = f"pnl_fund_{key}"
        ret_col = f"ret_{key}"
        if pnl_col in fd.columns:
            out[f"fund_pnl_{key}_usd"] = fd[pnl_col]
            fund_pnl_cols.append(f"fund_pnl_{key}_usd")
        if ret_col in fd.columns:
            # Sleeve standalone return (input to fund combine), not fund return.
            out[f"fund_ret_{key}"] = fd[ret_col]

    if fund_pnl_cols:
        out["fund_pnl_sum_sleeves_usd"] = out[fund_pnl_cols].sum(axis=1)
        out["fund_pnl_residual_usd"] = out["fund_daily_pnl_usd"] - out["fund_pnl_sum_sleeves_usd"]

    # --- Standalone sleeve $ (each backtest on $100k; not additive to fund NAV) ---
    standalone_map = {
        "spy_theta": "pnl_spy_theta_mtm",
        "vxx_long_call": "pnl_vxx_long_call",
        "macro_aw": "pnl_macro_aw",
        "sector_momentum": "pnl_sector_momentum",
        "tactical_aw": "pnl_tactical_aw",
    }
    for key, src in standalone_map.items():
        if src in fd.columns:
            out[f"standalone_pnl_{key}_usd"] = fd[src]

    w_sp, w_r3k = equity_dip_split
    if "pnl_sp500_dip" in fd.columns:
        out["standalone_pnl_equity_dip_sp500_usd"] = fd["pnl_sp500_dip"]
    if "pnl_russell3000_dip" in fd.columns:
        out["standalone_pnl_equity_dip_russell3000_usd"] = fd["pnl_russell3000_dip"]
    if "pnl_sp500_dip" in fd.columns and "pnl_russell3000_dip" in fd.columns:
        out["standalone_pnl_equity_dip_blended_usd"] = (
            fd["pnl_sp500_dip"] * w_sp + fd["pnl_russell3000_dip"] * w_r3k
        )
    elif "pnl_sp500_dip" in fd.columns:
        out["standalone_pnl_equity_dip_blended_usd"] = fd["pnl_sp500_dip"]

    spy_al = spy.reindex(idx)
    out["standalone_equity_spy_theta_usd"] = spy_al["equity_mtm_usd"]
    out["standalone_margin_spy_theta_usd"] = spy_al["margin_total_usd"].fillna(0)
    out["standalone_bp_util_spy_theta"] = spy_al["bp_utilization"].fillna(0)
    out["standalone_unrealized_spy_theta_usd"] = spy_al["unrealized_mtm_usd"].fillna(0)

    w_spy = float(fund_weights.get("spy_theta", 0))
    spy_eq = out["standalone_equity_spy_theta_usd"].replace(0, np.nan)
    spy_scale = (out["fund_allocated_spy_theta_usd"] / spy_eq).fillna(0.0)
    out["fund_margin_spy_theta_est_usd"] = out["standalone_margin_spy_theta_usd"] * spy_scale
    out["fund_bp_util_spy_theta_est"] = np.where(
        nav > 0,
        out["fund_margin_spy_theta_est_usd"] / nav,
        0.0,
    )
    out["fund_unrealized_spy_theta_est_usd"] = (
        out["standalone_unrealized_spy_theta_usd"] * spy_scale
    )

    vxx_al = vxx.reindex(idx)
    out["standalone_pnl_vxx_regime_stack_usd"] = vxx_al.get("pnl_stack", 0).fillna(0)
    out["standalone_equity_vxx_regime_usd"] = vxx_al.get("equity_mtm_usd", np.nan)
    for c in vxx_al.columns:
        if c.startswith("pnl_") and c not in ("pnl_stack",):
            out[f"standalone_{c}_usd"] = vxx_al[c].fillna(0)

    if len(dip):
        dip_al = dip.reindex(idx).fillna(0)
        out["activity_equity_dip_positions_held"] = dip_al.get(
            "n_equity_positions_held", 0
        ).astype(int)
        out["activity_equity_dip_new_entries"] = dip_al.get("n_new_dip_entries", 0).astype(
            int
        )
        out["activity_equity_dip_tickers_held"] = dip_al.get("tickers_held_sample", "")
        if "n_slots_sp500" in dip_al.columns:
            out["activity_equity_dip_slots_sp500"] = dip_al["n_slots_sp500"]
            out["activity_equity_dip_slots_russell3000"] = dip_al["n_slots_russell3000"]
    else:
        out["activity_equity_dip_positions_held"] = 0
        out["activity_equity_dip_new_entries"] = 0
        out["activity_equity_dip_tickers_held"] = ""

    out["activity_spy_theta_n_open"] = spy_al["n_open_positions"].fillna(0).astype(int)
    out["activity_spy_theta_n_open_vrp"] = spy_al["n_open_vrp"].fillna(0).astype(int)
    out["activity_spy_theta_open_sleeves"] = spy_al["open_sleeves"].fillna("").astype(str)

    out["fund_start_capital_usd"] = float(capital)
    out = out.reset_index(names="date")
    ordered = _ordered_daily_state_columns(list(out.columns))
    return out[ordered]


def collect_vxx_regime_trades(
    start: str,
    end: str,
    capital: float,
    risk_budget: float,
    fund_weight: float,
) -> pd.DataFrame:
    """Slow: re-run six regime backtests and flatten ``Trade`` rows."""
    from RenTech.strategy_stack.backtest_vxx_bear_call_contango import run_backtest
    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 (
        _ensure_vx3_panel,
        _load_contango,
        run_one_spec,
    )
    from RenTech.strategy_stack.run_vxx_regime_mtm_report import (
        REGIME_IDEAS,
        _curated_spec_by_name,
    )

    ct = _ensure_vx3_panel(_load_contango())
    dates = [
        pd.Timestamp(d).normalize()
        for d in sorted(ct.index)
        if start <= str(d.date()) <= end
    ]
    ct_put = enrich_vix_panel(_load_contango(), start, end, 60)
    rows: list[dict] = []
    for idea in REGIME_IDEAS:
        name = idea["name"]
        if idea["source"] == "vx1_vx3":
            spec = _curated_spec_by_name(idea["spec_name"])
            trades = run_one_spec(
                spec,
                ct,
                dates,
                risk_budget_usd=risk_budget,
                daily_mtm_records=None,
                capital=capital,
            )
        elif idea["source"] == "long_put":
            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_put,
                dates,
                entry=entry,
                moneyness=-0.03,
                dte_min=14,
                dte_max=28,
                hold_days=20,
                risk_budget_usd=risk_budget,
            )
        else:
            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,
            )
        for t in trades:
            if hasattr(t, "__dataclass_fields__"):
                d = asdict(t)
            elif isinstance(t, dict):
                d = t
            else:
                continue
            rows.append(
                _trade_row_base(
                    fund_book="vxx_regime",
                    sleeve=name,
                    entry_date=str(d.get("entry_date", ""))[:10],
                    exit_date=str(d.get("exit_date", ""))[:10],
                    pnl_usd=float(d.get("pnl_total", d.get("pnl_usd", 0))),
                    strategy=str(d.get("strategy", name)),
                    contracts=int(d.get("contracts", 1)),
                    broker_risk_usd=float(d.get("broker_risk_usd", 0)),
                    exit_reason=str(d.get("exit_reason", "")),
                    fund_weight=fund_weight,
                    source_file="run_vxx_regime_mtm_report",
                )
            )
    return pd.DataFrame(rows)


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-04-02")
    ap.add_argument("--capital", type=float, default=100_000.0)
    ap.add_argument("--fund-daily", type=Path, default=DEFAULT_FUND_DAILY)
    ap.add_argument("--spy-mtm-daily", type=Path, default=DEFAULT_LIT_MTM_DAILY)
    ap.add_argument("--spy-trades", type=Path, default=DEFAULT_SPY_TRADES)
    ap.add_argument("--vxx-daily", type=Path, default=DEFAULT_VXX_DAILY)
    ap.add_argument("--long-call-jsonl", type=Path, default=DEFAULT_LONG_CALL_JSONL)
    ap.add_argument("--macro-aw-trades", type=Path, default=DEFAULT_MACRO_AW_TRADES)
    ap.add_argument(
        "--sector-momentum-rebalances",
        type=Path,
        default=DEFAULT_SECTOR_MOMENTUM_REBALANCES,
    )
    ap.add_argument(
        "--sector-momentum-daily",
        type=Path,
        default=DEFAULT_SECTOR_MOMENTUM_DAILY,
    )
    ap.add_argument(
        "--run-sector-momentum",
        action="store_true",
        help="Regenerate sector momentum daily + rebalance CSV before export",
    )
    ap.add_argument(
        "--tactical-aw-allocations",
        type=Path,
        default=DEFAULT_TACTICAL_AW_ALLOCATIONS,
    )
    ap.add_argument("--tactical-aw-daily", type=Path, default=DEFAULT_TACTICAL_AW_DAILY)
    ap.add_argument(
        "--run-tactical-aw",
        action="store_true",
        help="Regenerate tactical AW daily + allocation CSV before export",
    )
    ap.add_argument("--out-prefix", type=Path, default=LOGS / "best_ideas_fund")
    ap.add_argument(
        "--include-vxx-regime-trades",
        action="store_true",
        help="Re-run six VXX regime backtests for trade log (slow; chains)",
    )
    ap.add_argument(
        "--refresh-dip-cache",
        action="store_true",
        help="Pass refresh_cache to Yahoo equity dip download",
    )
    ap.add_argument(
        "--dip-universe",
        choices=("sp500", "russell3000", "both"),
        default="both",
        help="Which equity dip universes to simulate for trades/daily (both is slow first run)",
    )
    ap.add_argument(
        "--skip-dip-position-export",
        action="store_true",
        help="Omit equity dip holdings columns (use fund dip return cols only)",
    )
    ap.add_argument(
        "--sp500-dip-top-n",
        type=int,
        default=DEFAULT_SP500_DIP_TOP_N,
        help="Max new S&P 500 dip entries per day in trade export (default 5)",
    )
    ap.add_argument(
        "--russell3000-dip-top-n",
        type=int,
        default=DEFAULT_RUSSELL3000_DIP_TOP_N,
        help="Max new Russell 3000 dip entries per day in trade export (default 10)",
    )
    ap.add_argument("--yahoo-period", default="max")
    args = ap.parse_args()

    if not args.fund_daily.is_file():
        raise SystemExit(
            f"Missing {args.fund_daily}\n"
            "Run combine_best_ideas_stack.py with --mtm --fund-mode first."
        )

    if args.run_sector_momentum:
        import subprocess

        sm_prefix = args.sector_momentum_rebalances.expanduser().resolve()
        sm_prefix = sm_prefix.with_name(sm_prefix.name.replace("_rebalances.csv", ""))
        cmd = [
            sys.executable,
            str(_REPO / "RenTech/strategy_stack/run_sector_momentum_standard.py"),
            "--start",
            args.start,
            "--capital",
            str(args.capital),
            "--out-prefix",
            str(sm_prefix),
        ]
        if args.end:
            cmd += ["--end", args.end]
        print("Running sector momentum standard (daily + rebalance log) …", flush=True)
        subprocess.run(cmd, check=True, cwd=str(_REPO))

    if args.run_tactical_aw:
        import subprocess

        tac_prefix = args.tactical_aw_allocations.expanduser().resolve()
        tac_prefix = tac_prefix.with_name(tac_prefix.name.replace("_allocations.csv", ""))
        cmd = [
            sys.executable,
            str(_REPO / "RenTech/strategy_stack/run_tactical_all_weather_standard.py"),
            "--start",
            args.start,
            "--capital",
            str(args.capital),
            "--out-prefix",
            str(tac_prefix),
        ]
        if args.end:
            cmd += ["--end", args.end]
        print("Running tactical All Weather standard …", flush=True)
        subprocess.run(cmd, check=True, cwd=str(_REPO))

    fund_daily_probe = pd.read_csv(args.fund_daily, nrows=1)
    has_macro = "pnl_fund_macro_aw" in fund_daily_probe.columns
    has_sector_mom = "pnl_fund_sector_momentum" in fund_daily_probe.columns
    has_tactical = "pnl_fund_tactical_aw" in fund_daily_probe.columns
    has_tsmom = "pnl_fund_tsmom" in fund_daily_probe.columns
    fund_weights = _resolve_fund_weights(
        spy=None,
        vxx=None,
        equity_dip=None,
        vxx_long_call=None,
        macro_aw=None,
        sector_momentum=None,
        tactical_aw=None,
        tsmom=None,
        active={
            "spy_theta": True,
            "vxx_regime": True,
            "equity_dip": True,
            "vxx_long_call": True,
            "macro_aw": has_macro,
            "sector_momentum": has_sector_mom,
            "tactical_aw": has_tactical,
            "tsmom": has_tsmom,
        },
    )
    w_sp, w_r3k = _resolve_equity_dip_weights(
        sp500_frac=None,
        russell3000_frac=None,
        sharpe_weight=True,
        equal_weight=False,
    )

    fund_daily = pd.read_csv(args.fund_daily, parse_dates=["date"])
    spy_daily = pd.read_csv(args.spy_mtm_daily, parse_dates=["date"])
    vxx_daily = pd.read_csv(args.vxx_daily, parse_dates=["date"])

    print("Collecting equity dip daily state + trades …", flush=True)
    dip_daily_parts = []
    dip_trades_parts = []
    universes = []
    if args.dip_universe in ("sp500", "both"):
        universes.append("sp500")
    if args.dip_universe in ("russell3000", "both"):
        universes.append("russell3000")
    if not args.skip_dip_position_export:
        for uni in universes:
            uni_top_n = (
                int(args.sp500_dip_top_n)
                if uni == "sp500"
                else int(args.russell3000_dip_top_n)
            )
            d_daily, d_trades = collect_buy_the_dip_trades_and_daily(
                universe=uni,
                start=args.start,
                end=args.end,
                yahoo_period=args.yahoo_period,
                top_n=uni_top_n,
                hold_days=10,
                pct_drop_min=0.03,
                refresh_cache=args.refresh_dip_cache,
            )
            dip_daily_parts.append(d_daily)
            dip_trades_parts.append(d_trades)
    dip_daily = pd.DataFrame()
    if dip_daily_parts:
        if len(dip_daily_parts) == 1:
            dip_daily = dip_daily_parts[0].copy()
            held = dip_daily["n_equity_positions_held"]
            if universes[0] == "sp500":
                dip_daily["n_slots_sp500"] = held
                dip_daily["n_slots_russell3000"] = 0
            else:
                dip_daily["n_slots_sp500"] = 0
                dip_daily["n_slots_russell3000"] = held
        else:
            dip_daily_sp = dip_daily_parts[0].set_index("date")
            dip_daily_r3 = dip_daily_parts[1].set_index("date")
            idx_u = dip_daily_sp.index.union(dip_daily_r3.index)
            dip_daily = pd.DataFrame(index=idx_u)
            dip_daily["n_slots_sp500"] = dip_daily_sp["n_equity_positions_held"].reindex(
                idx_u
            ).fillna(0)
            dip_daily["n_slots_russell3000"] = dip_daily_r3[
                "n_equity_positions_held"
            ].reindex(idx_u).fillna(0)
            dip_daily["n_equity_positions_held"] = (
                dip_daily["n_slots_sp500"] + dip_daily["n_slots_russell3000"]
            )
            dip_daily["n_new_dip_entries"] = (
                dip_daily_sp["n_new_dip_entries"].reindex(idx_u).fillna(0)
                + dip_daily_r3["n_new_dip_entries"].reindex(idx_u).fillna(0)
            )
            tickers_combined = []
            for dt in idx_u:
                a = (
                    dip_daily_sp.loc[dt, "tickers_held_sample"]
                    if dt in dip_daily_sp.index
                    else ""
                )
                b = (
                    dip_daily_r3.loc[dt, "tickers_held_sample"]
                    if dt in dip_daily_r3.index
                    else ""
                )
                parts = [x for x in (str(a), str(b)) if x and x != "nan"]
                tickers_combined.append("; ".join(parts)[:500])
            dip_daily["tickers_held_sample"] = tickers_combined
            dip_daily = dip_daily.reset_index(names="date")

    dip_trades = (
        pd.concat(dip_trades_parts, ignore_index=True) if dip_trades_parts else pd.DataFrame()
    )
    if len(dip_trades):
        dip_trades["exit_date"] = pd.to_datetime(dip_trades["exit_date"])
        dip_trades = dip_trades[
            (dip_trades["exit_date"] >= pd.Timestamp(args.start))
            & (dip_trades["exit_date"] <= pd.Timestamp(args.end))
        ]
        dip_trades["exit_date"] = dip_trades["exit_date"].dt.strftime("%Y-%m-%d")
        dip_trades["fund_weight_dip_slot"] = float(fund_weights["equity_dip"])

    daily_state = build_daily_fund_state(
        fund_daily,
        spy_daily,
        vxx_daily,
        dip_daily,
        fund_weights,
        args.capital,
        equity_dip_split=(w_sp, w_r3k),
    )

    trade_parts = []
    if args.spy_trades.is_file():
        print(f"Loading SPY margin trades from {args.spy_trades} …", flush=True)
        trade_parts.append(load_spy_margin_trades(args.spy_trades, fund_weights["spy_theta"]))
    if args.long_call_jsonl.is_file():
        print(f"Loading VXX long call trades from {args.long_call_jsonl} …", flush=True)
        trade_parts.append(
            load_long_call_jsonl_trades(
                args.long_call_jsonl, fund_weights["vxx_long_call"]
            )
        )
    if has_macro and args.macro_aw_trades.is_file():
        print(f"Loading Macro AW trades from {args.macro_aw_trades} …", flush=True)
        trade_parts.append(
            load_macro_aw_trades(args.macro_aw_trades, fund_weights["macro_aw"])
        )
    if has_sector_mom and args.sector_momentum_rebalances.is_file():
        if not args.sector_momentum_daily.is_file():
            raise SystemExit(
                f"Missing {args.sector_momentum_daily}; run run_sector_momentum_standard.py "
                "or pass --run-sector-momentum"
            )
        print(
            f"Loading sector momentum rebalances from {args.sector_momentum_rebalances} …",
            flush=True,
        )
        trade_parts.append(
            load_sector_momentum_rebalance_trades(
                args.sector_momentum_rebalances,
                args.sector_momentum_daily,
                args.capital,
                fund_weights["sector_momentum"],
            )
        )
    if has_tactical and args.tactical_aw_allocations.is_file():
        if not args.tactical_aw_daily.is_file():
            raise SystemExit(
                f"Missing {args.tactical_aw_daily}; run run_tactical_all_weather_standard.py "
                "or pass --run-tactical-aw"
            )
        print(
            f"Loading tactical AW allocations from {args.tactical_aw_allocations} …",
            flush=True,
        )
        trade_parts.append(
            load_tactical_aw_allocation_trades(
                args.tactical_aw_allocations,
                args.tactical_aw_daily,
                args.capital,
                fund_weights["tactical_aw"],
            )
        )
    if len(dip_trades):
        trade_parts.append(dip_trades)

    if args.include_vxx_regime_trades:
        print("Running VXX regime backtests for trade log (slow) …", flush=True)
        trade_parts.append(
            collect_vxx_regime_trades(
                args.start,
                args.end,
                args.capital,
                risk_budget=3000.0,
                fund_weight=fund_weights["vxx_regime"],
            )
        )

    all_trades = pd.concat(trade_parts, ignore_index=True) if trade_parts else pd.DataFrame()
    all_trades = all_trades.sort_values(["exit_date", "fund_book", "sleeve"]).reset_index(
        drop=True
    )

    prefix = args.out_prefix.expanduser().resolve()
    prefix.parent.mkdir(parents=True, exist_ok=True)
    daily_path = Path(f"{prefix}_daily_state.csv")
    trades_path = Path(f"{prefix}_ALL_TRADES.csv")
    meta_path = Path(f"{prefix}_reports_meta.json")

    daily_state.to_csv(daily_path, index=False)
    all_trades.to_csv(trades_path, index=False)

    meta = {
        "start": args.start,
        "end": args.end,
        "capital_usd": args.capital,
        "fund_weights": fund_weights,
        "equity_dip_split": {"sp500": w_sp, "russell3000": w_r3k},
        "n_daily_rows": int(len(daily_state)),
        "n_trades": int(len(all_trades)),
        "trades_by_book": all_trades.groupby("fund_book").size().to_dict()
        if len(all_trades)
        else {},
        "daily_state_csv": str(daily_path),
        "all_trades_csv": str(trades_path),
        "fund_daily_source": str(args.fund_daily.resolve()),
        "vxx_regime_trades_included": bool(args.include_vxx_regime_trades),
        "daily_state_column_glossary": DAILY_STATE_COLUMN_GLOSSARY,
        "attribution_rules": {
            "fund_pnl_sums_to_nav": (
                "sum(fund_pnl_*_usd) ≈ fund_daily_pnl_usd "
                "(spy, vxx, equity_dip, vxx_long_call, macro_aw, sector_momentum, tactical_aw, tsmom when present)"
            ),
            "standalone_pnl": (
                "Each standalone_pnl_*_usd is that sleeve on its own $100k; "
                "do not sum to fund NAV."
            ),
            "margin": (
                "Only SPY θ has margin in data. fund_margin_spy_theta_est_usd is a "
                "linear scale of standalone margin to the fund's SPY budget — not "
                "unified fund margin. VXX / equity dip margin is not modeled."
            ),
            "activity": "Position counts and tickers are for audit; not fund $ slices.",
        },
        "note_vxx_regime": (
            "Per-regime standalone_pnl_* on VXX are reference only. "
            "Fund VXX attribution is fund_pnl_vxx_regime_usd. "
            "Per-structure trade rows require --include-vxx-regime-trades."
        ),
        "sector_momentum_rebalances_csv": str(args.sector_momentum_rebalances.resolve())
        if has_sector_mom
        else None,
        "note_sector_momentum": (
            "Sector momentum trades are monthly ETF rebalances (not options). "
            "See trade_kind monthly_rebalance and sector_hold rows; detail in "
            "sector_momentum_standard_rebalances.csv."
        ),
        "tactical_aw_allocations_csv": str(args.tactical_aw_allocations.resolve())
        if has_tactical
        else None,
        "note_tactical_aw": (
            "Tactical All Weather trades are ETF allocation changes (not options). "
            "See trade_kind allocation_change / sleeve_weight; detail in "
            "tactical_aw_standard_allocations.csv."
        ),
    }
    meta_path.write_text(json.dumps(meta, indent=2) + "\n")

    print(f"\nDaily fund state → {daily_path}  ({len(daily_state)} rows)", flush=True)
    print(f"All trades       → {trades_path}  ({len(all_trades)} rows)", flush=True)
    print(f"Meta             → {meta_path}", flush=True)
    if len(all_trades):
        print("\nTrades by book:", flush=True)
        for k, v in meta["trades_by_book"].items():
            print(f"  {k}: {v}", flush=True)


if __name__ == "__main__":
    main()
