#!/usr/bin/env python3
"""
**Rates Carry Sleeve** — systematic fixed-income carry + trend.

Three sub-strategies combined with equal weight:

1. **Duration carry**: long TLT/IEF when 10yr minus 3m yield spread > 0 (normal curve);
   move to SHY (short-duration) when inverted. Rebalance monthly.

2. **Bond trend**: long TLT when TLT > its 200-day SMA; else long SHY.
   Simple momentum — bonds trend for months to years.

3. **Inflation regime**: long TIP (inflation-linked) when trailing 3m CPI proxy
   (DBC/commodity momentum) is positive; else long IEF (nominal).

Why this helps weak equity years:
  2015: yield curve normal, bonds trending → flat/positive
  2018: curve flattened early → rotated to SHY before TLT fell; TIP helped
  2019: easing cycle, long TLT +14%
  2022: curve inverted Feb 2022 → exits TLT before worst losses; rotates to SHY

Data: yfinance (TLT, IEF, SHY, TIP, DBC); yield spread from ^TNX (10yr) and ^IRX (3m).
Output: daily CSV compatible with combine_best_ideas_stack.py.

Example::

    cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 \\
      .venv/bin/python RenTech/strategy_stack/run_rates_carry.py \\
      --start 2011-01-03 --end 2025-12-31 --capital 100000
"""

from __future__ import annotations

import argparse
import json
import math
import sys
from pathlib import Path

import numpy as np
import pandas as pd
import yfinance as yf

_REPO = Path(__file__).resolve().parents[2]
LOGS = _REPO / "RenTech" / "data" / "logs"


def _download(tickers: list[str], start: str, end: str) -> pd.DataFrame:
    raw = yf.download(tickers, start=start, end=end, auto_adjust=True, progress=False)
    if isinstance(raw.columns, pd.MultiIndex):
        close = raw["Close"].copy()
    else:
        close = raw.copy()
    close.index = pd.to_datetime(close.index).tz_localize(None)
    return close.sort_index().ffill(limit=5)


def run_rates_carry(
    start: str,
    end: str,
    capital: float,
    *,
    out_prefix: Path,
    sma_window: int = 200,
    verbose: bool = True,
) -> dict:
    t0 = pd.Timestamp(start)
    t1 = pd.Timestamp(end)
    fetch_start = (t0 - pd.DateOffset(years=2)).strftime("%Y-%m-%d")
    fetch_end = t1.strftime("%Y-%m-%d")

    # Bond ETFs
    etfs = _download(["TLT", "IEF", "SHY", "TIP", "DBC"], fetch_start, fetch_end)
    # Treasury yields (^TNX = 10yr, ^IRX = 3m)
    yields_raw = yf.download(["^TNX", "^IRX"], start=fetch_start, end=fetch_end,
                              auto_adjust=True, progress=False)
    if isinstance(yields_raw.columns, pd.MultiIndex):
        yields = yields_raw["Close"].copy()
    else:
        yields = yields_raw.copy()
    yields.index = pd.to_datetime(yields.index).tz_localize(None)
    yields = yields.sort_index().ffill(limit=10)
    yields.columns = [c.replace("^", "") for c in yields.columns]

    idx = etfs.index[(etfs.index >= t0) & (etfs.index <= t1)]

    sma_tlt = etfs["TLT"].rolling(sma_window, min_periods=sma_window // 2).mean()
    sma_dbc = etfs["DBC"].rolling(63, min_periods=30).mean() if "DBC" in etfs.columns else None

    # Monthly rebalance dates
    rebal_dates = etfs.resample("BME").last().index

    # Sub-strategy signals (updated monthly, applied next day)
    sig1 = pd.Series(index=etfs.index, dtype=str)  # carry: "TLT"/"IEF"/"SHY"
    sig2 = pd.Series(index=etfs.index, dtype=str)  # trend: "TLT"/"SHY"
    sig3 = pd.Series(index=etfs.index, dtype=str)  # inflation: "TIP"/"IEF"

    cur1, cur2, cur3 = "IEF", "SHY", "IEF"

    for rd in rebal_dates:
        if rd not in etfs.index:
            prior = etfs.index[etfs.index <= rd]
            if not len(prior):
                continue
            rd = prior[-1]

        # Sub-strategy 1: yield-curve carry
        if "TNX" in yields.columns and "IRX" in yields.columns:
            tnx = yields["TNX"].reindex([rd]).iloc[0] if rd in yields.index else float("nan")
            irx = yields["IRX"].reindex([rd]).iloc[0] if rd in yields.index else float("nan")
            if math.isfinite(tnx) and math.isfinite(irx):
                spread = tnx - irx
                if spread > 1.0:
                    cur1 = "TLT"
                elif spread > 0.0:
                    cur1 = "IEF"
                else:
                    cur1 = "SHY"

        # Sub-strategy 2: bond trend (TLT vs SMA200)
        if rd in sma_tlt.index:
            tlt_px = etfs["TLT"].reindex([rd]).iloc[0]
            sma_v  = sma_tlt.reindex([rd]).iloc[0]
            if math.isfinite(tlt_px) and math.isfinite(sma_v):
                cur2 = "TLT" if tlt_px > sma_v else "SHY"

        # Sub-strategy 3: inflation regime (DBC trend as commodity/inflation proxy)
        if sma_dbc is not None and rd in sma_dbc.index and "DBC" in etfs.columns:
            dbc_px  = etfs["DBC"].reindex([rd]).iloc[0]
            dbc_sma = sma_dbc.reindex([rd]).iloc[0]
            if math.isfinite(dbc_px) and math.isfinite(float(dbc_sma)):
                cur3 = "TIP" if dbc_px > float(dbc_sma) else "IEF"

        # Apply from day after rebalance to next rebalance
        next_rdates = rebal_dates[rebal_dates > rd]
        next_rd = next_rdates[0] if len(next_rdates) else etfs.index[-1]
        mask = (etfs.index > rd) & (etfs.index <= next_rd)
        sig1[mask] = cur1
        sig2[mask] = cur2
        sig3[mask] = cur3

    # Fill any gaps at the start
    sig1 = sig1.ffill().fillna("IEF")
    sig2 = sig2.ffill().fillna("SHY")
    sig3 = sig3.ffill().fillna("IEF")

    # Combine: equal 1/3 weight to each sub-strategy
    # Each sub-strategy is 100% in one ETF → portfolio return = mean of 3 daily returns
    rets = etfs.pct_change().fillna(0.0)

    port_daily = pd.Series(0.0, index=idx)
    for dt in idx:
        if dt not in rets.index:
            continue
        prev_dt = rets.index[rets.index < dt]
        if not len(prev_dt):
            continue
        # Use signal from previous day (no lookahead)
        prev = prev_dt[-1]
        s1 = str(sig1.get(prev, "IEF"))
        s2 = str(sig2.get(prev, "SHY"))
        s3 = str(sig3.get(prev, "IEF"))
        r1 = float(rets.at[dt, s1]) if s1 in rets.columns else 0.0
        r2 = float(rets.at[dt, s2]) if s2 in rets.columns else 0.0
        r3 = float(rets.at[dt, s3]) if s3 in rets.columns else 0.0
        port_daily.at[dt] = (r1 + r2 + r3) / 3.0

    cap = float(capital)
    eq = cap * (1.0 + port_daily).cumprod()
    n = len(port_daily)
    years = n / 252.0
    end_eq = float(eq.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 = float((eq / eq.cummax() - 1.0).min())
    sd = float(port_daily.std(ddof=1))
    sharpe = float(port_daily.mean() / sd * math.sqrt(252)) if sd > 1e-12 else float("nan")

    spy = _download(["SPY"], fetch_start, fetch_end)["SPY"].pct_change().reindex(idx).fillna(0.0)
    rho_spy = float(pd.DataFrame({"p": port_daily, "spy": spy}).dropna().corr().iloc[0, 1])

    yr_rows = []
    eq_cur = cap
    for yr, g in port_daily.groupby(port_daily.index.year):
        ret_y = float((1 + g).prod() - 1) * 100
        end_y = eq_cur * (1 + ret_y / 100)
        yr_rows.append({"year": int(yr), "return_pct": round(ret_y, 2), "pnl_usd": round(end_y - eq_cur, 0)})
        eq_cur = end_y
    yr_df = pd.DataFrame(yr_rows)

    # Sub-strategy position diagnostics
    pos_counts: dict[str, dict] = {}
    for sub, sig in [("carry", sig1), ("trend", sig2), ("inflation", sig3)]:
        vals = sig.reindex(idx).value_counts()
        pos_counts[sub] = {str(k): int(v) for k, v in vals.items()}

    daily_df = pd.DataFrame({
        "date":          port_daily.index.strftime("%Y-%m-%d"),
        "daily_ret":     port_daily.values,
        "daily_pnl_usd": (port_daily * cap).values,
        "equity_usd":    eq.values,
    })

    out_prefix = Path(out_prefix)
    out_prefix.parent.mkdir(parents=True, exist_ok=True)
    daily_df.to_csv(f"{out_prefix}_daily.csv", index=False)
    yr_df.to_csv(f"{out_prefix}_yearly.csv", index=False)

    meta = {
        "strategy": "rates_carry",
        "sub_strategies": ["yield_curve_carry", "bond_trend_sma200", "inflation_regime_dbc"],
        "start": str(idx.min().date()),
        "end": str(idx.max().date()),
        "n_sessions": n,
        "capital": cap,
        "ending_equity_usd": round(end_eq, 2),
        "total_return_pct": round(total_ret * 100, 4),
        "cagr_pct": round(cagr * 100, 4),
        "sharpe": round(sharpe, 4),
        "max_dd_pct": round(dd * 100, 4),
        "rho_spy": round(rho_spy, 4),
        "position_counts": pos_counts,
        "daily_csv": f"{out_prefix}_daily.csv",
        "yearly_csv": f"{out_prefix}_yearly.csv",
    }
    with open(f"{out_prefix}_meta.json", "w") as fh:
        json.dump(meta, fh, indent=2)

    if verbose:
        print("=== Rates Carry Sleeve ===")
        print(f"Window: {meta['start']} → {meta['end']}  ({n} sessions)")
        print(f"Return {total_ret*100:.1f}%  CAGR {cagr*100:.1f}%  Sharpe {sharpe:.2f}  MaxDD {dd*100:.1f}%  ρ(SPY) {rho_spy:.2f}")
        print("\nYearly returns:")
        print(yr_df.to_string(index=False))
        print("\nSub-strategy position breakdown (sessions in each ETF):")
        for sub, counts in pos_counts.items():
            print(f"  {sub}: {counts}")

    return meta


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--start", default="2011-01-03")
    ap.add_argument("--end", default="2025-12-31")
    ap.add_argument("--capital", type=float, default=100_000.0)
    ap.add_argument("--sma-window", type=int, default=200)
    ap.add_argument("--out-prefix", type=Path, default=LOGS / "rates_carry_standard")
    args = ap.parse_args()

    run_rates_carry(
        start=args.start,
        end=args.end,
        capital=float(args.capital),
        out_prefix=args.out_prefix,
        sma_window=args.sma_window,
        verbose=True,
    )


if __name__ == "__main__":
    main()
