"""OPT07 — Collar Overlay
IDEA: Long spot + buy protective put + sell covered call (zero-ish cost collar).
  - Long 1 unit spot BTC/ETH
  - Sell OTM call at strike K_call = S * exp(+call_otm * sigma * sqrt(T))
  - Buy OTM put at strike K_put  = S * exp(-put_otm * sigma * sqrt(T))
  Net premium ≈ call premium received - put premium paid (can be near-zero or small debit/credit
  depending on the strikes chosen).

Goal: reduce drawdown vs buy&hold by capping upside (call) and flooring downside (put).
      Does this improve risk-adjusted return (Sharpe)?

Hypothesis: the vol risk premium means we receive more on the call than we pay for the put
(IV > RV historically), so the collar should produce a positive carry vs buying naked insurance.
In a crash the put activates and limits losses. Net effect should be improved Sharpe.

MODELED: premiums computed via Black-Scholes with DVOL as IV (no skew, no slippage on options).
DVOL history starts 2021-03 -> backtest from 2021-03 only.
CAVEAT: modeled, lead-only.

Grid (4 configs, 1 TF = 4 study_weights calls -> <=8 total backtests):
  1. Symmetric collar: call OTM=0.10, put OTM=0.10 (weekly)
  2. Tighter collar:   call OTM=0.05, put OTM=0.05 (weekly)
  3. Asymmetric:       call OTM=0.05, put OTM=0.10 (debit collar, more protection, less upside cap)
  4. Asymmetric:       call OTM=0.10, put OTM=0.05 (credit collar, less protection, more upside cap)

Style: study_weights (continuous position ~1x long + option overlay adjustments at settlement).
"""

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 call and put prices ────────────────────────────────────────
def bs_call(S: float, K: float, T: float, sigma: float, r: float = 0.0) -> float:
    """Black-Scholes call price. T in years. sigma annualized."""
    if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
        return 0.0
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return float(S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2))


def bs_put(S: float, K: float, T: float, sigma: float, r: float = 0.0) -> float:
    """Black-Scholes put price via put-call parity."""
    c = bs_call(S, K, T, sigma, r)
    return float(c - S + K * np.exp(-r * T))


# ── Collar P&L per settlement cycle ──────────────────────────────────────────
def collar_cycle_return(S_start: float, S_end: float,
                        K_call: float, K_put: float,
                        call_prem: float, put_cost: float) -> float:
    """
    Compute the net return of a collar for one option cycle.

    At initiation:
      - Receive call_prem (sell call)
      - Pay put_cost (buy put)
      Net option carry = call_prem - put_cost (per unit of spot, as fraction of S_start)

    At settlement:
      Spot P&L: S_end / S_start - 1
      Call settled: -max(0, S_end - K_call) / S_start   (we're short call)
      Put settled: +max(0, K_put - S_end) / S_start      (we're long put)

    Total: (S_end/S_start - 1)
           - max(0, S_end - K_call) / S_start
           + max(0, K_put - S_end) / S_start
           + (call_prem - put_cost) / S_start

    Which simplifies to the textbook collar:
      If S_end >= K_call: net = (K_call/S_start - 1) + carry   (upside capped)
      If S_end <= K_put:  net = (K_put/S_start - 1) + carry    (downside floored)
      Otherwise:          net = (S_end/S_start - 1) + carry
    """
    carry = (call_prem - put_cost) / S_start  # net option premium (positive = net credit)

    if S_end >= K_call:
        return (K_call / S_start - 1.0) + carry
    elif S_end <= K_put:
        return (K_put / S_start - 1.0) + carry
    else:
        return (S_end / S_start - 1.0) + carry


# ── Build collar target array ─────────────────────────────────────────────────
def build_collar_target(close: np.ndarray, sigma_ann: np.ndarray,
                        call_otm: float, put_otm: float,
                        roll_bars: int, T_years: float) -> np.ndarray:
    """
    Build a synthetic 'effective position' array for the collar strategy.

    At each bar i, target[i] is held during bar i+1.
    On settlement bars: effective position encodes the full cycle's collar P&L.
    On non-settlement bars (mid-cycle): position = 1.0 (pure spot, no adjustment yet).

    Settlement bar technique (same as OPT01):
      target[i-1] * r_spot[i] ≈ cc_return for the cycle
      For multi-bar cycles: option_adj = collar_r - cycle_spot_r is applied at settlement.
    """
    n = len(close)
    target = np.ones(n)  # default: long spot

    # Find first bar with valid DVOL
    first_valid = np.where(np.isfinite(sigma_ann) & (sigma_ann > 0))[0]
    if len(first_valid) == 0:
        return target
    start_bar = int(first_valid[0])

    r_spot = al.simple_returns(close)

    # Initialize first collar at start_bar
    S0 = close[start_bar]
    sig0 = sigma_ann[start_bar]

    option_K_call = None
    option_K_put = None
    call_prem = 0.0
    put_cost = 0.0
    cycle_start_bar = start_bar
    cycle_start_price = S0

    if sig0 > 0 and np.isfinite(sig0):
        K_call = S0 * np.exp(call_otm * sig0 * np.sqrt(T_years))
        K_put  = S0 * np.exp(-put_otm * sig0 * np.sqrt(T_years))
        option_K_call = K_call
        option_K_put  = K_put
        call_prem = bs_call(S0, K_call, T_years, sig0)
        put_cost  = bs_put(S0, K_put,  T_years, sig0)

    for i in range(start_bar + 1, n):
        bars_in_cycle = i - cycle_start_bar

        if option_K_call is None or option_K_put is None:
            # No active collar -> pure spot
            target[i - 1] = 1.0
            # Try to re-initialize
            sig_i = sigma_ann[i]
            if np.isfinite(sig_i) and sig_i > 0:
                S_i = close[i]
                K_call = S_i * np.exp(call_otm * sig_i * np.sqrt(T_years))
                K_put  = S_i * np.exp(-put_otm * sig_i * np.sqrt(T_years))
                option_K_call = K_call
                option_K_put  = K_put
                call_prem = bs_call(S_i, K_call, T_years, sig_i)
                put_cost  = bs_put(S_i, K_put,  T_years, sig_i)
                cycle_start_bar = i
                cycle_start_price = S_i
            continue

        if bars_in_cycle >= roll_bars:
            # Settlement bar: compute collar payoff for the full cycle
            S_end   = close[i]
            S_start = cycle_start_price

            collar_r = collar_cycle_return(
                S_start, S_end,
                option_K_call, option_K_put,
                call_prem, put_cost
            )
            cycle_spot_r = S_end / S_start - 1.0

            # Encode the option adjustment on the settlement bar
            r_i = r_spot[i]
            option_adj = collar_r - cycle_spot_r  # premium carry ± cap/floor adjustments

            if abs(r_i) > 1e-10:
                target[i - 1] = 1.0 + option_adj / r_i
            else:
                # r_spot[i] ≈ 0: no spot movement on settlement bar -> just carry position=1
                # (option_adj can't be embedded cleanly, but it's typically small)
                target[i - 1] = 1.0

            # Roll new collar
            sig_new = sigma_ann[i]
            if np.isfinite(sig_new) and sig_new > 0:
                K_call_new = S_end * np.exp(call_otm * sig_new * np.sqrt(T_years))
                K_put_new  = S_end * np.exp(-put_otm * sig_new * np.sqrt(T_years))
                option_K_call = K_call_new
                option_K_put  = K_put_new
                call_prem = bs_call(S_end, K_call_new, T_years, sig_new)
                put_cost  = bs_put(S_end, K_put_new,  T_years, sig_new)
            else:
                option_K_call = None
                option_K_put  = None
                call_prem = 0.0
                put_cost  = 0.0

            cycle_start_bar = i
            cycle_start_price = S_end
        else:
            # Mid-cycle: hold spot (position=1, no adjustment)
            target[i - 1] = 1.0

    target = np.nan_to_num(target, nan=1.0)
    # Clip extreme values (guard against division artifacts when r_spot ≈ 0)
    target = np.clip(target, -5.0, 5.0)
    return target


# ── Per-asset runner (wraps study_weights) ────────────────────────────────────
def run_collar(call_otm: float, put_otm: float, roll_days: int = 7,
               tfs: tuple = ("1d",)) -> dict:
    """Run collar study for one config. Returns report dict."""
    name = f"OPT07-COLLAR-C{int(call_otm*100)}P{int(put_otm*100)}-roll{roll_days}d"
    T_years = roll_days / 365.25

    cells = []
    for tf in tfs:
        per_asset = {}
        fee_ok_all = True
        for asset in al.CERTIFIED:
            df = al.get(asset, tf)
            sigma_ann = al.dvol(df, asset) / 100.0
            roll_bars = roll_days  # 1d tf: 1 bar = 1 day

            tgt = build_collar_target(
                df["close"].values.astype(float),
                sigma_ann,
                call_otm=call_otm,
                put_otm=put_otm,
                roll_bars=roll_bars,
                T_years=T_years
            )

            base = al.eval_weights(df, tgt, fee_side=al.FEE_SIDE)
            sweep = {
                f"{2*f*100:.2f}%RT": al.eval_weights(df, tgt, fee_side=f)["full"]["sharpe"]
                for f in al.FEE_SWEEP
            }
            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(float(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)


# ── Main: small grid ──────────────────────────────────────────────────────────
if __name__ == "__main__":
    # Grid: 4 configs x 1 TF = 4 study calls = 8 total asset backtests (fine for 2 CPUs)
    CONFIGS = [
        # (call_otm, put_otm, roll_days, description)
        (0.10, 0.10, 7,  "symmetric 10%/10% weekly"),
        (0.05, 0.05, 7,  "tight 5%/5% weekly"),
        (0.05, 0.10, 7,  "debit collar: call 5% / put 10% -> more downside protection"),
        (0.10, 0.05, 7,  "credit collar: call 10% / put 5% -> less protection, net credit"),
    ]

    print("OPT07 Collar Overlay — MODELED on DVOL (lead-only, from 2021-03)")
    print("Long spot + sell OTM call + buy OTM put (zero-ish cost collar)")
    print()

    best_rep = None
    best_score = -999.0

    for call_otm, put_otm, roll_days, desc in CONFIGS:
        print(f"--- {desc} (call_otm={call_otm}, put_otm={put_otm}, roll={roll_days}d) ---")
        rep = run_collar(call_otm=call_otm, put_otm=put_otm, roll_days=roll_days, 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))
