#!/usr/bin/env python3
"""
Canonical **Tactical All Weather** equity sleeve (Bridgewater-style macro rotation).

Uses :class:`TacticalAllWeatherManager` on SPY/TLT/IEF/GLD/DBC with per-sleeve
``close > SMA(200)`` and ``aqr_mom > 0`` gates; uninvested weight earns cash yield.

Example::

    cd /Users/robzingale/trading_bot
    PYTHONUNBUFFERED=1 .venv/bin/python RenTech/strategy_stack/run_tactical_all_weather_standard.py \\
        --start 2016-01-04 --yahoo-period max
"""

from __future__ import annotations

import argparse
import json
import sys
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.main import _compute_daily_backtest_features
from RenTech.strategy_stack.portfolio_risk_manager import (
    BASE_WEIGHTS,
    TacticalAWConfig,
    TacticalAllWeatherManager,
)

LOGS = _REPO / "RenTech" / "data" / "logs"
DEFAULT_OUT_PREFIX = LOGS / "tactical_aw_standard"
MACRO_TICKERS = tuple(BASE_WEIGHTS.keys())


def _load_macro_dict(yahoo_period: str) -> dict[str, pd.DataFrame]:
    loader = DataLoader()
    out: dict[str, pd.DataFrame] = {}
    for t in MACRO_TICKERS:
        daily = loader.fetch_daily(t, period=yahoo_period)
        if daily.empty:
            raise RuntimeError(f"No daily data for {t}")
        out[t] = _compute_daily_backtest_features(daily)
    return out


def _allocation_audit(portfolio_df: pd.DataFrame) -> pd.DataFrame:
    """One row per (date, ticker) when applied sleeve weight changes."""
    weight_cols = [c for c in portfolio_df.columns if c.startswith("weight_")]
    if not weight_cols:
        return pd.DataFrame()
    w = portfolio_df[weight_cols].astype(np.float64)
    tickers = [c.replace("weight_", "") for c in weight_cols]
    w.columns = tickers
    changed = w.diff().abs().sum(axis=1) > 1e-9
    changed.iloc[0] = True
    rows: list[dict] = []
    for dt in w.index[changed]:
        cash_w = float(portfolio_df.loc[dt, "cash_weight"])
        for tkr in tickers:
            rows.append(
                {
                    "date": pd.Timestamp(dt).strftime("%Y-%m-%d"),
                    "ticker": tkr,
                    "weight": float(w.loc[dt, tkr]),
                    "baseline_weight": float(BASE_WEIGHTS[tkr]),
                    "cash_weight": cash_w,
                    "total_invested_weight": float(portfolio_df.loc[dt, "total_invested_weight"]),
                }
            )
    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="")
    ap.add_argument("--yahoo-period", default="max")
    ap.add_argument("--capital", type=float, default=100_000.0)
    ap.add_argument("--cash-yield", type=float, default=0.04, help="Annual cash yield (default 4%%)")
    ap.add_argument(
        "--config-json",
        type=Path,
        default=None,
        help="Optional TacticalAWConfig JSON (from tactical_aw_variant_sweep_best.json)",
    )
    ap.add_argument("--out-prefix", type=Path, default=DEFAULT_OUT_PREFIX)
    args = ap.parse_args()

    macro_dict = _load_macro_dict(args.yahoo_period)
    cfg = TacticalAWConfig()
    if args.config_json is not None:
        raw = json.loads(Path(args.config_json).read_text(encoding="utf-8"))
        if "config" in raw:
            raw = raw["config"]
        cfg_keys = {f.name for f in TacticalAWConfig.__dataclass_fields__.values()}
        cfg = TacticalAWConfig(**{k: raw[k] for k in raw if k in cfg_keys})
    pm = TacticalAllWeatherManager(config=cfg)
    port = pm.build_portfolio(macro_dict, cash_annual_yield=float(args.cash_yield))
    port.index = pd.to_datetime(port.index).tz_localize(None)
    r_full = port["portfolio_bar_ret"].astype(np.float64)

    mask = port.index >= pd.Timestamp(args.start)
    if args.end.strip():
        mask &= port.index <= pd.Timestamp(args.end)
    r = r_full.loc[mask].fillna(0.0)
    port_win = port.loc[mask]

    cap = float(args.capital)
    pnl = r * cap
    eq_unit = (1.0 + r).cumprod()
    eq_usd = cap * eq_unit

    n = len(r)
    years = n / 252.0
    end_eq = float(eq_usd.iloc[-1])
    total_ret = end_eq / cap - 1.0
    cagr = (end_eq / cap) ** (1.0 / years) - 1.0 if years > 0 else float("nan")
    dd = eq_usd / eq_usd.cummax() - 1.0
    max_dd = float(dd.min())
    sd = float(r.std(ddof=1)) if n > 1 else float("nan")
    sharpe = float(r.mean() / sd * np.sqrt(252.0)) if sd > 1e-12 else float("nan")

    spy_r = macro_dict["SPY"]["ret"].astype(float)
    spy_r.index = pd.to_datetime(spy_r.index).tz_localize(None)
    rho_spy = float(pd.DataFrame({"aw": r, "spy": spy_r}).dropna().corr().iloc[0, 1])

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

    # Margin estimate: all positions are long ETFs → portfolio margin rate 15%.
    invested_w = port_win["total_invested_weight"].reindex(r.index).fillna(0.0)
    margin_usd_series = eq_usd * invested_w * 0.15

    static_r = port_win["static_all_weather_ret"].reindex(r.index).fillna(0.0)
    out = pd.DataFrame(
        {
            "date": r.index.strftime("%Y-%m-%d"),
            "daily_ret": r.values,
            "static_daily_ret": static_r.values,
            "daily_pnl_usd": pnl.values,
            "equity_unit": eq_unit.values,
            "equity_usd": eq_usd.values,
            "total_invested_weight": invested_w.values,
            "margin_usd": margin_usd_series.values,
        }
    )
    out.to_csv(daily_path, index=False)

    alloc = _allocation_audit(port_win)
    alloc.to_csv(alloc_path, index=False)

    meta = {
        "strategy": "tactical_all_weather",
        "baseline_weights": BASE_WEIGHTS,
        "start": str(r.index.min().date()) if len(r) else None,
        "end": str(r.index.max().date()) if len(r) else None,
        "capital": cap,
        "cash_annual_yield": float(args.cash_yield),
        "total_return_pct": float(total_ret * 100.0),
        "cagr_pct": float(cagr * 100.0),
        "sharpe": sharpe,
        "max_drawdown_pct": float(max_dd * 100.0),
        "corr_vs_spy": rho_spy,
        "daily_csv": str(daily_path),
        "allocations_csv": str(alloc_path),
    }
    meta_path.write_text(json.dumps(meta, indent=2) + "\n")

    cmd = (
        f"cd {_REPO} && PYTHONUNBUFFERED=1 .venv/bin/python "
        f"RenTech/strategy_stack/run_tactical_all_weather_standard.py "
        f"--start {args.start} --yahoo-period {args.yahoo_period}"
    )
    if args.end.strip():
        cmd += f" --end {args.end}"
    metrics_path.write_text(
        f"# Tactical All Weather standard ({meta['start']} -> {meta['end']})\n\n"
        f"Command:\n{cmd}\n\n"
        f"Headline (${cap:,.0f} notional):\n"
        f"  Total return: {total_ret * 100:.2f}%\n"
        f"  CAGR: {cagr * 100:.2f}%\n"
        f"  Sharpe: {sharpe:.3f}\n"
        f"  Max DD: {max_dd * 100:.2f}%\n"
        f"  Corr vs SPY: {rho_spy:.3f}\n\n"
        f"Artifacts:\n  {daily_path}\n  {alloc_path}\n  {meta_path}\n"
    )

    print(f"# Tactical All Weather standard ({meta['start']} -> {meta['end']})\n")
    print(f"Command:\n{cmd}\n")
    print(f"Headline (${cap:,.0f} notional):")
    print(f"  Total return: {total_ret * 100:.1f}%")
    print(f"  CAGR: {cagr * 100:.2f}%")
    print(f"  Sharpe: {sharpe:.3f}")
    print(f"  Max DD: {max_dd * 100:.2f}%")
    print(f"  Corr vs SPY: {rho_spy:.3f}")
    print(f"\nWrote {daily_path}")
    print(f"Wrote {alloc_path}  ({len(alloc)} rows)")


if __name__ == "__main__":
    main()
