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>
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user