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

359 lines
14 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.
"""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))