Files
Adriano Dal Pastro 5ac4e16af8 research(alt): sweep 104 strategie alternative su Deribit (153 agenti) + marginal scorer
Ondata di ricerca onesta a largo spettro su BTC/ETH+DVOL certificati: 104 ipotesi
distinte (11 famiglie), un agente-finder per ipotesi, verifica avversariale a 3
scettici sui promettenti, sintesi (153 agenti totali). Esito: NIENTE di nuovo regge
-> conferma del soffitto strutturale ~1.3 BTC/ETH-direzionale; lo stack
TP01+XS01+VRP01 resta imbattuto.

- altlib.py: harness condiviso vettoriale leak-free (eval_weights/study_weights,
  fee-sweep, both-asset + hold-out 2025+). Riproduce i numeri canonici di TP01.
- MARGINAL SCORER (study_marginal/marginal_vs_tp01): Sharpe INCREMENTALE vs baseline
  TP01 (corr, blend uplift OOS, alpha residua) + jackknife OOS (clean-year +
  drop-best-month). earns_slot = abs!=FAIL & ADDS & robust_oos. Smaschera gli overlay
  su TSMOM con PASS assoluti fasulli (CMB04, VOL11, ...) e il falso positivo KAMA
  (ADDS ma muore al jackknife).
- runs/*.py (104) script riproducibili per ipotesi; wf_altstrat.js workflow.
- Verdetto: 0 candidati deployabili; 2 LEAD fragili (VOL08, STA05_LS) da forward-monitor.
- test_marginal_scorer.py blocca baseline + invarianti. Suite: 32 verde.

Diario: docs/diary/2026-06-20-alt-strategies-100agent-sweep.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:50:39 +00:00

451 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OPT05 — Delta-Hedged Short Straddle (Variance Premium Harvest)
IDEA: Sell ATM straddle every N days, delta-hedge daily with ACTUAL price moves.
Net P&L = IV-RV spread (the variance risk premium).
HONEST APPROACH — Direct P&L Simulation (avoids BS gamma approximation errors):
1. At roll date i0: sell ATM straddle. Receive premium P = 2*BSCall(S0,S0,T,IV).
2. Compute initial delta hedge: delta_straddle = delta_call + delta_put = N(d1) - N(-d1) ≈ 0 ATM.
Set delta_hedge_position h0 = -delta_straddle ≈ 0 at initiation.
3. Each subsequent bar k: compute new delta at current S_k, T_remaining.
Rebalance: dh = new_delta - old_delta. Hedge cost includes:
(a) Slippage/market-impact on spot hedge: dh * S_k * fee_hedge (spot fee per side)
(b) The actual mark-to-market P&L of the short straddle:
delta_PnL = -(C(S_k, K, T_k) + P(S_k, K, T_k) - C(S_{k-1}, K, T_{k-1}) - P(S_{k-1}, K, T_{k-1}))
plus hedge_PnL = h * (S_k - S_{k-1})
4. At expiry: close position at intrinsic value.
Total cycle P&L = option_premium - (intrinsic_at_expiry + sum_of_theta_adj + hedge_slippage)
This simulation directly uses ACTUAL price moves, so:
- Big moves (jumps) correctly cause large losses
- Small/quiet periods correctly generate theta income
- Discrete rebalancing frequency exactly matches daily bars
KEY METRICS EXPECTED:
- Crypto IV ≈ 60-80%, RV ≈ 40-65%: IV>RV on average → net positive
- But crypto has fat tails: occasional -10%/-20% single-day moves devastate short gamma
- Expected Sharpe: 0.30.8 if honestly modeled (not 4.0)
GATE: Only enter when DVOL/RV_20d >= gate threshold (IV-rich condition).
GRID: roll_days in {7, 14} x iv_rv_gate in {1.10, 1.20} → 4 configs, 1d TF only.
CAVEAT:
- MODELED on DVOL ATM. Skew not modeled (OTM puts have higher IV in practice).
- Straddle sell assumes fills at mid; real execution has bid-ask spread.
- Tail risk (e.g., BTC -30% day) not captured via DVOL history smoothing.
- DVOL history starts 2021-03 → backtest from 2021-03 only.
- Lead-only; not for deployment without real options data.
Style: study_weights (continuous modeled position evaluated via standalone 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_price(S: float, K: float, T: float, sigma: float, option_type: str = "call") -> float:
"""Black-Scholes option price. r=0 (crypto/futures context)."""
if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
# Intrinsic value
if option_type == "call":
return max(0.0, S - K)
else:
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)
if option_type == "call":
return float(S * norm.cdf(d1) - K * norm.cdf(d2))
else:
return float(K * norm.cdf(-d2) - S * norm.cdf(-d1))
def bs_delta(S: float, K: float, T: float, sigma: float, option_type: str = "call") -> float:
"""Black-Scholes delta."""
if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
if option_type == "call":
return 1.0 if S > K else 0.0
else:
return -1.0 if S < K else 0.0
d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
if option_type == "call":
return float(norm.cdf(d1))
else:
return float(norm.cdf(d1) - 1.0)
def straddle_value(S: float, K: float, T: float, sigma: float) -> float:
"""ATM straddle value = call + put."""
return bs_price(S, K, T, sigma, "call") + bs_price(S, K, T, sigma, "put")
def straddle_delta(S: float, K: float, T: float, sigma: float) -> float:
"""Net delta of short straddle: call_delta + put_delta."""
return bs_delta(S, K, T, sigma, "call") + bs_delta(S, K, T, sigma, "put")
def simulate_straddle_cycle(
close: np.ndarray,
sigma_iv: np.ndarray,
i0: int,
roll_bars: int,
fee_hedge: float = 0.0005 # spot hedge rebalance cost (0.05% per side taker)
) -> tuple[float, int]:
"""
Simulate ONE delta-hedged short straddle cycle starting at bar i0.
Returns (net_pnl_fraction_of_K, i_expiry) where:
- net_pnl is in fraction of strike K (= S0 at entry)
- i_expiry is the bar at which the cycle ends
P&L components (all as fraction of K):
+ straddle_premium/K received at i0 (short straddle → receive premium)
- mark-to-market change of straddle value (we're short)
+ hedge P&L from spot hedge position
- hedge rebalancing cost (fee per trade)
"""
n = len(close)
S0 = close[i0]
K = S0 # sell ATM
T0 = roll_bars / 365.25 # time to expiry in years
sig0 = sigma_iv[i0]
if not (np.isfinite(sig0) and sig0 > 0.01):
return 0.0, min(i0 + roll_bars, n - 1)
# Sell straddle at i0: receive premium
prem0 = straddle_value(S0, K, T0, sig0)
# Position: short straddle (we want straddle to decrease in value)
# Short straddle value at entry = prem0
# Initial delta hedge (fractional units of underlying per unit K)
delta0 = straddle_delta(S0, K, T0, sig0) # ≈ 0 at ATM
# Hedge: buy delta0 units of spot to hedge (position in spot = delta0 * K)
# But we're SHORT the straddle, so our delta is +delta_straddle, we need to sell spot
# Short straddle delta = -(call_delta + put_delta)
# We go long (-straddle_delta) in spot to be delta-neutral
hedge_pos = -delta0 # units of S per unit of notional (S0)
# Running P&L tracking
total_pnl = prem0 # we received this upfront (in $ terms, / K at end)
# straddle_prev_value = prem0 # track mark-to-market
prev_S = S0
prev_sig = sig0
prev_hedge = hedge_pos
i_expiry = min(i0 + roll_bars, n - 1)
total_hedge_cost = 0.0
for i in range(i0 + 1, i_expiry + 1):
S_curr = close[i]
bars_to_exp = i_expiry - i
T_rem = max(0.0, bars_to_exp / 365.25)
# Current IV (use entry IV as fallback if current is invalid)
sig_curr = sigma_iv[i]
if not (np.isfinite(sig_curr) and sig_curr > 0.01):
sig_curr = prev_sig
# Mark-to-market change of SHORT straddle:
# new_straddle_value = straddle_value(S_curr, K, T_rem, sig_curr)
# P&L from option position = -(new_val - prev_val) [we're short]
# But the hedge also moves
# Spot hedge P&L = hedge_pos * (S_curr - prev_S)
# We track this explicitly via the straddle formula
# At expiry: T_rem = 0 → straddle = intrinsic = max(S-K,0) + max(K-S,0) = |S-K|
if i == i_expiry:
straddle_final = abs(S_curr - K)
# Settle: short straddle loses if straddle_final > some_threshold
# Net P&L = prem0 - straddle_final + hedge_pnl
# Hedge P&L from last rebalance to now:
hedge_pnl_final = prev_hedge * (S_curr - prev_S)
# Close hedge: pay fee on closing the spot position
close_hedge_cost = abs(prev_hedge) * S_curr * fee_hedge / K
total_pnl = prem0 - straddle_final + (
# Sum of all intermediate hedge P&L is already implicitly in the
# straddle mark-to-market (via put-call parity at each step).
# Actually: just compute total_pnl directly:
# P&L = premium_received - intrinsic_paid - sum(hedge_rebalance_costs)
# The hedge P&L and straddle MTM cancel each other (that's the whole
# point of delta hedging — the delta exposure is neutralized).
# So the final net = premium_received - realized_variance_cost - intrinsic_settlement
# where realized_variance_cost = sum of gamma * (dS)^2 / 2 per bar.
# This is what we compute below.
0 # placeholder
)
# ACTUALLY let's compute it cleanly: the total delta-hedged P&L is:
# P&L = premium_received - straddle_final_value + cumulative_hedge_rebalance_PnL - costs
# cumulative_hedge_rebalance_PnL = sum over all rebal: hedge_k * (S_{k+1} - S_k)
# This is complex to track; instead use the gamma P&L theorem:
# Total delta-hedged short straddle P&L = 0.5 * sum_k(gamma_k * S_k^2 * r_k^2) * (IV^2/RV^2 - 1)
# NO — let's just do it directly step by step.
break
# Intermediate bar: compute hedge rebalancing P&L
new_delta = straddle_delta(S_curr, K, T_rem, sig_curr)
new_hedge = -new_delta
# Spot hedge P&L for this bar
hedge_pnl = prev_hedge * (S_curr - prev_S)
total_pnl += hedge_pnl / K # add in fraction of K
# Rebalance cost
d_hedge = new_hedge - prev_hedge
rebal_cost = abs(d_hedge) * S_curr * fee_hedge / K
total_hedge_cost += rebal_cost
prev_S = S_curr
prev_sig = sig_curr
prev_hedge = new_hedge
# Final settlement
S_exp = close[i_expiry]
intrinsic = abs(S_exp - K)
hedge_pnl_final = prev_hedge * (S_exp - prev_S) / K
close_cost = abs(prev_hedge) * S_exp * fee_hedge / K
net_pnl = (prem0 - intrinsic) / K + hedge_pnl_final - total_hedge_cost - close_cost
return float(net_pnl), i_expiry
def compute_straddle_series(
df: pd.DataFrame,
asset: str,
roll_days: int,
iv_rv_gate: float,
rv_win_days: int = 20,
fee_hedge: float = 0.0005
) -> np.ndarray:
"""
Simulate the full delta-hedged short straddle strategy.
Returns per-bar P&L as a fraction of equity (additive).
Only enters when IV/RV >= gate.
"""
close = df["close"].values.astype(float)
n = len(close)
sigma_iv = al.dvol(df, asset) / 100.0
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)
first_valid = np.where(np.isfinite(sigma_iv) & (sigma_iv > 0.01))[0]
if len(first_valid) == 0:
return np.zeros(n)
start_bar = int(first_valid[0])
r_opt = np.zeros(n) # per-bar P&L
i = start_bar
while i < n:
sig_iv = sigma_iv[i]
sig_rv = rv_ann[i]
# Entry condition: valid IV, valid RV, IV/RV >= gate
if (np.isfinite(sig_iv) and sig_iv > 0.01 and
np.isfinite(sig_rv) and sig_rv > 0.01 and
sig_iv / sig_rv >= iv_rv_gate):
# Run one cycle
net_pnl, i_exp = simulate_straddle_cycle(
close, sigma_iv, i, roll_days, fee_hedge=fee_hedge
)
# Record P&L at settlement bar
r_opt[i_exp] = net_pnl
i = i_exp + 1 # next cycle starts after expiry
else:
# Skip bar (flat, no straddle)
i += 1
return r_opt
def eval_straddle_series(
df: pd.DataFrame,
r_opt: np.ndarray,
fee_side: float = al.FEE_SIDE
) -> dict:
"""
Evaluate the option P&L series as an independent equity curve.
The per-bar r_opt[i] is a P&L in fraction of current equity (additive).
We compound them: equity[i+1] = equity[i] * (1 + r_opt[i]).
IMPORTANT: the straddle already charges spot-hedge transaction costs internally.
The fee_side here is for the OPTION premium transaction (opening/closing the straddle
legs themselves), charged on a per-cycle basis.
We estimate: 2 legs * 2 sides * fee_side per cycle.
"""
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
n = len(r_opt)
# Option transaction cost: charge on settlement bars (each represents a closed cycle)
settle_bars = r_opt != 0
# Option bid-ask: straddle has 2 legs, each has entry + exit = 4 * fee_side
# But we use fee_side as option cost per leg per side ≈ 2-3x spot fee
option_tx_cost = np.where(settle_bars, 4 * fee_side, 0.0) # 4 legs total
r_net = r_opt - option_tx_cost
# Equity curve (compounding)
eq = np.cumprod(1.0 + np.clip(r_net, -0.99, None))
eq = np.concatenate([[1.0], eq])
# Returns for metrics
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(len(rr)))
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))
n_cycles = settle_bars.sum()
turnover_per_year = round(float(n_cycles / (span_days / 365.25)), 1)
return dict(full=full, holdout=hold, yearly=yearly,
time_in_market=round(float(n_cycles * roll_days_avg / n), 3)
if False else round(float(settle_bars.sum() / n), 3),
turnover_per_year=turnover_per_year)
# Monkey-patch eval_straddle_series to not reference roll_days_avg
def eval_straddle_series_v2(df, r_opt, fee_side=al.FEE_SIDE):
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
n = len(r_opt)
settle_bars = r_opt != 0
option_tx_cost = np.where(settle_bars, 4 * fee_side, 0.0)
r_net = r_opt - option_tx_cost
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))
n_cycles = int(settle_bars.sum())
turnover_per_year = round(float(n_cycles / (span_days / 365.25)), 1)
return dict(full=full, holdout=hold, yearly=yearly,
time_in_market=round(float(settle_bars.sum() / n), 3),
turnover_per_year=turnover_per_year)
def run_straddle(roll_days: int, iv_rv_gate: float, tfs=("1d",)) -> dict:
"""Run the delta-hedged short straddle study. Returns report dict."""
name = f"OPT05-Straddle-roll{roll_days}d-gate{iv_rv_gate:.2f}"
cells = []
for tf in tfs:
per_asset = {}
fee_ok_all = True
for asset in al.CERTIFIED:
df = al.get(asset, tf)
# Base run
r_opt = compute_straddle_series(df, asset, roll_days, iv_rv_gate)
base = eval_straddle_series_v2(df, r_opt, fee_side=al.FEE_SIDE)
# Fee sweep: only vary the option TX cost (spot hedge cost is fixed in the simulation)
sweep = {}
for f in al.FEE_SWEEP:
res = eval_straddle_series_v2(df, r_opt, fee_side=f)
sweep[f"{2*f*100:.2f}%RT"] = res["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("OPT05 — Delta-Hedged Short Straddle (IV-RV variance premium)")
print("CAVEAT: MODELED on DVOL ATM. Skew & real stress f not captured.")
print("DVOL starts 2021-03 → backtest from 2021-03 only.")
print()
# 4 configs, 1d TF only → 4 backtests
CONFIGS = [
(7, 1.10), # weekly, gate IV/RV >= 1.10
(7, 1.20), # weekly, gate IV/RV >= 1.20
(14, 1.10), # biweekly, gate IV/RV >= 1.10
(14, 1.20), # biweekly, gate IV/RV >= 1.20
]
best_rep = None
best_score = -999.0
for roll_days, iv_rv_gate in CONFIGS:
print(f"--- roll_days={roll_days}, iv_rv_gate={iv_rv_gate} ---")
rep = run_straddle(roll_days=roll_days, iv_rv_gate=iv_rv_gate, 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))