"""OPT04 — Iron Condor Weekly (DVOL-gated).

IDEA: Sell OTM call+put spreads weekly, collect premium from both sides. Iron condor =
  - Sell OTM put  (delta ~-0.20), Buy further OTM put  (delta ~-0.08)  <- put credit spread
  - Sell OTM call (delta ~+0.20), Buy further OTM call (delta ~+0.08)  <- call credit spread

Premium collected from BOTH sides. Profit if underlying stays within the wings (range-bound week).
Max loss = wing width - net premium (total of both spreads).

MODELED APPROACH:
  - DVOL used as ATM vol proxy (symmetric BS, no skew).
  - Gate: IV-rank > 0.30 (sell only when IV is rich relative to own history).
  - Optional crash-skip: IV-rank > 0.90 -> vol already exploded, skip.
  - Capital = put wing width + call wing width (total defined risk).
  - Fee: 12.5% of net premium (Deribit options cap, per side; 4 legs total = 2 round-trips).

GRID (4 configs on 1d TF):
  A) delta ±0.20/±0.08, ivr_gate=0.30, no crash-skip
  B) delta ±0.25/±0.10, ivr_gate=0.30, no crash-skip
  C) delta ±0.20/±0.08, ivr_gate=0.30, crash-skip at 0.90
  D) delta ±0.25/±0.10, ivr_gate=0.30, crash-skip at 0.90

CAVEAT: premiums MODELED on DVOL ATM (no skew, no real chain). Lead quantification only.
        DVOL history starts 2021-03 -> effective backtest from ~2021-Q3.
"""
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, sig: float) -> float:
    """Black-Scholes put price, r=0."""
    if T <= 0 or sig <= 0:
        return max(K - S, 0.0)
    d1 = (np.log(S / K) + 0.5 * sig**2 * T) / (sig * np.sqrt(T))
    d2 = d1 - sig * np.sqrt(T)
    return K * norm.cdf(-d2) - S * norm.cdf(-d1)


def bs_call(S: float, K: float, T: float, sig: float) -> float:
    """Black-Scholes call price, r=0."""
    if T <= 0 or sig <= 0:
        return max(S - K, 0.0)
    d1 = (np.log(S / K) + 0.5 * sig**2 * T) / (sig * np.sqrt(T))
    d2 = d1 - sig * np.sqrt(T)
    return S * norm.cdf(d1) - K * norm.cdf(d2)


def strike_put_from_delta(S: float, T: float, sig: float, delta: float) -> float:
    """Strike for a put with given delta (delta < 0, e.g. -0.20).
    put_delta = -N(-d1) = delta  ->  N(-d1) = -delta  ->  -d1 = N^{-1}(-delta)
    d1 = -N^{-1}(-delta)
    K = S * exp(0.5*sig^2*T - d1*sig*sqrt(T))."""
    d1 = -norm.ppf(-delta)
    return S * np.exp(0.5 * sig**2 * T - d1 * sig * np.sqrt(T))


def strike_call_from_delta(S: float, T: float, sig: float, delta: float) -> float:
    """Strike for a call with given delta (delta > 0, e.g. +0.20).
    call_delta = N(d1) = delta  ->  d1 = N^{-1}(delta)
    K = S * exp(-d1*sig*sqrt(T) + 0.5*sig^2*T)."""
    d1 = norm.ppf(delta)
    return S * np.exp(-d1 * sig * np.sqrt(T) + 0.5 * sig**2 * T)


# ─── IV-rank (causal, expanding window) ──────────────────────────────────────

def iv_rank_series(dv_pts: np.ndarray, min_history: int = 60) -> np.ndarray:
    """Causal expanding-window IV rank: fraction of past DVOL values below current.
    NaN until min_history valid bars are available."""
    n = len(dv_pts)
    ivr = np.full(n, np.nan)
    valid = np.where(np.isfinite(dv_pts))[0]
    if len(valid) < min_history:
        return ivr
    start = valid[0]
    for i in valid:
        hist_len = i - start
        if hist_len >= min_history:
            hist = dv_pts[start:i]
            hist = hist[np.isfinite(hist)]
            if len(hist) >= min_history:
                ivr[i] = float((hist < dv_pts[i]).mean())
    return ivr


# ─── Standalone iron condor backtest ─────────────────────────────────────────

def backtest_ic(
    df: pd.DataFrame,
    asset: str,
    short_delta_put: float = -0.20,
    long_delta_put: float  = -0.08,
    short_delta_call: float = 0.20,
    long_delta_call: float  = 0.08,
    ivr_gate: float        = 0.30,
    crash_skip: float      = 1.01,   # >1 disables crash-skip
    tenor_d: int           = 7,
    fee_side: float        = al.FEE_SIDE,
) -> dict:
    """Honest backtest of weekly iron condor on daily bars.

    P&L mechanics:
      - Every tenor_d bars (weekly) decide at bar i (close[i] known), settle at i+tenor_d.
      - Net premium = put_net + call_net (both modeled with BS on DVOL, no skew).
      - Payoff realized on close[i+tenor_d].
      - Capital basis = put_wing + call_wing (total defined risk).
      - Return_week = (net_premium - payoffs - fee) / capital.
      - Booked at settlement bar; 0 elsewhere.

    Returns al.eval_weights-compatible dict.
    """
    close = df["close"].values.astype(float)
    dts   = pd.to_datetime(df["datetime"], utc=True)
    n     = len(close)
    T_yr  = tenor_d / 365.25

    dv_pts = al.dvol(df, asset)
    dv     = dv_pts / 100.0
    ivr    = iv_rank_series(dv_pts, min_history=60)

    daily_pnl = np.zeros(n)
    in_trade  = np.zeros(n, dtype=bool)

    # Start from first bar where we have at least 60 bars of DVOL history
    valid_dvol = np.where(np.isfinite(dv_pts))[0]
    if len(valid_dvol) < 60:
        return _empty_result(df, dts)

    i_start = valid_dvol[60]  # first bar with 60 history points
    i = i_start

    trades = 0
    while i + tenor_d < n:
        S0  = close[i]
        sig = dv[i]

        # DVOL must be available
        if not np.isfinite(sig) or sig <= 0.0:
            i += tenor_d
            continue

        # IV-rank must be available
        if not np.isfinite(ivr[i]):
            i += tenor_d
            continue

        # Gate: sell only when IV rank above threshold
        if ivr_gate > 0.0 and ivr[i] < ivr_gate:
            i += tenor_d
            continue

        # Crash-skip: do not sell when vol already exploded
        if crash_skip < 1.0 and ivr[i] > crash_skip:
            i += tenor_d
            continue

        # ── PUT credit spread ──────────────────────────────────────────────
        Ks_put = strike_put_from_delta(S0, T_yr, sig, short_delta_put)  # short (less OTM)
        Kl_put = strike_put_from_delta(S0, T_yr, sig, long_delta_put)   # long  (more OTM)
        prem_s_put = bs_put(S0, Ks_put, T_yr, sig)
        prem_l_put = bs_put(S0, Kl_put, T_yr, sig)
        net_put    = prem_s_put - prem_l_put
        wing_put   = Ks_put - Kl_put       # put short strike > long strike -> positive

        # ── CALL credit spread ─────────────────────────────────────────────
        Ks_call = strike_call_from_delta(S0, T_yr, sig, short_delta_call)  # short (less OTM)
        Kl_call = strike_call_from_delta(S0, T_yr, sig, long_delta_call)   # long  (more OTM)
        prem_s_call = bs_call(S0, Ks_call, T_yr, sig)
        prem_l_call = bs_call(S0, Kl_call, T_yr, sig)
        net_call    = prem_s_call - prem_l_call
        wing_call   = Kl_call - Ks_call    # call long strike > short strike -> positive

        # Sanity: net premiums must be positive (should always be true by construction)
        if net_put <= 0.0 or net_call <= 0.0 or wing_put <= 0.0 or wing_call <= 0.0:
            i += tenor_d
            continue

        S1 = close[i + tenor_d]

        # ── PUT spread payoff ──────────────────────────────────────────────
        payoff_put  = max(0.0, Ks_put - S1) - max(0.0, Kl_put - S1)

        # ── CALL spread payoff ─────────────────────────────────────────────
        payoff_call = max(0.0, S1 - Ks_call) - max(0.0, S1 - Kl_call)

        # ── Net P&L ────────────────────────────────────────────────────────
        gross_pnl  = (net_put - payoff_put) + (net_call - payoff_call)

        # Capital basis: total defined risk (both wings)
        cap = wing_put + wing_call

        # Deribit options fee: 0.03% of notional per leg, cap 12.5% of premium.
        # 4 legs total for an iron condor. Conservative: cap 12.5% of gross net premium.
        FEE_FRAC = 0.125
        fee_cost = FEE_FRAC * (net_put + net_call)

        ret_week = (gross_pnl - fee_cost) / cap

        # Book at settlement bar
        settle = i + tenor_d
        daily_pnl[settle] += ret_week
        in_trade[i:settle] = True
        trades += 1

        i += tenor_d

    idx  = pd.DatetimeIndex(dts)
    net  = daily_pnl
    full = al._metrics_from_net(net, idx)
    hmask = idx >= al.HOLDOUT
    hold  = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 3 else dict(sharpe=0.0, n=0)
    bpy_d = al.bars_per_day(df) * 365.25

    return dict(
        full=full, holdout=hold, yearly=al._yearly(net, idx),
        time_in_market=round(float(np.mean(in_trade)), 3),
        turnover_per_year=round(float(trades * 2) / max(1, len(net) / bpy_d), 1),
        net=net, idx=idx,
    )


def _empty_result(df, dts):
    idx = pd.DatetimeIndex(pd.to_datetime(dts, utc=True))
    net = np.zeros(len(df))
    return dict(
        full=al._metrics_from_net(net, idx), holdout=dict(sharpe=0.0, n=0),
        yearly=al._yearly(net, idx), time_in_market=0.0, turnover_per_year=0.0,
        net=net, idx=idx,
    )


# ─── Config grid ──────────────────────────────────────────────────────────────

CONFIGS = [
    # (label,        sdp,   ldp,   ivr_gate, crash_skip)
    ("w20-08-ivr30",      -0.20, -0.08, 0.30, 1.01),   # wider wing, gate only
    ("w25-10-ivr30",      -0.25, -0.10, 0.30, 1.01),   # narrower wing, gate only
    ("w20-08-ivr30-cs90", -0.20, -0.08, 0.30, 0.90),   # wider + crash-skip
    ("w25-10-ivr30-cs90", -0.25, -0.10, 0.30, 0.90),   # narrower + crash-skip
]


def run_config(label, sdp, ldp, ivr_gate, cs, tf="1d") -> dict:
    name = f"OPT04-IC-{label}"
    per_asset = {}
    fee_ok_all = True

    for asset in ("BTC", "ETH"):
        df   = al.get(asset, tf)
        base = backtest_ic(df, asset,
                           short_delta_put=sdp, long_delta_put=ldp,
                           short_delta_call=-sdp, long_delta_call=-ldp,
                           ivr_gate=ivr_gate, crash_skip=cs)

        # Fee sweep: re-run with different fee fracs via fee_side proxy
        # (fee_side not directly used in our custom backtest; we scale FEE_FRAC)
        sweep = {}
        for f_side in al.FEE_SWEEP:
            # Map taker fee to options fee frac: baseline is 0.125 at f_side=FEE_SIDE=0.0005
            # Scale proportionally
            scale = f_side / al.FEE_SIDE if al.FEE_SIDE > 0 else 1.0
            fee_frac_scaled = 0.125 * scale

            # Recompute with scaled fee
            net_scaled = _recompute_net_scaled(df, asset, sdp, ldp, ivr_gate, cs, fee_frac_scaled)
            net_arr = net_scaled["net"]
            idx_arr = net_scaled["idx"]
            m = al._metrics_from_net(net_arr, idx_arr)
            sweep[f"{2*f_side*100:.2f}%RT"] = m["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 ("BTC", "ETH"))
    min_hold = min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in ("BTC", "ETH"))
    cells = [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 ("BTC", "ETH")]), 3),
        fee_survives=fee_ok_all,
    )]
    return dict(name=name, kind="weights", cells=cells, verdict=al._verdict(cells))


def _recompute_net_scaled(df, asset, sdp, ldp, ivr_gate, cs, fee_frac):
    """Recompute iron condor returns with a different fee fraction."""
    close  = df["close"].values.astype(float)
    dts    = pd.to_datetime(df["datetime"], utc=True)
    n      = len(close)
    T_yr   = 7 / 365.25

    dv_pts = al.dvol(df, asset)
    dv     = dv_pts / 100.0
    ivr    = iv_rank_series(dv_pts, min_history=60)

    daily_pnl = np.zeros(n)

    valid_dvol = np.where(np.isfinite(dv_pts))[0]
    if len(valid_dvol) < 60:
        return dict(net=daily_pnl, idx=pd.DatetimeIndex(pd.to_datetime(dts, utc=True)))

    i = valid_dvol[60]
    while i + 7 < n:
        S0 = close[i]; sig = dv[i]
        if not np.isfinite(sig) or sig <= 0:
            i += 7; continue
        if not np.isfinite(ivr[i]):
            i += 7; continue
        if ivr_gate > 0 and ivr[i] < ivr_gate:
            i += 7; continue
        if cs < 1.0 and ivr[i] > cs:
            i += 7; continue

        Ks_put  = strike_put_from_delta(S0, T_yr, sig, sdp)
        Kl_put  = strike_put_from_delta(S0, T_yr, sig, ldp)
        net_put = bs_put(S0, Ks_put, T_yr, sig) - bs_put(S0, Kl_put, T_yr, sig)
        wing_put = Ks_put - Kl_put

        Ks_call  = strike_call_from_delta(S0, T_yr, sig, -sdp)
        Kl_call  = strike_call_from_delta(S0, T_yr, sig, -ldp)
        net_call = bs_call(S0, Ks_call, T_yr, sig) - bs_call(S0, Kl_call, T_yr, sig)
        wing_call = Kl_call - Ks_call

        if net_put <= 0 or net_call <= 0 or wing_put <= 0 or wing_call <= 0:
            i += 7; continue

        S1 = close[i + 7]
        payoff_put  = max(0.0, Ks_put - S1) - max(0.0, Kl_put - S1)
        payoff_call = max(0.0, S1 - Ks_call) - max(0.0, S1 - Kl_call)

        gross = (net_put - payoff_put) + (net_call - payoff_call)
        fee   = fee_frac * (net_put + net_call)
        cap   = wing_put + wing_call

        daily_pnl[i + 7] += (gross - fee) / cap
        i += 7

    return dict(net=daily_pnl, idx=pd.DatetimeIndex(pd.to_datetime(dts, utc=True)))


# ─── Main ─────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("OPT04 — Iron Condor Weekly (DVOL-gated)")
    print("CAVEAT: premiums MODELED on DVOL ATM (no skew). Lead quantification only.")
    print("DVOL history starts 2021-03 -> effective backtest from ~2021-Q3.")
    print()

    results = []
    for label, sdp, ldp, ivr_gate, cs in CONFIGS:
        print(f"Running: {label}")
        rep = run_config(label, sdp, ldp, ivr_gate, cs, tf="1d")
        results.append(rep)
        print(al.fmt(rep))
        print()

    best = max(results, key=lambda r: max(
        (c["min_asset_holdout_sharpe"] for c in r["cells"]), default=-9.0))

    print("=" * 70)
    print("BEST CONFIG:", best["name"])
    print(al.fmt(best))
    print()
    print("JSON:", al.as_json(best))
