"""OPT06 — Ratio Put Spread (Defensive Short-Vol with Tail Hedge)

IDEA: Ratio put spread (1x2 put ratio) modeled on DVOL:
  - Sell 1 OTM put at strike K1 = S * exp(-delta1)  (e.g., -0.15 log-moneyness)
  - Buy  2 OTM puts at strike K2 = S * exp(-delta2)  (e.g., -0.30 log-moneyness)
  Net: collect premium from the short put, use proceeds to buy tail protection.
  This is a "defensive short-vol" structure:
    - Moderate down moves (to K2) → profitable (net premium + short put profit)
    - Crash moves (below K2) → protected (long 2 puts offset the short)
    - Up moves → lose net premium received (small cost)

  The ratio 1:2 means the structure has POSITIVE gamma below K2 (net long put delta
  when S < K2) — the tail hedge kicks in. Above K2 but below K1, it's short-gamma
  (collects theta). Above K1, it's short a single put (small risk).

GATE: Only enter when DVOL >= gate threshold (elevated IV → richer premium).
      Also gated on DVOL/RV ratio (only sell vol when IV > RV).

ROLL: Weekly (7d) or biweekly (14d).

GRID: 4 configs:
  (short_moneyness=-0.10, long_moneyness=-0.25, gate_dvol=50)
  (short_moneyness=-0.10, long_moneyness=-0.25, gate_dvol=60)
  (short_moneyness=-0.15, long_moneyness=-0.30, gate_dvol=50)
  (short_moneyness=-0.15, long_moneyness=-0.30, gate_dvol=60)
  → 4 configs × 1d TF = 4 backtests (within <=6 limit)

CAVEAT:
  - MODELED on DVOL (ATM). Real puts have skew (OTM puts cost more → less premium).
  - History starts 2021-03 (DVOL). Backtest from 2021-03 only.
  - Tail risk partially mitigated by the ratio structure, but skew model error matters.
  - Not for deployment without real options pricing data.
  - Lead-only / modeled.

Style: study_weights (continuous modeled position via P&L series).
"""

import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al

import numpy as np
import pandas as pd
from scipy.stats import norm


# ── Black-Scholes helpers ──────────────────────────────────────────────────
def bs_put(S: float, K: float, T: float, sigma: float) -> float:
    """Black-Scholes put price (r=0, crypto/futures)."""
    if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
        return max(0.0, K - S)
    d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return float(K * norm.cdf(-d2) - S * norm.cdf(-d1))


def bs_put_delta(S: float, K: float, T: float, sigma: float) -> float:
    """Black-Scholes put delta (negative)."""
    if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
        return -1.0 if S < K else 0.0
    d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
    return float(norm.cdf(d1) - 1.0)


def ratio_spread_value(S: float, K1: float, K2: float, T: float, sigma: float) -> float:
    """Value of short 1 put(K1) + long 2 puts(K2). Positive = we received cash."""
    # Short 1 put at K1 (we receive premium = +put_K1)
    # Long 2 puts at K2  (we pay premium   = -2*put_K2)
    # Net received = put(K1) - 2*put(K2)
    p1 = bs_put(S, K1, T, sigma)
    p2 = bs_put(S, K2, T, sigma)
    return p1 - 2.0 * p2


def ratio_spread_delta(S: float, K1: float, K2: float, T: float, sigma: float) -> float:
    """Net delta of position: short 1 put(K1) + long 2 puts(K2)."""
    d1 = bs_put_delta(S, K1, T, sigma)
    d2 = bs_put_delta(S, K2, T, sigma)
    return -d1 + 2.0 * d2


def ratio_spread_payoff(S_exp: float, K1: float, K2: float) -> float:
    """Payoff at expiry of short 1 put(K1) + long 2 puts(K2) (as fraction of S0)."""
    payoff_short = -max(0.0, K1 - S_exp)
    payoff_long = 2.0 * max(0.0, K2 - S_exp)
    return payoff_short + payoff_long


def simulate_ratio_spread_cycle(
    close: np.ndarray,
    sigma_iv: np.ndarray,
    i0: int,
    roll_bars: int,
    short_moneyness: float,   # log-moneyness of short put (e.g., -0.10 → 10% OTM)
    long_moneyness: float,    # log-moneyness of long puts (e.g., -0.25 → 25% OTM)
    fee_side: float = 0.001   # 0.10% per leg per side (options spread)
) -> tuple[float, int]:
    """
    Simulate one ratio put spread cycle.

    At entry i0:
      - K1 = S0 * exp(short_moneyness)  [e.g., S0 * exp(-0.10) ≈ S0 * 0.905]
      - K2 = S0 * exp(long_moneyness)   [e.g., S0 * exp(-0.25) ≈ S0 * 0.779]
      - Sell 1 put at K1, buy 2 puts at K2
      - Net premium received = put(K1) - 2*put(K2) [in $]

    At expiry i_exp:
      - P&L = net_premium_received + payoff_at_expiry - transaction_costs

    P&L per unit of notional S0 (fraction of S0):
      net_pnl = (p1_entry - 2*p2_entry)/S0
                + payoff(S_exp, K1, K2)/S0
                - (3 legs * 2 sides * fee_side)   [3 legs: 1 short + 2 long → 3 contracts]
    """
    n = len(close)
    S0 = close[i0]
    T = roll_bars / 365.25

    sig = sigma_iv[i0]
    if not (np.isfinite(sig) and sig > 0.02):
        return 0.0, min(i0 + roll_bars, n - 1)

    K1 = S0 * np.exp(short_moneyness)   # short put (less OTM)
    K2 = S0 * np.exp(long_moneyness)    # long puts (more OTM)

    # Net premium received at entry
    p1 = bs_put(S0, K1, T, sig)
    p2 = bs_put(S0, K2, T, sig)
    net_prem = p1 - 2.0 * p2  # positive → we received net premium

    i_exp = min(i0 + roll_bars, n - 1)
    S_exp = close[i_exp]

    # Payoff at expiry (from position payoff)
    payoff = ratio_spread_payoff(S_exp, K1, K2)

    # Transaction costs: 3 contracts (1 short + 2 long), entry + exit = 2 sides each
    # fee_side applies per contract per side
    tx_cost = 3 * 2 * fee_side * S0  # in $ terms

    net_pnl_dollar = net_prem + payoff - tx_cost
    net_pnl_frac = net_pnl_dollar / S0

    return float(net_pnl_frac), i_exp


def compute_ratio_spread_series(
    df: pd.DataFrame,
    asset: str,
    roll_days: int,
    short_moneyness: float,
    long_moneyness: float,
    gate_dvol: float,         # minimum DVOL level to enter (vol points, e.g., 50)
    iv_rv_gate: float = 1.05, # minimum IV/RV ratio to enter
    rv_win_days: int = 20,
    fee_side: float = 0.001
) -> np.ndarray:
    """
    Simulate the full ratio put spread strategy.
    Returns per-bar P&L as fraction of equity (additive).
    Flat when not in a cycle or gate not met.
    """
    close = df["close"].values.astype(float)
    n = len(close)
    sigma_iv = al.dvol(df, asset) / 100.0  # convert vol points → decimal

    log_r = al.log_returns(close)
    bpy = al.bars_per_year(df)
    rv_win = max(5, rv_win_days)
    rv_ann = pd.Series(log_r).rolling(rv_win, min_periods=max(2, rv_win // 2)).std().values * np.sqrt(bpy)

    # Find first bar with valid DVOL
    first_valid = np.where(np.isfinite(sigma_iv) & (sigma_iv > 0.02))[0]
    if len(first_valid) == 0:
        return np.zeros(n)
    start_bar = int(first_valid[0]) + rv_win  # also need RV to warm up

    r_opt = np.zeros(n)
    i = start_bar

    while i < n - 1:
        sig_iv = sigma_iv[i]
        sig_rv = rv_ann[i]
        dvol_pts = sig_iv * 100.0  # back to vol points for gate

        # Entry conditions:
        # 1. Valid DVOL
        # 2. DVOL >= gate_dvol (vol is elevated → richer premium)
        # 3. IV/RV >= iv_rv_gate (selling vol when IV > RV)
        if (np.isfinite(sig_iv) and sig_iv > 0.02 and
                np.isfinite(sig_rv) and sig_rv > 0.02 and
                dvol_pts >= gate_dvol and
                sig_iv / sig_rv >= iv_rv_gate):
            net_pnl, i_exp = simulate_ratio_spread_cycle(
                close, sigma_iv, i, roll_days,
                short_moneyness=short_moneyness,
                long_moneyness=long_moneyness,
                fee_side=fee_side
            )
            r_opt[i_exp] = net_pnl
            i = i_exp + 1
        else:
            i += 1

    return r_opt


def eval_ratio_spread(df: pd.DataFrame, r_opt: np.ndarray) -> dict:
    """Evaluate ratio put spread P&L series into standard metrics."""
    idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
    n = len(r_opt)

    # The transaction costs are already inside simulate_ratio_spread_cycle.
    # Just compound the net P&L.
    r_net = r_opt.copy()
    eq = np.cumprod(1.0 + np.clip(r_net, -0.99, None))
    eq = np.concatenate([[1.0], eq])
    r_eq = np.diff(eq) / eq[:-1]
    r_eq = np.nan_to_num(r_eq)

    bpy = al.bars_per_year(df)
    rr = r_eq[np.isfinite(r_eq)]
    sharpe = float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)) if np.std(rr) > 0 else 0.0
    pk = np.maximum.accumulate(eq[1:])
    dd = float(np.max((pk - eq[1:]) / pk)) if len(eq) > 1 else 0.0
    span_days = (idx[-1] - idx[0]).total_seconds() / 86400 if len(idx) > 1 else 1.0
    years = max(span_days / 365.25, 1e-6)
    total_ret = eq[-1] / eq[0] - 1
    cagr = (eq[-1] / eq[0]) ** (1 / years) - 1

    full = dict(sharpe=round(sharpe, 3), cagr=round(cagr, 4),
                maxdd=round(dd, 4), ret=round(total_ret, 4), n=int(n))

    hmask = idx >= al.HOLDOUT
    hold = dict(sharpe=0.0, ret=0.0, n=0)
    if hmask.sum() > 3:
        r_h = r_eq[hmask]
        hs = float(np.mean(r_h) / np.std(r_h) * np.sqrt(bpy)) if np.std(r_h) > 0 else 0.0
        eq_h = np.cumprod(1.0 + np.clip(r_h, -0.99, None))
        hold = dict(sharpe=round(hs, 3), ret=round(float(eq_h[-1] - 1), 4), n=int(hmask.sum()))

    s = pd.Series(r_eq, index=idx)
    yearly = {}
    for y, g in s.groupby(s.index.year):
        eq_y = np.cumprod(1 + g.values)
        pk_y = np.maximum.accumulate(eq_y)
        yearly[int(y)] = dict(ret=round(float(eq_y[-1] - 1), 4),
                               dd=round(float(np.max((pk_y - eq_y) / pk_y)), 4))

    settle_bars = (r_opt != 0).sum()
    turnover_per_year = round(float(settle_bars / (span_days / 365.25)), 1)

    return dict(full=full, holdout=hold, yearly=yearly,
                time_in_market=round(float(settle_bars / n), 3),
                turnover_per_year=turnover_per_year)


def run_ratio_spread(
    short_moneyness: float,
    long_moneyness: float,
    gate_dvol: float,
    roll_days: int = 7,
    tfs=("1d",)
) -> dict:
    """Run ratio put spread study for one parameter config."""
    name = (f"OPT06-RatioPutSpread-short{abs(short_moneyness)*100:.0f}pct"
            f"-long{abs(long_moneyness)*100:.0f}pct-dvol{gate_dvol:.0f}")
    cells = []
    for tf in tfs:
        per_asset = {}
        fee_ok_all = True
        for asset in al.CERTIFIED:
            df = al.get(asset, tf)
            r_opt = compute_ratio_spread_series(
                df, asset,
                roll_days=roll_days,
                short_moneyness=short_moneyness,
                long_moneyness=long_moneyness,
                gate_dvol=gate_dvol
            )
            base = eval_ratio_spread(df, r_opt)

            # Fee sweep: scale the option tx cost
            # Base fee_side=0.001; sweep by adjusting the per-cycle cost
            sweep = {}
            for f_side in al.FEE_SWEEP:
                r_sweep = compute_ratio_spread_series(
                    df, asset,
                    roll_days=roll_days,
                    short_moneyness=short_moneyness,
                    long_moneyness=long_moneyness,
                    gate_dvol=gate_dvol,
                    fee_side=f_side
                )
                sw = eval_ratio_spread(df, r_sweep)
                # Key: 0.20%RT = 0.0010/side = what we label
                sweep[f"{2*f_side*100:.2f}%RT"] = sw["full"]["sharpe"]

            fee_ok = sweep.get("0.20%RT", -9) > 0
            fee_ok_all = fee_ok_all and fee_ok
            per_asset[asset] = dict(full=base["full"], holdout=base["holdout"],
                                    tim=base["time_in_market"],
                                    turnover=base["turnover_per_year"],
                                    fee_sweep=sweep, yearly=base["yearly"])

        min_full = min(per_asset[a]["full"]["sharpe"] for a in al.CERTIFIED)
        min_hold = min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in al.CERTIFIED)
        cells.append(dict(tf=tf, per_asset=per_asset,
                          min_asset_full_sharpe=round(min_full, 3),
                          min_asset_holdout_sharpe=round(min_hold, 3),
                          full_sharpe=round(np.mean([per_asset[a]["full"]["sharpe"]
                                                     for a in al.CERTIFIED]), 3),
                          fee_survives=fee_ok_all))

    verdict = al._verdict(cells)
    return dict(name=name, kind="weights", cells=cells, verdict=verdict)


if __name__ == "__main__":
    print("OPT06 — Ratio Put Spread (Defensive Short-Vol with Tail Hedge)")
    print("CAVEAT: MODELED on DVOL ATM. Skew not modeled → OTM puts underpriced in model.")
    print("DVOL starts 2021-03 → backtest from 2021-03 only.")
    print("Lead-only / modeled. Not for deployment.")
    print()

    # Grid: 4 configs
    # (short_moneyness, long_moneyness, gate_dvol)
    CONFIGS = [
        (-0.10, -0.25, 50.0),   # 10%/25% OTM, gate DVOL>=50
        (-0.10, -0.25, 60.0),   # 10%/25% OTM, gate DVOL>=60
        (-0.15, -0.30, 50.0),   # 15%/30% OTM, gate DVOL>=50
        (-0.15, -0.30, 60.0),   # 15%/30% OTM, gate DVOL>=60
    ]

    best_rep = None
    best_score = -999.0

    for short_m, long_m, gate_d in CONFIGS:
        print(f"--- short={short_m*100:.0f}%, long={long_m*100:.0f}%, gate_dvol={gate_d} ---")
        rep = run_ratio_spread(
            short_moneyness=short_m,
            long_moneyness=long_m,
            gate_dvol=gate_d,
            roll_days=7,
            tfs=("1d",)
        )
        print(al.fmt(rep))
        score = rep["verdict"].get("best_holdout_sharpe", -9)
        if score > best_score:
            best_score = score
            best_rep = rep
        print()

    print("=" * 60)
    print("BEST CONFIG:")
    print(al.fmt(best_rep))
    print()
    print("JSON:", al.as_json(best_rep))
