research(mr02eth): ricerca sostituto + integrazione dati opzioni cerbero-bite

Ricerca sostituto/miglioria a MR02/ETH (127 strategie + 18 overlay opzioni,
verifica avversariale, gate PORT06). Esito: miglioria = no-SL (gia' ~catturata
da EXIT-16 live); tail-hedge opzioni NO-GO empirico su prezzi reali.

Infrastruttura opzioni REALE (muro ARGO/GEX caduto):
- options_fetcher.py: importa lo storico per-strike + market_snapshots da
  cerbero-bite -> data/options/ (chain bid/ask/IV/greche/OI + pannello regime
  con net-GEX dealer/gamma-flip/VRP/funding/liquidation).
- options_chain.py: loader + skew_curve/premium_levels (aggregati reali) +
  quote() causale + load_market() (pannello regime).
- option_overlay_lab.py: overlay opzioni BS su DVOL (pricing sintetico).
- mr02eth_port06_gate.py / eth_collar_gate.py: gate swap-sleeve e collar.
- mr02eth_search/options.workflow.js: i 2 workflow.

Numeri reali: skew put 10%OTM ~1.1 (liquido), premio ~1.0%/mese; niente strike
10%OTM a 24h (overlay per-trade infattibile); collar standing paga nei crash ma
net-negativo a PORT06 (alza OOS DD). Diario + CLAUDE.md aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-09 07:05:00 +00:00
parent cff40283f4
commit df8bfbceaa
10 changed files with 1516 additions and 1 deletions
+126
View File
@@ -0,0 +1,126 @@
"""Gate del CATASTROPHE-CAP auto-finanziato (collar standing) sullo sleeve ETH no-SL.
Tesi: lo sleeve ETH no-SL ha la coda da crash (un long-fade puo' perdere -50/-65% in un
gap). Un COLLAR standing rollato mensilmente — put lunga ~13% OTM finanziata da call corta
~10% OTM — cappa quella coda a premio netto ~zero (validato sui premi REALI di cerbero-bite:
put -13%≈1.0%/m IV55, call +10%≈1.05%/m IV49). Pricing BS calibrato sul reale: skew_put 1.12,
skew_call 1.0. Caveat: il collar aggiunge delta SHORT-ETH con dead-zone -p/+c -> cappa anche
l'upside; nei mesi tranquilli (ETH dentro la banda) costa ~zero. Il gate dice se aiuta netto.
uv run python scripts/analysis/eth_collar_gate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.explore_lab import get_df
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns, _norm
from scripts.analysis.option_overlay_lab import dvol_for, bs_put, bs_call
from scripts.analysis.mr02eth_port06_gate import (
gen_donchian_base, build_trades, build_trades_exit16, daily_equity, port_metrics, CAPS)
from src.portfolio.sleeves import all_sleeve_equities
HY = 24 * 365.0
def collar_daily_returns(df, dvol, p_otm=0.13, c_otm=0.10, skew_put=1.12, skew_call=1.0,
roll_h=24 * 30) -> pd.Series:
"""Collar standing rollato ogni roll_h ore. Ritorna la SERIE di rendimenti GIORNALIERI
(frazione del notional collar): MTM = d(intrinseco) - theta (premio netto amortizzato)."""
c = df["close"].values; ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
n = len(c)
val = np.zeros(n) # valore collar (frac notional) marcato a intrinseco - premio residuo
T = roll_h / HY
k = 0
while k < n - 1:
S0 = c[k]; sig = dvol[k] if not np.isnan(dvol[k]) else 0.6
Kp = S0 * (1 - p_otm); Kc = S0 * (1 + c_otm)
prem = bs_put(S0, Kp, T, sig * skew_put) / S0 - bs_call(S0, Kc, T, sig * skew_call) / S0
end = min(k + roll_h, n - 1)
span = end - k
for j in range(k, end + 1):
intr = (max(Kp - c[j], 0.0) - max(c[j] - Kc, 0.0)) / S0
frac_elapsed = (j - k) / span if span else 1.0
val[j] = intr - prem * (1 - frac_elapsed) # premio pagato up-front, amortizzato a 0 a scadenza
k = end
s = pd.Series(val, index=ts)
daily = s.resample("1D").last().reindex(IDX).ffill().bfill()
return daily.diff().fillna(0.0) # rendimento giornaliero (frac notional)
def combine(fade_eq: pd.Series, collar_dr: pd.Series, hedge_frac: float) -> pd.Series:
"""sleeve = fade no-SL + hedge_frac * collar. Combina i rendimenti giornalieri."""
fr = fade_eq.pct_change().fillna(0.0)
return _norm((1 + fr + hedge_frac * collar_dr).cumprod())
def crash_audit(df, dvol, p_otm, c_otm, hedge_frac):
"""P&L del collar nei mesi di crollo ETH peggiori (frac notional)."""
cr = collar_daily_returns(df, dvol, p_otm=p_otm, c_otm=c_otm)
# ETH daily ret mensile
cdf = pd.Series(df["close"].values, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True)).resample("1D").last().reindex(IDX).ffill()
mret = cdf.resample("30D").last().pct_change()
collar_m = (1 + cr).resample("30D").apply(lambda x: x.prod()) - 1
worst = mret.nsmallest(5)
print(f" {'mese (fine)':>12}{'ETH 30g%':>10}{'collar P&L%':>13}")
for t, r in worst.items():
cm = collar_m.reindex([t], method="nearest").iloc[0] * hedge_frac * 100
print(f" {str(t.date()):>12}{r*100:>9.0f}%{cm:>12.1f}%")
def main():
print("=" * 96)
print(f" GATE collar standing (catastrophe-cap) sullo sleeve ETH no-SL | OOS da {OOS_DATE}")
print("=" * 96)
df = get_df("ETH", "1h"); dvol = dvol_for(df, "ETH")
eq = dict(all_sleeve_equities())
ids = [k for k in eq if k in {"MR01_BTC","MR02_BTC","MR07_BTC","MR01_ETH","MR02_ETH","MR07_ETH",
"DIP01_BTC","TR01_basket","ROT02_rot","PR_ETHBTC","PR_LTCETH","PR_ADAETH","PR_BTCLTC","PR_ETHSOL","TSM01","SH_BTC","SH_ETH"}]
base_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
nosl_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False)
def pm(ce):
m = dict(eq); m["MR02_ETH"] = ce; return port_metrics(m, ids)
f_l, o_l = pm(daily_equity(build_trades_exit16(base_ents, df, sl_confirm=0.5), df))
fade_nosl = daily_equity(build_trades(nosl_ents, df), df)
f0, o0 = pm(fade_nosl)
print(f"\n {'sleeve ETH':<30s}{'FULL Sh':>8s}{'FULL DD':>8s} |{'OOS Sh':>8s}{'OOS DD':>8s}")
print(" " + "-" * 78)
print(f" {'LIVE EXIT-16 (rif)':<30s}{f_l['sharpe']:>8.2f}{f_l['dd']:>8.2f} |{o_l['sharpe']:>8.2f}{o_l['dd']:>8.2f}")
print(f" {'no-SL nudo':<30s}{f0['sharpe']:>8.2f}{f0['dd']:>8.2f} |{o0['sharpe']:>8.2f}{o0['dd']:>8.2f}")
configs = [
("put13/call10 hf0.45", 0.13, 0.10, 0.45),
("put13/call10 hf0.30", 0.13, 0.10, 0.30),
("put15/call12 hf0.45", 0.15, 0.12, 0.45),
("put20/call15 hf0.45", 0.20, 0.15, 0.45),
("put13/call08 hf0.45", 0.13, 0.08, 0.45),
]
rows = []
for name, p, cc, hf in configs:
cr = collar_daily_returns(df, dvol, p_otm=p, c_otm=cc)
ce = combine(fade_nosl, cr, hf)
f_c, o_c = pm(ce)
rows.append((name, p, cc, hf, f_c, o_c))
print(f" {'no-SL + '+name:<30s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f} |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}")
print("\n " + "=" * 90)
print(f" vs LIVE EXIT-16 (FULL {f_l['sharpe']:.2f}/{f_l['dd']:.2f} OOS {o_l['sharpe']:.2f}/{o_l['dd']:.2f}) e vs no-SL")
print(" " + "-" * 90)
for name, p, cc, hf, f_c, o_c in rows:
print(f" {name:<22s} Δ vsEXIT16 FULL {f_c['sharpe']-f_l['sharpe']:+.2f}/{f_c['dd']-f_l['dd']:+.2f} "
f"OOS {o_c['sharpe']-o_l['sharpe']:+.2f}/{o_c['dd']-o_l['dd']:+.2f} | "
f"Δ vsNoSL DD {f_c['dd']-f0['dd']:+.2f}")
print("\n --- audit crash: P&L collar (hf-scaled) nei 5 mesi ETH peggiori (put13/call10 hf0.45) ---")
crash_audit(df, dvol, 0.13, 0.10, 0.45)
if __name__ == "__main__":
main()
@@ -0,0 +1,129 @@
export const meta = {
name: 'mr02eth-options-protection',
description: 'Overlay di opzioni (protezione coda) sul fade ETH: 18 design DVOL-prezzati + sintesi con audit crash reali',
phases: [
{ title: 'Options', detail: '18 agenti, overlay opzione distinto ciascuno, prezzato via DVOL+BS, premio dedotto' },
{ title: 'Synthesis', detail: 'audit crash reali (FTX/maggio-21/ago-24) e sintesi del miglior overlay deployabile' },
],
}
const RECIPE = `Lavora in /opt/docker/PythagorasGoal. Testa UN overlay di OPZIONI come protezione di coda sul fade ETH (per rendere DEPLOYABILE la versione ad alto Sharpe e cappare i crash).
Usa l'harness condiviso scripts.analysis.option_overlay_lab (gia' pronto e verificato):
from scripts.analysis.option_overlay_lab import get_df, dvol_for, donchian_fade, simulate_hedged, evaluate_hedged, bs_put, bs_call
- df = get_df("ETH","1h"); dv = dvol_for(df,"ETH") # DVOL frazione, causale (oraria 2021-03 -> 2026-06; pre-2021 e' bfilled approssimato)
- donchian_fade(df, n=20, sl_atr=2.0, max_bars=24, trend_max=3.0, ema_long=200, use_sl=True/False) -> entries MR02/ETH (use_sl=False = togli lo stop ATR)
- simulate_hedged(entries, df, dv, hedge=..., otm=..., otm2=..., skew_mult=1.10, tenor_mult=1.0, hedge_side="both|long|short", min_dvol=0.0, split=-1) -> dict(trades,win,ret,dd,sharpe,yearly,exposure,prem_paid_pct,pay_recv_pct)
- evaluate_hedged(name, entries, df, dv, **hcfg) -> {full,oos,sweep,sweep_oos,pos_yrs,n_yrs} e stampa una riga (prem/pay = premi pagati e payoff incassati cumulati %).
hedge: "none" | "put" (put se long / call se short, floor a otm) | "put_spread" (compra otm, vende otm2 piu' lontano) | "collar" (compra protezione otm, vende l'opzione opposta a otm2 per finanziare).
Prezzo opzioni: Black-Scholes(spot, DVOL) r=0, DELIBERATAMENTE CONSERVATIVO (bias CONTRO le opzioni): skew_mult rincara la sigma dei comprati, all'uscita anticipata l'opzione vale solo l'INTRINSECO. Se sotto queste ipotesi pessimistiche l'overlay aiuta, e' robusto. I numeri sono INDICATIVI (la DVOL e' reale e gratuita, ma niente fill/skew per-strike reali).
RIFERIMENTO gia' misurato (ETH 1h, fee 0.10% RT, lev 3x, pos 0.15):
- baseline MR02/ETH con SL-ATR + trend3.0: Sharpe 8.45, full DD 25%, OOS DD 13%.
- no-SL (stesso fade SENZA stop ATR): Sharpe 11.63, full DD 31%, OOS DD 7%, anniPos 8/9 (togliere lo stop e' il salto piu' grande, coerente con EXIT-16; ma sl=0 NON e' deployabile per il rischio coda da crash -> qui le opzioni servono a cappare quella coda).
- no-SL + put 8% OTM: Sharpe 12.13, full DD 33%, premio cum 360% ~ break-even col payoff 255%.
- no-SL + put 3% OTM: premio mostruoso (2363%), peggiora -> troppo vicina.
OBIETTIVO: trovare l'overlay che dia il MIGLIOR rischio/rendimento DEPLOYABILE. Conta soprattutto: (a) Sharpe FULL e OOS >= no-SL, (b) drawdown contenuto, (c) la CODA nei crash veri sia cappata (l'opzione DEVE pagare quando ETH crolla), (d) premio netto sostenibile, (e) regge skew_mult piu' alto. Focalizza la lettura sul 2021+ e sull'OOS (DVOL reale).
Procedura: implementa/configura il tuo overlay (puoi scrivere uno script in /tmp/opt_KEY.py se serve, ma puoi anche chiamare direttamente le funzioni), fai uno sweep PICCOLO dei suoi parametri, scegli la config migliore. Riporta i numeri VERI dall'esecuzione. Sii onesto: se l'overlay non aiuta, dillo (e' informazione utile).`
const SCHEMA = {
type: 'object', additionalProperties: false,
required: ['key','structure','base','ran_ok','best_config','full_sharpe','full_ret','full_dd','oos_sharpe','oos_dd','prem_pct','pay_pct','full_dd_skew150','deployable','improves_over_noSL','improves_over_baseline','no_lookahead','tail_note','note'],
properties: {
key: { type: 'string' },
structure: { type: 'string', description: 'put | put_spread | collar | standing | other' },
base: { type: 'string', description: 'no-SL | SL-ATR | exit16' },
ran_ok: { type: 'boolean' },
best_config: { type: 'string' },
full_sharpe: { type: 'number' },
full_ret: { type: 'number' },
full_dd: { type: 'number' },
oos_sharpe: { type: 'number' },
oos_dd: { type: 'number' },
prem_pct: { type: 'number', description: 'premio cumulato pagato % (full)' },
pay_pct: { type: 'number', description: 'payoff cumulato incassato % (full)' },
full_dd_skew150: { type: 'number', description: 'full DD con skew_mult=1.5 (stress del premio); -1 se non testato' },
deployable: { type: 'boolean', description: 'coda cappata + premio sostenibile -> deployabile (a differenza del no-SL nudo)' },
improves_over_noSL: { type: 'boolean' },
improves_over_baseline: { type: 'boolean', description: 'batte MR02/ETH baseline SL-ATR (Sharpe 8.45 / DD 25)' },
no_lookahead: { type: 'boolean', description: 'DVOL causale, ingresso a close[i], nessun look-ahead' },
tail_note: { type: 'string', description: 'comportamento nei crash: l opzione paga? di quanto cappa la perdita peggiore?' },
note: { type: 'string' },
},
}
const SPECS = [
['put_floor_sweep','put','no-SL','no-SL + put protettiva (call sugli short). Sweep OTM 5/8/10/12%. Trova il floor che massimizza Sharpe a premio netto sostenibile. Riferimento: 8% sembra il punto dolce.'],
['put_spread_ladder','put_spread','no-SL','no-SL + debit put-spread (compra otm, vende otm2 piu lontano) per abbattere il premio. Sweep (otm,otm2): (5,15),(8,20),(8,25),(10,25). Il cap del payoff e accettabile se i crash ETH raramente vanno oltre otm2 nell orizzonte.'],
['collar_financed','collar','no-SL','no-SL + collar (vende call OTM per finanziare la put). Sweep (put_otm, call_otm): (5,8),(8,10),(8,15),(10,15). Attenzione: la call venduta cappa gli short-fade vincenti -> testa se il netto regge.'],
['atm_put_shortdated','put','no-SL','no-SL + put quasi-ATM ma tenor minimo (tenor_mult=1, copre solo l orizzonte): e mai conveniente la protezione massima a scadenza cortissima? Sweep otm 0/2/3%. Probabile premio troppo alto: verifica.'],
['longer_tenor_put','put','no-SL','no-SL + put a tenor PIU LUNGO (tenor_mult 3/5/7 = settimanale/mensile): il time-value per-ora e piu basso, e la stessa put copre piu trade. Sweep tenor_mult e otm. Confronta premio cum vs tenor=1.'],
['regime_gated_put_high','put','no-SL','no-SL + put SOLO quando DVOL all ingresso > soglia (min_dvol 0.8/1.0/1.2): proteggi solo nei regimi di vol elevata (dove i crash si addensano), resta nudo nella calma. Sweep soglia/otm. Riduce il premio totale?'],
['regime_gated_put_low','put','no-SL','no-SL + put SOLO quando DVOL e BASSA (assicurazione a buon mercato prima che la vol esploda). Implementa un gate min_dvol-inverso (modifica/wrap: salta l hedge se dvol>soglia). Test controintuitivo: la calma e dove la coda e meno prezzata.'],
['long_only_floor','put','no-SL','no-SL + put SOLO sui long-fade (hedge_side="long", le prese-coltello su un ETH che crolla = il failure mode documentato di MR02). Gli short-fade restano nudi. Sweep otm. Dimezza il premio focalizzandolo dove serve?'],
['short_only_cap','put','no-SL','no-SL + call SOLO sugli short-fade (hedge_side="short"): cappa il rischio di shortare un rimbalzo esplosivo. Sweep otm. ETH rimbalza meno violentemente di quanto crolla -> probabilmente meno utile dei long: misura.'],
['asym_put_call','put','no-SL','no-SL + protezione ASIMMETRICA: put piu vicina sui long (ETH crolla forte e veloce), call piu lontana sugli short. Implementa due chiamate simulate_hedged side-split, oppure due passate con hedge_side e somma le equity. Sweep (put_otm, call_otm).'],
['skew_stress','put','no-SL','Prendi la migliore put-floor (es 8% OTM) e fai lo STRESS dello skew: skew_mult 1.0/1.1/1.25/1.5/1.75. A che skew il premio mangia l edge? Riporta la sensibilita: e l overlay sopravvive allo skew realistico (~1.2-1.4 sui put cripto)?'],
['dynamic_otm_dvol','put','no-SL','OTM dinamico in funzione di DVOL: protezione piu lontana quando la vol e alta (la stessa % di mossa e piu probabile -> protezione relativamente piu economica). Implementa un loop che varia otm = base + k*(dvol-mediana). Sweep base/k.'],
['exit16_base_put','put','exit16','Base = EXIT-16 (close-confirm SL, gia LIVE) invece del no-SL nudo: applica la put-floor sopra l EXIT-16. Modella EXIT-16 come SL-at-level approssimato (usa donchian_fade con use_sl=True ma sl piu largo, sl_atr 3.0) + put. Confronta: l opzione aggiunge valore SOPRA l EXIT-16 gia deployato?'],
['standing_weekly_put','standing','no-SL','Put SETTIMANALE STANDING su notional ETH fisso (non per-trade): compra una put 1-settimana ~10% OTM ogni 168h, rolla, ammortizza il premio su TUTTI i trade della settimana. Implementa una serie di payoff/premio settimanale dalla DVOL e sommala all equity del fade no-SL. Confronta col per-trade.'],
['portfolio_tail_overlay','standing','no-SL','Overlay di coda a livello SLEEVE: una put mensile ~15-20% OTM su notional ETH costante come assicurazione catastrofica (rara ma paga nei crash -30%+). Misura il costo annuo (premio) vs la riduzione del DD/coda dello sleeve no-SL. E un trade-off accettabile (pochi bps/anno per cappare il -39% FTX)?'],
['funding_financed','put','no-SL','Finanzia il premio della put col CARRY del funding perp: quando il funding e positivo, il long perp incassa funding che offre il premio. Usa eth_funding (interest_8h) per stimare il carry accumulato per-trade e sottrailo dal premio netto. Il funding copre quanto del premio?'],
['ratio_put_spread','put_spread','no-SL','Ratio put-spread (compra 1 put otm, vende 2 put otm2): premio quasi-zero o credito, ma reintroduce rischio sotto otm2. Approssima con put_spread + una seconda put venduta. Test del trade-off: il credito vale il rischio di coda residuo? Audita i crash.'],
['best_combo','put','no-SL','SINTESI best-guess: no-SL + put-floor regime-gated (DVOL>0.8) all OTM ottimale + funding-offset. Combina le leve migliori che ti aspetti e riporta full/OOS Sharpe, DD, premio netto, e il comportamento nei 3 peggiori crash. E questo il candidato deployabile?'],
]
phase('Options')
log(`Lancio ${SPECS.length} agenti di overlay-opzione (DVOL-prezzati, premio conservativo)`)
const results = await parallel(SPECS.map(([key, structure, base, idea]) => () =>
agent(
`${RECIPE}\n\n========\nIL TUO OVERLAY — key="${key}", structure="${structure}", base="${base}":\n${idea}\n\nRestituisci il risultato strutturato (key/structure/base coi tuoi valori).`,
{ label: `opt:${key}`, phase: 'Options', schema: SCHEMA }
).then(r => r ? { ...r, key, structure, base } : null)
))
const ran = results.filter(Boolean)
log(`${ran.length}/${SPECS.length} overlay rientrati`)
// candidati deployabili interessanti: girati, no look-ahead, migliorano almeno la baseline, coda cappata
const good = ran.filter(r => r.ran_ok && r.no_lookahead && r.deployable &&
(r.improves_over_baseline || r.improves_over_noSL))
good.sort((a, b) => (b.oos_sharpe - 0.1 * b.full_dd) - (a.oos_sharpe - 0.1 * a.full_dd))
const top = good.slice(0, 6)
phase('Synthesis')
log(`Sintesi: audit crash reali sui top ${top.length} overlay`)
const SYN = {
type: 'object', additionalProperties: false,
required: ['recommendation','deployable_config','rationale','crash_audit','caveats','full_sharpe','full_dd','oos_sharpe','oos_dd','net_premium_drag_pct'],
properties: {
recommendation: { type: 'string', description: 'verdetto: usare le opzioni si/no e come' },
deployable_config: { type: 'string', description: 'la config esatta consigliata (struttura, OTM, tenor, gating)' },
rationale: { type: 'string' },
crash_audit: { type: 'string', description: 'comportamento nei crash reali (FTX nov-22, crash mag-21, ago-24): perdita no-SL vs hedged' },
caveats: { type: 'string', description: 'limiti del modello DVOL-BS (no skew/fill reali), rischio esecuzione' },
full_sharpe: { type: 'number' },
full_dd: { type: 'number' },
oos_sharpe: { type: 'number' },
oos_dd: { type: 'number' },
net_premium_drag_pct: { type: 'number', description: 'premio netto (premio-payoff) cumulato % della config consigliata' },
},
}
const summary = await agent(
`${RECIPE}\n\n========\nSEI L'AGENTE DI SINTESI. Hai i risultati dei migliori overlay-opzione sul fade ETH:\n${JSON.stringify(top, null, 1)}\n\nFai un AUDIT dei CRASH REALI: ricostruisci (con option_overlay_lab) il comportamento della migliore config nelle finestre di crollo ETH piu' severe (crash maggio 2021, collasso FTX nov 2022, e altri drawdown -25%+ del 2024/2025). Per ciascuna mostra: la perdita peggiore del fade no-SL NUDO vs con l'overlay (l'opzione DEVE pagare li). Scrivi uno script /tmp/crash_audit.py se serve (filtra i trade per finestra temporale e confronta).\n\nPoi dai la RACCOMANDAZIONE finale: conviene usare le opzioni per proteggere lo sleeve ETH? Quale config esatta e' deployabile (struttura, OTM, tenor, regime-gating)? Con quali numeri (full/OOS Sharpe, DD, premio netto)? E i CAVEAT onesti (il modello DVOL-BS non ha skew/fill reali; serve verificare liquidita'/prezzi reali su Deribit prima di rischiare capitale). Sii sobrio e onesto.`,
{ label: 'synthesis', phase: 'Synthesis', schema: SYN }
)
return {
n_specs: SPECS.length, n_ran: ran.length, n_good: good.length,
recommendation: summary,
top_overlays: top.map(r => ({ key: r.key, structure: r.structure, base: r.base, best_config: r.best_config,
full_sharpe: r.full_sharpe, full_dd: r.full_dd, oos_sharpe: r.oos_sharpe, oos_dd: r.oos_dd,
prem_pct: r.prem_pct, pay_pct: r.pay_pct, deployable: r.deployable, tail_note: r.tail_note, note: r.note })),
all: ran.map(r => ({ key: r.key, structure: r.structure, full_sharpe: r.full_sharpe, full_dd: r.full_dd,
oos_sharpe: r.oos_sharpe, oos_dd: r.oos_dd, prem_pct: r.prem_pct, pay_pct: r.pay_pct,
deployable: r.deployable, improves_over_baseline: r.improves_over_baseline, improves_over_noSL: r.improves_over_noSL, note: r.note })),
}
+338
View File
@@ -0,0 +1,338 @@
"""GATE PORT06: i top candidati sostituiscono MR02/ETH. Misura FULL+OOS Sharpe/DD.
Ogni candidato genera i trade ETH 1h con l'ENGINE INTRABAR identico al sleeve canonico
(explore_lab.simulate: SL/TP intrabar al livello, fee 0.10% RT, lev 3x), equity giornaliera
normalizzata su IDX (2021-01-01 -> 2026-05-26), swap su all_sleeve_equities()['MR02_ETH'],
e ri-misura PORT06 (cap weighting PAIRS 0.33 / SHAPE 0.0588).
uv run python scripts/analysis/mr02eth_port06_gate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.explore_lab import get_df, atr, ema
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns, _norm
from src.portfolio.sleeves import all_sleeve_equities
from src.portfolio import weighting as W
FEE_RT, LEV, POS = 0.001, 3.0, 0.15
CAPS = {"PAIRS": 0.33, "SHAPE": 0.0588}
# ----------------------- engine intrabar (== explore_lab.simulate) -----------------------
def build_trades(entries, df, fee_rt=FEE_RT, lev=LEV):
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c); out = []; last = -1
for e in entries:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_sl:
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if k == mb:
exit_p = c[j]
out.append((i, j, (exit_p - entry) / entry * d * lev - fee_rt * lev)); last = j
return out
def build_trades_exit16(entries, df, sl_confirm=0.5, fee_rt=FEE_RT, lev=LEV,
dvol=None, otm=None, skew=1.10, tenor_mult=1.0):
"""Engine EXIT-16 close-confirm (== config LIVE): SL intrabar OFF, lo stop scatta solo se il
CLOSE sfonda sl ∓ sl_confirm*ATR14; TP intrabar ha priorita'.
Se dvol+otm sono dati, AGGIUNGE un overlay opzione (put se long / call se short) a otm OTM."""
from scripts.analysis.option_overlay_lab import bs_put, bs_call
h, l, c = df["high"].values, df["low"].values, df["close"].values
a = atr(df, 14); n = len(c); out = []; last = -1
for e in entries:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_tp:
exit_p = tp; break
if sl is not None and not np.isnan(a[j]):
buf = sl_confirm * a[j]
if (d == 1 and c[j] < sl - buf) or (d == -1 and c[j] > sl + buf):
exit_p = c[j]; break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee_rt * lev
if dvol is not None and otm is not None:
T = max(mb * tenor_mult, 1.0) / _HOURS_YEAR; sig = dvol[i] * skew
if d == 1:
K = entry * (1 - otm); prem = bs_put(entry, K, T, sig) / entry; pay = max(K - exit_p, 0.0) / entry
else:
K = entry * (1 + otm); prem = bs_call(entry, K, T, sig) / entry; pay = max(exit_p - K, 0.0) / entry
ret += lev * (pay - prem)
out.append((i, j, ret)); last = j
return out
def blend_equity(eqs, weights=None) -> pd.Series:
"""Combina N equity giornaliere mediando i rendimenti giornalieri (ribilancio daily)."""
drs = [e.pct_change().fillna(0.0) for e in eqs]
w = weights or [1.0 / len(drs)] * len(drs)
dr = sum(wi * di for wi, di in zip(w, drs))
return _norm((1 + dr).cumprod())
def daily_equity(trades, df) -> pd.Series:
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
n = len(df); eq = np.full(n, 1000.0); cap = 1000.0
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0); eq[j:] = cap
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
return _norm(s)
# ----------------------- indicatori -----------------------
def choppiness(df, n=14):
h, l, c = df["high"].values, df["low"].values, df["close"].values
pc = np.roll(c, 1); pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
str_ = pd.Series(tr).rolling(n).sum().values
hh = pd.Series(h).rolling(n).max().values
ll = pd.Series(l).rolling(n).min().values
rng = hh - ll
with np.errstate(divide="ignore", invalid="ignore"):
ci = 100.0 * np.log10(str_ / rng) / np.log10(n)
return ci
def var_ratio(close, k=30, win=100):
r1 = pd.Series(close).pct_change()
rk = pd.Series(close).pct_change(k)
v1 = r1.rolling(win).var().values
vk = rk.rolling(win).var().values
with np.errstate(divide="ignore", invalid="ignore"):
vr = vk / (k * v1)
return vr
def donchian_levels(df, n):
hh = pd.Series(df["high"].values).rolling(n).max().shift(1).values
ll = pd.Series(df["low"].values).rolling(n).min().shift(1).values
return hh, ll
# ----------------------- generatori di segnale (candidati) -----------------------
def gen_donchian_base(df, n=20, sl_atr=2.0, max_bars=24, trend_max=None, ema_long=200, gate=None, use_sl=True):
"""gate(i)->bool: True = consenti il segnale alla barra i. None = sempre. use_sl=False -> sl=None (no-SL)."""
h, l, c = df["high"].values, df["low"].values, df["close"].values
a = atr(df, 14); hh, ll = donchian_levels(df, n); em = ema(c, ema_long)
ents = []
for i in range(max(n, ema_long, 14) + 1, len(c)):
if np.isnan(hh[i]) or np.isnan(ll[i]) or np.isnan(a[i]) or a[i] <= 0:
continue
if trend_max is not None and not np.isnan(em[i]) and abs(c[i] - em[i]) / a[i] > trend_max:
continue
if gate is not None and not gate(i):
continue
tp = (hh[i] + ll[i]) / 2.0
if c[i] < ll[i] and c[i - 1] >= ll[i - 1]:
ents.append({"i": i, "d": 1, "tp": tp, "sl": (c[i] - sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
elif c[i] > hh[i] and c[i - 1] <= hh[i - 1]:
ents.append({"i": i, "d": -1, "tp": tp, "sl": (c[i] + sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
return ents
# ----------------------- engine intrabar + overlay opzione (per i candidati no-SL) -----------------------
_HOURS_YEAR = 24 * 365.0
def build_trades_hedged(entries, df, dvol, otm=0.10, skew=1.10, tenor_mult=1.0, fee_rt=FEE_RT, lev=LEV):
from scripts.analysis.option_overlay_lab import bs_put, bs_call
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c); out = []; last = -1
for e in entries:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_sl:
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if k == mb:
exit_p = c[j]
base = (exit_p - entry) / entry * d * lev - fee_rt * lev
T = max(mb * tenor_mult, 1.0) / _HOURS_YEAR; sig = dvol[i]; sigb = sig * skew
if d == 1:
K = entry * (1.0 - otm); prem = bs_put(entry, K, T, sigb) / entry; pay = max(K - exit_p, 0.0) / entry
else:
K = entry * (1.0 + otm); prem = bs_call(entry, K, T, sigb) / entry; pay = max(exit_p - K, 0.0) / entry
out.append((i, j, base + lev * (pay - prem))); last = j
return out
def cand_choppiness_gate_fade(df):
ci = choppiness(df, 14)
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=None,
gate=lambda i: not np.isnan(ci[i]) and ci[i] >= 50.0)
def cand_choppiness_donchian(df):
ci = choppiness(df, 14)
return gen_donchian_base(df, n=14, sl_atr=2.0, trend_max=3.0,
gate=lambda i: not np.isnan(ci[i]) and ci[i] > 61.8)
def cand_varratio_gate_fade(df):
vr = var_ratio(df["close"].values, k=30, win=100)
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0,
gate=lambda i: not np.isnan(vr[i]) and vr[i] < 0.95)
def cand_baseline_recon(df):
"""MR02/ETH canonico ricostruito col MIO engine (sanity check vs all_sleeve_equities)."""
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
def cand_vrp_neg_dvol_low(df):
from scripts.analysis.regime_lab import load, regime_features
rdf = load("ETH", "1h")
R = regime_features(rdf)
# allinea per indice posizionale (regime_lab.load parte da get_df, stesso ordinamento)
vrp = R["vrp"]; dvp = R["dvol_pct"]
m = min(len(df), len(vrp))
def gate(i):
if i >= m:
return False
return (not np.isnan(vrp[i]) and vrp[i] < 0) and (not np.isnan(dvp[i]) and dvp[i] < 0.60)
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=None, gate=gate)
# ----------------------- gate PORT06 -----------------------
def port_metrics(members, ids):
w = W.weight_vector("cap", ids, None, caps=CAPS)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
def main():
print("=" * 104)
print(f" GATE PORT06 — sostituire MR02/ETH | finestra {IDX[0].date()}..{IDX[-1].date()} | OOS da {OOS_DATE}")
print("=" * 104)
eq = dict(all_sleeve_equities())
ids = [k for k in eq if k in {
"MR01_BTC","MR02_BTC","MR07_BTC","MR01_ETH","MR02_ETH","MR07_ETH",
"DIP01_BTC","TR01_basket","ROT02_rot",
"PR_ETHBTC","PR_LTCETH","PR_ADAETH","PR_BTCLTC","PR_ETHSOL","TSM01","SH_BTC","SH_ETH"}]
print(f" sleeve PORT06: {len(ids)} | MR02_ETH presente: {'MR02_ETH' in ids}")
f_b, o_b = port_metrics(eq, ids)
print(f"\n {'variante':<22s}{'FULL Sh':>8s}{'FULL DD':>8s}{'FULL CAGR':>10s} |{'OOS Sh':>8s}{'OOS DD':>8s}{'OOS CAGR':>9s}")
print(" " + "-" * 100)
print(f" {'BASELINE (canonico)':<22s}{f_b['sharpe']:>8.2f}{f_b['dd']:>8.2f}{f_b['cagr']:>9.0f}% |{o_b['sharpe']:>8.2f}{o_b['dd']:>8.2f}{o_b['cagr']:>8.0f}%")
df = get_df("ETH", "1h")
from scripts.analysis.option_overlay_lab import dvol_for
dvol = dvol_for(df, "ETH")
# candidati: (nome, builder) dove builder(df)->trades
def b_signal(fn):
return lambda df: build_trades(fn(df), df)
base_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
def b_exit16(df):
return build_trades_exit16(base_ents, df, sl_confirm=0.5)
def b_exit16_put10(df):
return build_trades_exit16(base_ents, df, sl_confirm=0.5, dvol=dvol, otm=0.10)
def b_noSL(df):
return build_trades(gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False), df)
def b_noSL_put10(df):
ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False)
return build_trades_hedged(ents, df, dvol, otm=0.10)
cands = {
"MR02recon(sanity)": b_signal(cand_baseline_recon),
"varratio_gate": b_signal(cand_varratio_gate_fade),
"choppiness_donch": b_signal(cand_choppiness_donchian),
"vrp_neg_dvol_low": b_signal(cand_vrp_neg_dvol_low),
"EXIT16(live)": b_exit16,
"EXIT16+put10%OTM": b_exit16_put10,
"noSL_raw": b_noSL,
"noSL_put10%OTM": b_noSL_put10,
}
rows = []
equ = {} # salva le equity per i blend
for name, fn in cands.items():
try:
tr = fn(df)
ce = daily_equity(tr, df)
equ[name] = ce
m2 = dict(eq); m2["MR02_ETH"] = ce
f_c, o_c = port_metrics(m2, ids)
rows.append((name, len(tr), f_c, o_c))
print(f" {name:<22s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f}{f_c['cagr']:>9.0f}% |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}{o_c['cagr']:>8.0f}%"
f" ({len(tr)} trade)")
except Exception as ex:
print(f" {name:<22s} ERRORE: {ex}")
# ---- BLEND within-sleeve: riempi lo sleeve ETH con EXIT-16 + un candidato decorrelato ----
print(" " + "-" * 100 + "\n BLEND within-sleeve (lo sleeve ETH = mix di 2 strategie, peso PORT06 invariato):")
blends = {
"50/50 EXIT16+varratio": (["EXIT16(live)", "varratio_gate"], [0.5, 0.5]),
"50/50 EXIT16+chopDonch": (["EXIT16(live)", "choppiness_donch"], [0.5, 0.5]),
"50/50 EXIT16+vrp": (["EXIT16(live)", "vrp_neg_dvol_low"], [0.5, 0.5]),
"70/30 EXIT16+vrp": (["EXIT16(live)", "vrp_neg_dvol_low"], [0.7, 0.3]),
"50/50 EXIT16put+vrp": (["EXIT16+put10%OTM", "vrp_neg_dvol_low"], [0.5, 0.5]),
"tri EXIT16put+vrp+chop": (["EXIT16+put10%OTM", "vrp_neg_dvol_low", "choppiness_donch"], [0.5, 0.25, 0.25]),
}
for name, (keys, wts) in blends.items():
try:
be = blend_equity([equ[k] for k in keys], wts)
m2 = dict(eq); m2["MR02_ETH"] = be
f_c, o_c = port_metrics(m2, ids)
rows.append((name, -1, f_c, o_c))
print(f" {name:<22s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f}{f_c['cagr']:>9.0f}% |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}{o_c['cagr']:>8.0f}%")
except Exception as ex:
print(f" {name:<22s} ERRORE: {ex}")
# riferimento ONESTO = EXIT-16 (config LIVE), non il canonico intrabar-SL
ex = next((r for r in rows if r[0] == "EXIT16(live)"), None)
f_l, o_l = (ex[2], ex[3]) if ex else (f_b, o_b)
print("\n " + "=" * 100)
print(f" GATE vs LIVE EXIT-16 (FULL {f_l['sharpe']:.2f}/{f_l['dd']:.2f} OOS {o_l['sharpe']:.2f}/{o_l['dd']:.2f}):")
print(" MIGLIORIA = nessuna metrica peggiora oltre il rumore E almeno una migliora (Sharpe +>=0.03 o DD -)")
print(" " + "-" * 100)
for name, ntr, f_c, o_c in rows:
if name.startswith("MR02recon") or name == "EXIT16(live)":
continue
dfs, dfd = f_c['sharpe'] - f_l['sharpe'], f_c['dd'] - f_l['dd']
dos, dod = o_c['sharpe'] - o_l['sharpe'], o_c['dd'] - o_l['dd']
no_worse = dfs >= -0.03 and dos >= -0.03 and dfd <= 0.05 and dod <= 0.03
better = dfs >= 0.03 or dos >= 0.03 or dfd <= -0.03 or dod <= -0.03
ok = no_worse and better
print(f" {name:<22s} ΔFULL Sh {dfs:+.2f} DD {dfd:+.2f} | ΔOOS Sh {dos:+.2f} DD {dod:+.2f} -> "
f"{'>>> MIGLIORIA' if ok else ('= pari' if no_worse else 'peggiora')}")
if __name__ == "__main__":
main()
+279
View File
@@ -0,0 +1,279 @@
export const meta = {
name: 'mr02eth-replace-search',
description: 'Cerca sostituto/miglioria a MR02/ETH: 112 strategie distinte su harness onesto + verifica avversariale',
phases: [
{ title: 'Generate', detail: '112 agenti, una strategia ETH distinta ciascuno, backtest onesto fee-aware OOS' },
{ title: 'Verify', detail: 'verifica avversariale dei survivor (look-ahead audit + re-run + robustezza)' },
],
}
// ---------------------------------------------------------------- baseline & recipe
const BASELINE = `BASELINE DA BATTERE (MR02/ETH sullo STESSO engine explore_lab, ETH 1h, fee 0.10% RT, lev 3x, pos 0.15):
- canonico (Donchian n=20, sl 2*ATR, max_bars 24, TP a centro canale): full Sharpe 7.49, OOS Sharpe 10.94, full DD 42%, OOS DD 18%, exposure 31%, 2392 trade, fee0.2% positivo.
- trend-filtrato (come sopra + salta se |close-EMA200|/ATR(14) > 3.0): full Sharpe 8.45, OOS Sharpe 11.96, full DD 25%, OOS DD 13%, exposure 16%, 1148 trade, fee0.2% positivo.
Il PROBLEMA di questo sleeve e' il DD/whipsaw (fada il trend al ribasso, poi shorta il rimbalzo). Un buon SOSTITUTO deve: battere OOS Sharpe ~12 E/O tagliare il full DD sotto ~20%, restando OOS positivo, sopravvivere a fee 0.20% RT, con la maggior parte degli anni 2021+ positivi. DD piu' basso a Sharpe comparabile e' GIA' valore. Anche un buon DIVERSIFICATORE (Sharpe decente + decorrelato, esposizione diversa) e' utile.`
const RECIPE = `Lavora in /opt/docker/PythagorasGoal. Testa UNA idea di strategia come candidata a SOSTITUIRE/MIGLIORARE MR02/ETH.
Usa l'harness ONESTO condiviso (no look-ahead, fee-aware, OOS). Scrivi UNO script self-contained in /tmp/cand_KEY.py (KEY = la tua key, univoca) ed eseguilo:
cd /opt/docker/PythagorasGoal && uv run python /tmp/cand_KEY.py
API (import from scripts.analysis.explore_lab):
- get_df(asset, tf) -> df [open,high,low,close,volume,timestamp,dt]. Usa asset="ETH", tf="1h" (puoi testare anche tf="4h").
- atr(df,n=14)->np.array ; ema(x,n)->np.array ; rsi(close,n=14)->np.array
- simulate(entries, df, fee_rt=0.001, lev=3.0, pos=0.15, split=-1) -> dict(trades,win,ret,dd,sharpe,yearly,exposure)
- evaluate(name, entries, df, fees=(0,0.0005,0.001,0.002)) -> dict(full,oos,sweep,sweep_oos,pos_yrs,n_yrs) e stampa una riga.
- robust(res)->bool.
Se la tua idea usa DVOL/funding/Hurst/fractali, puoi importare da scripts.analysis.regime_lab: load(asset,tf), regime_features(df), frac_features(df), load_features(asset,tf) (cache; vedi il file se serve). Se non sono disponibili, ripiega su feature calcolate dalle sole OHLCV. Se la tua idea e' shape/kNN, vedi scripts.analysis.shape_lab (analog_entries, analog_signals).
entries = list di {"i":int, "d":+1/-1, "max_bars":int, "tp":float|None, "sl":float|None}. L'ingresso esegue a close[i]. TP/SL sono intrabar al livello (SL valutato PRIMA del TP). split>0 nel simulate = solo entries con i>=split (OOS).
ANTI-LOOK-AHEAD (non negoziabile): direzione e prezzo alla barra i usano SOLO dati [0..i] (close[i] incluso). Mai usare high/low/close della barra i+ per decidere ingresso/direzione. Canali/rolling che devono escludere la barra corrente vanno .shift(1). Prezzo d'ingresso = close[i]. La famiglia squeeze e' stata scartata proprio per questo errore: NON ripeterlo.
${BASELINE}
Procedura: implementa l'idea, fai uno sweep PICCOLO (2-4 config di parametri), scegli la MIGLIORE per OOS Sharpe a parita' di: sopravvive a fee 0.20% RT e full DD non peggiore della baseline. Verifica l'assenza di look-ahead (perturba le barre future e controlla che gli entries non cambino, oppure argomenta perche' non c'e').
Restituisci SOLO il risultato strutturato della tua MIGLIORE config. Sii ONESTO: se non batte la baseline, riporta i numeri VERI (peggiori) — un risultato negativo e' informazione utile. NON inventare numeri: devono venire dall'esecuzione reale dello script.`
const SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['key','family','implemented','ran_ok','best_config','trades','exposure','full_sharpe','full_ret','full_dd','oos_sharpe','oos_ret','oos_dd','fee02_full','fee02_oos','pos_yrs','n_yrs','no_lookahead','beats_baseline','verdict','note'],
properties: {
key: { type: 'string' },
family: { type: 'string' },
implemented: { type: 'boolean' },
ran_ok: { type: 'boolean', description: 'lo script ha girato senza errori e i numeri vengono dall esecuzione reale' },
best_config: { type: 'string', description: 'parametri della config migliore' },
trades: { type: 'integer' },
exposure: { type: 'number', description: '% barre in posizione (full)' },
full_sharpe: { type: 'number' },
full_ret: { type: 'number', description: '% ritorno full' },
full_dd: { type: 'number', description: '% max drawdown full' },
oos_sharpe: { type: 'number' },
oos_ret: { type: 'number' },
oos_dd: { type: 'number' },
fee02_full: { type: 'number', description: 'ritorno % full a fee 0.20% RT' },
fee02_oos: { type: 'number', description: 'ritorno % OOS a fee 0.20% RT' },
pos_yrs: { type: 'integer', description: 'anni positivi' },
n_yrs: { type: 'integer' },
no_lookahead: { type: 'boolean' },
beats_baseline: { type: 'boolean', description: 'batte la baseline MR02/ETH trend-filtrata (OOS Sharpe ~12 e/o DD<20%)' },
verdict: { type: 'string', enum: ['substitute','improvement','diversifier','reject'] },
note: { type: 'string', description: '1-2 frasi: perche funziona o perche no, e l angolo distintivo' },
},
}
// ---------------------------------------------------------------- 112 idee distinte
// [key, family, idea]
const SPECS = [
// A. fade gated da regime (il fix del whipsaw)
['adx_gate_fade','gated-fade','Donchian/Bollinger fade su ETH ma SALTA quando ADX(14) e alto (regime trending). Calcola ADX causale; entra in fade solo se ADX<soglia (sweep 18/22/25). Tesi: evita di fadare i trend forti che fanno il whipsaw a MR02.'],
['kaufman_er_gate','gated-fade','Mean-reversion fade gated dal Kaufman Efficiency Ratio (|close-close[-n]| / somma|delta| su n barre). Fada solo se ER basso (mercato choppy/range), salta se ER alto (trending). Sweep n e soglia.'],
['hurst_gate_fade','gated-fade','Donchian fade ma entra solo se rolling-Hurst (causale, dalle close) < soglia anti-persistente (sweep 0.45/0.5/0.55). Usa frac_features da regime_lab se disponibile, altrimenti Hurst R/S manuale.'],
['varratio_gate_fade','gated-fade','Donchian fade gated dal variance-ratio (VR<1 = mean-reverting). Entra in fade solo quando VR(k) sotto soglia. Sweep k e soglia.'],
['rv_pct_gate_fade','gated-fade','Donchian fade ma salta quando la realized-vol percentile (rolling) e nel quartile alto: la vol estrema = crash/parabola dove la fade muore. Sweep finestra e soglia percentile.'],
['dvol_gate_fade','gated-fade','Bollinger/Donchian fade ETH gated dalla DVOL percentile (regime_lab.load+regime_features). Testa fade SOLO in DVOL bassa (calmo) vs alta. Riporta quale regime e migliore.'],
['vrp_gate_fade','gated-fade','Fade gated dal VRP (vol risk premium = dvol-rv) e dal livello DVOL. Test del finding FR01: edge su VRP<0 + DVOL bassa. Da regime_lab. Fada solo in quel regime.'],
['funding_gate_fade','gated-fade','Donchian fade gated dal funding (regime_lab): es. evita fade long quando funding molto negativo (panic). Sweep soglia funding_z.'],
['emaslope_gate_fade','gated-fade','Fade ma salta quando la pendenza di EMA200 (slope su k barre, normalizzata da ATR) e ripida: non fadare contro un trend strutturale. Sweep soglia slope.'],
['choppiness_gate_fade','gated-fade','Donchian fade gated dal Choppiness Index (alto=range, basso=trend). Entra solo se CI>soglia (range). Sweep finestra/soglia.'],
['rsq_gate_fade','gated-fade','Fade gated dall R^2 di una regressione lineare rolling sul log-prezzo: alto R^2 = trend pulito (salta), basso = range (fada). Sweep finestra/soglia.'],
['bbw_gate_fade','gated-fade','Bollinger fade gated dalla band-width (BBW): fada solo quando BBW in regime medio/alto ma non esplosivo. Sweep soglie BBW percentile.'],
['atr_pct_gate_fade','gated-fade','Donchian fade ma salta quando ATR-percentile rolling e estremo (>p80): la coda di vol e dove il whipsaw uccide. Sweep percentile.'],
['htf_align_fade','gated-fade','Fade ETH 1h ma NON contro il trend 4h: salta i long-fade se EMA20<EMA50 su 4h (downtrend) e i short-fade se uptrend 4h. Resample 4h dalla 1h. Fada solo allineato/neutro.'],
['asym_trenddist_fade','gated-fade','Donchian fade con soglie trend-distance ASIMMETRICHE long vs short, e cap piu stretto sul lato contro-trend HTF. Sweep coppie di soglie.'],
['btc_regime_fade','gated-fade','Fade ETH solo quando BTC e in regime range (BTC |close-EMA200|/ATR sotto soglia): se BTC sta trendando forte, anche ETH trenda e la fade muore. Trade ETH, gate su BTC.'],
// B. meccanismi mean-reversion alternativi
['rsi2_revert','mr-alt','RSI(2) di Connors: long quando RSI2<10, short quando RSI2>90, exit su RSI2 che torna a 50 o max_bars. Sweep soglie e max_bars su ETH 1h.'],
['rsi14_bands','mr-alt','RSI(14) revert con bande: long RSI<30, short RSI>70, TP al ritorno a 50 (proxy prezzo), SL ATR. Sweep.'],
['pctb_fade','mr-alt','Bollinger %B fade: long quando %B<0, short quando %B>1, TP a media, SL ATR. Sweep finestra/sigma/max_bars. Confronta con MR01.'],
['zscore_price_fade','mr-alt','Z-score del prezzo vs SMA(n): entra quando |z|>soglia contro il movimento, TP a z=0 (SMA), SL ATR. Sweep n/soglia. Versione simmetrica del DIP.'],
['zscore_ret_fade','mr-alt','Z-score del rendimento di barra (MR07-like) ma ri-tunato per ETH: |z|>soglia su finestra n, exit in multipli ATR. Sweep n/soglia/exit.'],
['vwap_revert','mr-alt','Reversion a VWAP rolling (volume-weighted): entra quando il prezzo devia >k*sigma dal VWAP, TP al VWAP. Sweep finestra/k.'],
['keltner_fade_eth','mr-alt','Keltner channel fade ri-tunato per ETH (EMA +/- m*ATR): fada la rottura della banda Keltner, TP a EMA. Sweep m/finestra. MR03 era debole su BTC ma ETH puo differire.'],
['cci_revert','mr-alt','CCI(n) extreme revert: long CCI<-100, short CCI>+100, exit su CCI->0 o max_bars. Sweep.'],
['williamsr_revert','mr-alt','Williams %R revert: long %R<-80, short %R>-20, TP a media canale, SL ATR. Sweep.'],
['stoch_revert','mr-alt','Stochastic %K/%D extreme revert su ETH: long quando %K<20 e cross-up, short %K>80 cross-down. Sweep finestre.'],
['connors_rsi','mr-alt','Connors RSI (RSI3 del prezzo + RSI2 della streak + percentrank del rendimento) extreme revert. Sweep soglie.'],
['anchored_vwap_revert','mr-alt','Reversion a VWAP ancorato all ultimo swing-low/high (Williams): entra quando il prezzo si allontana >k*ATR dall AVWAP, TP all AVWAP.'],
['median_z_fade','mr-alt','Z robusto: (close - rolling median) / rolling MAD. Fada |z|>soglia, TP a mediana. Piu robusto agli outlier della SMA-z. Sweep.'],
['pctrank_fade','mr-alt','Percent-rank del close nella finestra rolling: long se rank<0.05, short se rank>0.95, TP al rank 0.5 (proxy). Sweep finestra.'],
['double_bollinger','mr-alt','Doppia Bollinger: entra fade alla banda esterna (2.5-3 sigma), esce alla banda interna (1 sigma) invece che alla media. Sweep sigma esterna/interna.'],
['fade_to_ema','mr-alt','Donchian/Bollinger fade ma TP all EMA(n) invece che SMA/centro-canale (target dinamico che segue il trend). Sweep n EMA.'],
['nbar_lowhigh_rsi','mr-alt','Entra long su nuovo minimo a N barre CON RSI<35 (doppia conferma di esaurimento), short speculare. TP a media, SL ATR. Sweep N.'],
['overshoot_swing_fade','mr-alt','Fada quando il prezzo supera lo swing-high/low precedente (Williams) di k*ATR e poi la barra chiude rientrando: false-breakout fade. Sweep k.'],
// C. exit migliori sulla Donchian fade (stesso ingresso)
['tight_sl_confirm','exit','Donchian fade con SL piu stretto (1.0/1.5 ATR) ma close-confirm (esci solo se il close sfonda SL-0.5ATR). Replica EXIT-16. Sweep buffer.'],
['fast_tp_partial','exit','Donchian fade con TP a frazione del canale (0.25/0.5 width) invece che al centro: incassa prima il rimbalzo, riduce il tempo a rischio. Sweep frazione.'],
['short_timestop','exit','Donchian fade con time-stop corto (max_bars 6/8/12 invece di 24): esce prima che il whipsaw maturi. Sweep max_bars.'],
['atr_trailing_exit','exit','Donchian fade con uscita a ATR-trailing dopo l ingresso (trail il massimo favorevole di k*ATR). Sweep k. (la ricerca dice che il trailing spesso fallisce: verifica onestamente).'],
['breakeven_stop','exit','Donchian fade: dopo che il trade va in utile di +x*ATR, sposta lo stop a breakeven. Sweep x.'],
['rsi_exit','exit','Donchian fade con exit quando RSI torna oltre 50 (mean ripristinata) invece del TP fisso. Sweep.'],
['return_to_ema_exit','exit','Donchian fade con exit quando il prezzo ri-tocca l EMA(50) (ritorno alla media mobile) o max_bars. Sweep EMA.'],
['volscaled_maxbars','exit','Donchian fade con max_bars scalato sulla vol: meno barre in alta-vol, piu in bassa-vol. Sweep mapping.'],
['chandelier_exit','exit','Donchian fade con Chandelier exit (max favorevole - k*ATR). Sweep k.'],
['tp_opposite_band','exit','Donchian fade con TP alla banda OPPOSTA (reversione completa del canale) invece del centro. Piu ambizioso, meno frequente. Sweep.'],
// D. trend-following su ETH (prende cio che la fade perde)
['ema_cross_4h','trend','EMA20/EMA100 cross su ETH 4h, long quando EMA20>EMA100, flat altrimenti (TR01-style su ETH solo). Niente fade. Valuta come diversificatore direzionale.'],
['donchian_breakout','trend','Donchian BREAKOUT trend (opposto del fade): long su rottura del massimo a N barre, stop sotto il minimo. La ricerca dice che i breakout crypto rientrano: verifica ONESTAMENTE se su ETH regge netto fee.'],
['macd_trend','trend','MACD(12,26,9) trend-following su ETH 1h/4h: long su cross rialzista sopra zero, exit su cross opposto. Sweep TF.'],
['dual_ma_atr','trend','Dual-MA (fast/slow) con filtro ATR sul gap: entra trend solo se separazione MA > k*ATR (evita whipsaw delle MA). Sweep.'],
['supertrend','trend','Supertrend (ATR band) su ETH: segui il flip. Sweep multiplier/periodo.'],
['momentum_sign','trend','Momentum semplice: long se rendimento a H barre > 0 (12h/24h/48h), tieni H barre. Time-series momentum su ETH. Sweep H.'],
['tsmom_eth','trend','TSMOM multi-orizzonte (3/6/12 mesi sul daily resample di ETH) con risk-off se sotto SMA100. Solo ETH. Valuta come diversificatore.'],
['breakout_vol_confirm','trend','Channel breakout con conferma di volume (rottura valida solo se volume>media*k). Sweep.'],
['heikin_trend','trend','Heikin-Ashi trend: long su serie di candele HA verdi, exit su prima rossa. Costruisci HA causale. Sweep.'],
['triple_screen','trend','Triple-screen: trend 4h (EMA) definisce il bias, ingresso 1h su pullback nella direzione del trend (no contro-trend). Sweep.'],
// E. volatilita
['squeeze_expansion','vol','Bollinger-squeeze -> espansione, ma ingresso ONESTO: entra solo DOPO che la barra di espansione e CHIUSA, nella sua direzione, a close[i] (non anticipare). Verifica se l edge sparisce come per la famiglia squeeze scartata.'],
['atr_breakout','vol','ATR breakout: entra quando |close-close[-1]|>k*ATR nella direzione del movimento (continuazione) OPPURE contro (reversione) — testa entrambe e riporta quale regge. Sweep k.'],
['volofvol_revert','vol','Mean-reversion della vol-of-vol: quando la realized-vol fa uno spike estremo (z alto), fada il prezzo (gli spike di vol = capitolazione che rimbalza). Sweep.'],
['or_breakout_daily','vol','Opening-range breakout: definisci il range delle prime k ore UTC del giorno, entra sulla rottura con SL all altro lato. Sweep k. Trade ETH 1h.'],
['rv_regime_switch','vol','Switch fade/trend in base al regime di realized-vol: fade in bassa-vol (range), trend-follow in alta-vol. Una strategia adattiva. Sweep soglia.'],
['keltner_squeeze','vol','Keltner-inside-Bollinger squeeze: rileva la compressione, poi entra sulla direzione della prima rottura confermata (close oltre la banda). Onesto. Sweep.'],
// F. cross-asset / relative
['btc_leadlag','cross','BTC lead-lag: il rendimento di BTC alla barra i predice ETH alla barra i+1. Entra ETH a close[i] nella direzione del segnale BTC (solo dati fino a i). Sweep soglia/horizon.'],
['ethbtc_ratio_revert','cross','Reversione del ratio ETH/BTC: quando il log-ratio devia >z*sigma dalla sua media, prendi posizione DIREZIONALE su ETH (long ETH se ratio depresso). Sweep n/z.'],
['eth_beta_residual','cross','Residuo beta: regredisci ETH su BTC (beta rolling causale), fada il residuo (ETH ricco/povero vs BTC). Posizione direzionale ETH. Sweep finestra.'],
['btc_trend_eth_fade','cross','Fade ETH SOLO quando BTC e in range (gia simile a btc_regime_fade ma qui il gate e la pendenza EMA di BTC + la sua DVOL). Combina due gate BTC.'],
['eth_vs_basket_mom','cross','Cross-sectional: ETH momentum relativo vs basket (BTC,BNB,SOL,XRP). Long ETH se sovra-performa il basket di recente (continuazione) o sotto-performa (reversione) — testa entrambe.'],
['funding_spread','cross','Spread di funding ETH-BTC (regime_lab): quando il funding di ETH e estremo vs BTC, prendi posizione di reversione su ETH. Sweep.'],
// G. ML / shape
['sh01_logit_eth','ml','LogisticRegression stile SH01 su ETH: 17 feature di forma (body/shadow, rendimenti, pendenza, curvatura, pos max/min, RSI, estensione) su W24, predice segno a H12, walk-forward causale, entra se proba>0.58. Riusa scripts.analysis.shape_ml_research se possibile. ETH-specifico.'],
['analog_knn_eth','ml','Analog kNN sulla forma (shape_lab.analog_entries) su ETH: W/H/K sweep stretto, agree>0.6. Causale (KDTree ribuilt). Valuta come diversificatore.'],
['gbm_features_eth','ml','GradientBoostingClassifier walk-forward su feature ingegnerizzate (RSI, ATR-norm returns, distance-from-EMA, vol, Donchian-pos) per predire il segno a H barre. Train solo sul passato. Sweep H/threshold.'],
['logit_regime_mom','ml','LogReg walk-forward su feature di regime+momentum (DVOL, funding, ER, rendimenti multi-h) per predire direzione ETH a H. Da regime_lab. Sweep.'],
['rf_direction','ml','RandomForest walk-forward direzione ETH su feature tecniche. Train espandente. Sweep n_estimators/H/threshold. Onesto (no leakage temporale).'],
['ridge_return_sign','ml','Ridge regression walk-forward che prevede il rendimento a H barre; entra sul segno se |pred|>soglia. Feature: lag returns, RSI, ATR, distance-EMA. Sweep.'],
// H. microstruttura / calendario
['intraday_seasonality','micro','Stagionalita oraria: trova le ore UTC con edge di reversione/continuazione su ETH (split-half per evitare overfit), trada solo quelle. Onesto su OOS.'],
['gap_fade','micro','Gap fade: quando una barra apre/chiude con un gap >k*ATR vs la precedente, fada il gap (reversione). Sweep k.'],
['hourofday_fade','micro','Fade Donchian condizionato all ora del giorno: attiva la fade solo nelle ore storicamente mean-reverting. Verifica OOS (rischio overfit alto: sii severo).'],
['dayofweek','micro','Effetto giorno-della-settimana su ETH: posizioni condizionate al weekday (es. weekend illiquido). Severo su OOS (probabile rumore).'],
['funding_settlement','micro','Timing settlement funding (00/08/16 UTC): fade del movimento attorno al settlement (over-reaction pre/post funding). Sweep finestra.'],
['large_candle_reversal','micro','Reversione dopo candela estesa: se |close-open|>k*ATR (barra-shock), entra contro alla chiusura della barra successiva. Sweep k.'],
['three_bar_reversal','micro','Pattern 3-barre: 3 chiusure consecutive nello stesso verso poi entra contro (esaurimento). Sweep n-barre/soglia.'],
['consecutive_exhaustion','micro','N candele rosse consecutive -> long (e speculare). Conta le streak causalmente, entra all N-esima. Sweep N. TP a media, SL ATR.'],
// I. combinazioni / ensemble
['donchian_rsi_confirm','combo','Donchian fade CON conferma RSI (long solo se RSI<40 oltre alla rottura del canale): doppia conferma per ridurre i falsi segnali nei trend.'],
['bb_donchian_confluence','combo','Confluenza Bollinger + Donchian: fada solo quando ENTRAMBE le bande sono rotte simultaneamente (segnale piu forte, meno frequente, meno whipsaw).'],
['volspike_fade','combo','Fade solo su spike di volume (volume>media*k): la capitolazione su volume rimbalza meglio del drift silenzioso. Sweep k.'],
['funding_tailwind_fade','combo','Donchian fade con vento di funding: fada gli short (entra long) solo se funding positivo (i long pagano = pressione di unwind ribassista esaurita) e viceversa. Da regime_lab.'],
['regime_switch_classifier','combo','Classificatore di regime (trend vs range via ADX/ER/Hurst) che decide SE fadare o trend-followare ad ogni barra: una sola strategia adattiva. Sweep soglie del classificatore.'],
['vote_ensemble_mr','combo','Ensemble a voto di 3 segnali MR indipendenti (Bollinger, Donchian, z-return): entra solo se >=2 concordano sulla direzione. Riduce i falsi.'],
['adaptive_donchian','combo','Donchian a lookback ADATTIVO: n scalato sulla vol (canale piu lungo in alta-vol, piu corto in bassa-vol). Sweep mapping vol->n.'],
// J. statistico-robusto
['ou_halflife','stat','Ornstein-Uhlenbeck: stima half-life e media di reversione (rolling, causale), entra quando la deviazione dalla media OU supera z*sigma, exit a half-life. Sweep.'],
['kalman_meanrev','stat','Kalman filter sul prezzo (livello+trend) come media dinamica; fada la deviazione dal livello stimato. Causale (filtro one-pass). Sweep Q/R.'],
['quantile_band_fade','stat','Bande a quantili rolling (5/95 percentile su finestra n) invece di sigma: fada la rottura del quantile, TP al quantile 50. Piu robusto a code fat. Sweep n/quantili.'],
['theilsen_residual','stat','Detrend Theil-Sen rolling (pendenza robusta), fada il residuo quando estremo. Sweep finestra.'],
['hampel_outlier_fade','stat','Hampel filter: identifica gli outlier (|x-median|>k*MAD) e fada l outlier (reversione). Sweep k/finestra.'],
['rollreg_residual_z','stat','Regressione lineare rolling (prezzo~tempo), z del residuo; fada |z|>soglia, TP a residuo 0. Sweep finestra/soglia.'],
['dominant_cycle_revert','stat','Stima del ciclo dominante (autocorrelazione/zero-cross rolling), entra in reversione in fase con il ciclo. Causale. Sweep.'],
// K. ulteriori gate/varianti per arrivare a 112
['choppiness_donchian','gated-fade','Choppiness Index + Donchian: fada solo se CI>61.8 (range conclamato). Variante severa di choppiness_gate_fade con soglia fissa Fibonacci. Sweep secondario su n.'],
['er_bollinger','gated-fade','Efficiency-Ratio gate su Bollinger fade (non Donchian): fada la banda solo se ER<soglia. Confronta con kaufman_er_gate (li su Donchian).'],
['adx_rsi2','combo','ADX<20 (range) come gate per un RSI(2) extreme-revert: solo MR in regime non-trending. Sweep.'],
['hurst045_only','gated-fade','Fade attiva SOLO quando rolling-Hurst<0.45 (forte anti-persistenza): regime ultra-mean-reverting. Pochi trade, alta qualita? Verifica esposizione/OOS.'],
['dvol_low_fade','gated-fade','Fade ETH solo quando DVOL < mediana (regime calmo): la calma e dove la reversione paga e il whipsaw e raro. Da regime_lab. Sweep soglia.'],
['vrp_neg_dvol_low','gated-fade','Replica diretta del finding FR01 su ETH: fade attiva su VRP<0 E DVOL bassa. Da regime_lab. Misura Sharpe/DD/esposizione e decorrelazione.'],
['funding_extreme_fade','gated-fade','Carry/funding reversion: quando il funding_z e estremo (>|2|), fada il prezzo nella direzione dell unwind atteso. Da regime_lab. Sweep.'],
['range_day_fade','gated-fade','Filtro range-day: fada solo se il range della sessione corrente (rolling 24h) < media*soglia (giornata compressa = reversione affidabile). Sweep.'],
['mtf_donchian','combo','Donchian multi-timeframe: ingresso su rottura del canale 1h ma con canale di riferimento 4h (TP al centro del canale 4h). Sweep n1/n4.'],
['volweighted_fade','combo','Fade pesata dal volume: ingresso solo se il volume della barra di segnale e nel quartile alto (conferma di partecipazione). Sweep.'],
['false_break_fade','mr-alt','Fade del falso breakout: il prezzo rompe il canale Donchian ma la barra SUCCESSIVA chiude di nuovo dentro -> entra contro la rottura a quella chiusura. Onesto. Sweep n.'],
['band_walk_exhaustion','mr-alt','Esaurimento del band-walk: dopo k chiusure consecutive fuori dalla banda Bollinger (band walking), fada il rientro alla prima chiusura dentro. Sweep k/sigma.'],
['pivot_revert','stat','Reversione ai pivot floor classici (P, R1/R2, S1/S2 calcolati sul giorno precedente): fada il tocco di R2/S2 verso P. Causale (pivot da ieri). Sweep.'],
['fractal_swing_revert','stat','Reversione agli swing di Williams (frattali a 5 barre confermati con ritardo): fada il tocco di un nuovo frattale verso la media. Causale (conferma ritardata). Sweep.'],
['lrc_fade','stat','Linear Regression Channel: canale di regressione rolling +/- 2 std del residuo; fada il tocco dei bordi verso la linea centrale. Sweep finestra.'],
]
// ---------------------------------------------------------------- esecuzione
phase('Generate')
log(`Lancio ${SPECS.length} agenti, una strategia ETH distinta ciascuno (harness onesto fee-aware OOS)`)
const results = await parallel(SPECS.map(([key, family, idea]) => () =>
agent(
`${RECIPE}\n\n========\nLA TUA IDEA — key="${key}", family="${family}":\n${idea}\n\nUsa /tmp/cand_${key}.py come file. Restituisci il risultato strutturato (campi key e family valorizzati con i tuoi).`,
{ label: `gen:${key}`, phase: 'Generate', schema: SCHEMA }
).then(r => r ? { ...r, key, family } : null)
))
const ran = results.filter(Boolean)
log(`${ran.length}/${SPECS.length} agenti rientrati. Filtro i survivor per la verifica avversariale.`)
// survivor: ha girato davvero, e o batte la baseline o e' chiaramente interessante
// (OOS Sharpe alto, oppure DD ben piu basso a Sharpe decente), e regge la fee 0.2%.
const survivors = ran.filter(r =>
r.ran_ok && r.implemented && r.no_lookahead &&
r.full_ret > 0 && r.oos_ret > 0 && r.fee02_oos > 0 && r.fee02_full > 0 &&
(
r.beats_baseline ||
r.oos_sharpe >= 11.0 ||
(r.full_dd <= 20.0 && r.oos_sharpe >= 8.0) ||
(r.full_dd <= 12.0 && r.oos_sharpe >= 5.0) // diversificatore a basso DD
)
)
// ordina per una metrica composita: premia OOS Sharpe e penalizza DD
const score = r => r.oos_sharpe - 0.10 * r.full_dd
survivors.sort((a, b) => score(b) - score(a))
const toVerify = survivors.slice(0, 24) // cap la verifica ai 24 migliori
log(`${survivors.length} survivor; verifico avversarialmente i top ${toVerify.length}`)
phase('Verify')
const VSCHEMA = {
type: 'object', additionalProperties: false,
required: ['key','confirmed','reproduced','lookahead_found','overfit_risk','full_sharpe','full_dd','oos_sharpe','oos_dd','beats_mr02eth','note'],
properties: {
key: { type: 'string' },
confirmed: { type: 'boolean', description: 'edge reale, eseguibile, fee-robusto, dopo audit indipendente' },
reproduced: { type: 'boolean', description: 'ho ri-eseguito e ho ottenuto numeri coerenti (entro ~15%)' },
lookahead_found: { type: 'boolean' },
overfit_risk: { type: 'string', enum: ['low','med','high'] },
full_sharpe: { type: 'number' },
full_dd: { type: 'number' },
oos_sharpe: { type: 'number' },
oos_dd: { type: 'number' },
beats_mr02eth: { type: 'boolean', description: 'batte davvero MR02/ETH (OOS Sharpe>=~12 a DD<=25, o DD<20 a Sharpe comparabile)' },
note: { type: 'string' },
},
}
const verified = await parallel(toVerify.map(r => () =>
agent(
`Verifica AVVERSARIALE di una strategia candidata a sostituire MR02/ETH. Lavora in /opt/docker/PythagorasGoal con l'harness onesto scripts.analysis.explore_lab (get_df, simulate, evaluate, robust, atr, ema, rsi). Default: il tuo compito e' SMENTIRE.\n\n${BASELINE}\n\nCandidata key="${r.key}" (family ${r.family}).\nConfig dichiarata: ${r.best_config}\nNumeri dichiarati: full Sharpe ${r.full_sharpe}, OOS Sharpe ${r.oos_sharpe}, full DD ${r.full_dd}%, OOS DD ${r.oos_dd}%, trade ${r.trades}, exposure ${r.exposure}%, fee0.2% full ${r.fee02_full}, fee0.2% OOS ${r.fee02_oos}.\nDescrizione idea: la trovi rifacendola da zero.\n\nRI-IMPLEMENTA tu stesso la strategia (in /tmp/verify_${r.key}.py) sulla SUA config dichiarata, eseguila sull'harness, e controlla: (1) LOOK-AHEAD — l'ingresso/direzione usa solo dati fino a close[i]? Perturba le barre future e verifica che gli entries non cambino. (2) RIPRODUCIBILITA — i numeri tornano entro ~15%? (3) ROBUSTEZZA/OVERFIT — regge a fee 0.20% RT, a piccole variazioni dei parametri (plateau, non picco isolato), ed e' positiva nella maggior parte degli anni 2021+? (4) batte DAVVERO MR02/ETH (OOS Sharpe e/o DD)?\n\nSe non riesci a riprodurre o trovi look-ahead, confirmed=false. Riporta i TUOI numeri ri-eseguiti, non quelli dichiarati. Sii severo: meglio bocciare un falso positivo che promuovere un artefatto.`,
{ label: `verify:${r.key}`, phase: 'Verify', schema: VSCHEMA }
).then(v => v ? { ...v, key: r.key, family: r.family, gen: r } : null)
))
const vok = verified.filter(Boolean)
const confirmed = vok.filter(v => v.confirmed && !v.lookahead_found && v.beats_mr02eth)
confirmed.sort((a, b) => (b.oos_sharpe - 0.1 * b.full_dd) - (a.oos_sharpe - 0.1 * a.full_dd))
log(`Verifica completata: ${confirmed.length} confermati che battono MR02/ETH`)
return {
n_specs: SPECS.length,
n_ran: ran.length,
n_survivors: survivors.length,
n_verified: vok.length,
n_confirmed: confirmed.length,
confirmed,
all_survivors: survivors.map(s => ({ key: s.key, family: s.family, best_config: s.best_config,
full_sharpe: s.full_sharpe, full_dd: s.full_dd, oos_sharpe: s.oos_sharpe, oos_dd: s.oos_dd,
exposure: s.exposure, trades: s.trades, fee02_oos: s.fee02_oos, verdict: s.verdict, note: s.note })),
verified: vok.map(v => ({ key: v.key, confirmed: v.confirmed, reproduced: v.reproduced,
lookahead_found: v.lookahead_found, overfit_risk: v.overfit_risk, beats_mr02eth: v.beats_mr02eth,
full_sharpe: v.full_sharpe, full_dd: v.full_dd, oos_sharpe: v.oos_sharpe, oos_dd: v.oos_dd, note: v.note })),
}
+250
View File
@@ -0,0 +1,250 @@
"""Harness ONESTO per overlay di OPZIONI (protezione) sulle strategie ETH.
Vincolo noto (analisi ARGO/GEX 2026-06-01): lo storico per-strike dell'OI/prezzi
opzioni NON e' gratuito -> non backtestabile. MA la DVOL di Deribit (indice di vol
implicita ETH, annualizzato %) e' storica e gratuita in data/regime/eth_dvol.parquet
(oraria 2021-03 -> 2026-06). Quindi prezziamo le opzioni SINTETICAMENTE con
Black-Scholes(spot, DVOL) -> backtest del COSTO dell'overlay (premio) vs il payoff.
Approssimazione dichiarata (e DELIBERATAMENTE conservativa, bias CONTRO le opzioni):
- niente skew: i put reali costano piu' dell'ATM-IV -> applichiamo `skew_mult>=1`
alla sigma dei put (rincara il premio).
- all'uscita anticipata l'opzione vale solo l'INTRINSECO (nessun rebate di time-
value) -> sotto-stima il beneficio dell'hedge.
- niente costi di fill/liquidita' delle opzioni oltre lo skew_mult.
Se sotto queste ipotesi pessimistiche l'overlay MIGLIORA il rischio/rendimento,
l'edge e' robusto. I numeri sono INDICATIVI, non eseguibili come i perp.
Convenzione coerente con explore_lab: ret per-notional, lev 3x, pos 0.15, fee 0.10% RT.
L'opzione copre lo STESSO notional della posizione (lev*pos*cap), quindi il suo P&L
come frazione entra in ret' = ret_base + lev*(payoff - premio)/entry.
"""
from __future__ import annotations
import math
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.explore_lab import get_df, atr, ema, rsi, FEE_RT, LEV, POS, OOS_FRAC # noqa
HOURS_YEAR = 24 * 365.0
# --------------------------- Black-Scholes (r=0) ---------------------------
def _ncdf(x: float) -> float:
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
def bs_put(S: float, K: float, T: float, sigma: float) -> float:
"""Prezzo put europea, r=0. T in anni, sigma annualizzata (frazione)."""
if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
return max(K - S, 0.0)
sd = sigma * math.sqrt(T)
d1 = (math.log(S / K) + 0.5 * sigma * sigma * T) / sd
d2 = d1 - sd
return K * _ncdf(-d2) - S * _ncdf(-d1)
def bs_call(S: float, K: float, T: float, sigma: float) -> float:
if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
return max(S - K, 0.0)
sd = sigma * math.sqrt(T)
d1 = (math.log(S / K) + 0.5 * sigma * sigma * T) / sd
d2 = d1 - sd
return S * _ncdf(d1) - K * _ncdf(d2)
# --------------------------- DVOL allineata (causale) ---------------------------
def dvol_for(df: pd.DataFrame, asset: str = "ETH") -> np.ndarray:
"""DVOL (annualizzata, FRAZIONE es. 0.70) allineata causalmente ai bar di df.
merge_asof backward: ogni bar riceve l'ultima DVOL con ts <= bar. NaN -> ffill,
eventuale buco iniziale (pre-2021-03) -> bfill della prima disponibile."""
dv = pd.read_parquet(PROJECT_ROOT / "data" / "regime" / f"{asset.lower()}_dvol.parquet")
dv = dv[["timestamp", "dvol"]].sort_values("timestamp")
base = pd.DataFrame({"timestamp": df["timestamp"].values}).sort_values("timestamp")
m = pd.merge_asof(base, dv, on="timestamp", direction="backward")
s = (m["dvol"].values.astype(float)) / 100.0 # da % a frazione
s = pd.Series(s).ffill().bfill().values
return s
# --------------------------- engine con overlay opzione ---------------------------
def simulate_hedged(entries: list[dict], df: pd.DataFrame, dvol: np.ndarray | None = None,
fee_rt: float = FEE_RT, lev: float = LEV, pos: float = POS, split: int = -1,
hedge: str = "none", otm: float = 0.05, otm2: float = 0.15,
skew_mult: float = 1.10, tenor_mult: float = 1.0,
hedge_side: str = "both", min_dvol: float = 0.0) -> dict:
"""Come explore_lab.simulate ma con un overlay di opzione per-trade.
hedge:
"none" -> nessun overlay (identico a explore_lab.simulate)
"put" -> compra protezione direzionale: put se long, call se short (floor a otm)
"put_spread"-> debit spread: long protezione a otm, short a otm2 (piu' lontano) -> piu' economico, cap del payoff
"collar" -> protezione a otm finanziata vendendo l'opzione opposta OTM a otm2 (riduce/azzera il premio, cappa il guadagno)
otm: moneyness della protezione (frazione, es 0.05 = 5% OTM)
otm2: secondo strike (per put_spread / collar)
skew_mult: rincaro della sigma sull'opzione COMPRATA (conservativo)
tenor_mult: scadenza = max_bars*tenor_mult ore (>=1: copre almeno l'orizzonte)
hedge_side: "both" | "long" (solo i long-fade, le prese-coltello) | "short"
min_dvol: copri solo se DVOL all'ingresso >= soglia (frazione) -> hedge selettivo
"""
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
if dvol is None:
dvol = np.full(n, 0.7)
cap = peak = 1000.0
max_dd = 0.0
fee = fee_rt * lev
trades = wins = 0
last_exit = -1
bars_in = 0
yearly: dict[int, float] = {}
rets: list[float] = []
prem_paid = 0.0
pay_recv = 0.0
for e in entries:
i, d = e["i"], e["d"]
if i <= last_exit or i + 1 >= n or i < split:
continue
entry = c[i]
tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]
j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_sl:
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if k == mb:
exit_p = c[j]
base_ret = (exit_p - entry) / entry * d * lev - fee
# ---- overlay opzione ----
opt_ret = 0.0
do_hedge = hedge != "none" and (
hedge_side == "both"
or (hedge_side == "long" and d == 1)
or (hedge_side == "short" and d == -1)
) and dvol[i] >= min_dvol
if do_hedge:
T = max(mb * tenor_mult, 1.0) / HOURS_YEAR
sig = dvol[i]
sig_buy = sig * skew_mult
if d == 1: # posizione long -> rischio sotto -> PUT (floor)
Kp = entry * (1.0 - otm)
prem = bs_put(entry, Kp, T, sig_buy) / entry
payoff = max(Kp - exit_p, 0.0) / entry
if hedge == "put_spread":
Kp2 = entry * (1.0 - otm2)
prem -= bs_put(entry, Kp2, T, sig) / entry # vendo la coda piu' lontana
payoff -= max(Kp2 - exit_p, 0.0) / entry
elif hedge == "collar":
Kc = entry * (1.0 + otm2) # vendo call OTM per finanziare
prem -= bs_call(entry, Kc, T, sig) / entry
payoff -= max(exit_p - Kc, 0.0) / entry
else: # posizione short -> rischio sopra -> CALL (cap)
Kc = entry * (1.0 + otm)
prem = bs_call(entry, Kc, T, sig_buy) / entry
payoff = max(exit_p - Kc, 0.0) / entry
if hedge == "put_spread":
Kc2 = entry * (1.0 + otm2)
prem -= bs_call(entry, Kc2, T, sig) / entry
payoff -= max(exit_p - Kc2, 0.0) / entry
elif hedge == "collar":
Kp = entry * (1.0 - otm2)
prem -= bs_put(entry, Kp, T, sig) / entry
payoff -= max(Kp - exit_p, 0.0) / entry
opt_ret = lev * (payoff - prem)
prem_paid += lev * prem
pay_recv += lev * payoff
ret = base_ret + opt_ret
cb = cap
cap = max(cb + cb * pos * ret, 10.0)
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
trades += 1; wins += ret > 0; bars_in += (j - i)
last_exit = j
rets.append(ret * pos)
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets))) if len(rets) > 1 and np.std(rets) > 0 else 0.0
return {
"trades": trades, "win": wins / trades * 100 if trades else 0.0,
"ret": (cap / 1000 - 1) * 100, "dd": max_dd * 100, "sharpe": sharpe,
"yearly": yearly, "exposure": bars_in / n * 100 if n else 0.0,
"prem_paid_pct": prem_paid * 100, "pay_recv_pct": pay_recv * 100,
}
def evaluate_hedged(name: str, entries: list[dict], df: pd.DataFrame, dvol: np.ndarray,
fees=(0.0, 0.001, 0.002), **hcfg) -> dict:
"""FULL + OOS + sweep fee per una config di overlay. Stampa una riga."""
split = int(len(df) * (1 - OOS_FRAC))
full = simulate_hedged(entries, df, dvol, **hcfg)
oos = simulate_hedged(entries, df, dvol, split=split, **hcfg)
sweep = {f: simulate_hedged(entries, df, dvol, fee_rt=f, **hcfg)["ret"] for f in fees}
sweep_oos = {f: simulate_hedged(entries, df, dvol, fee_rt=f, split=split, **hcfg)["ret"] for f in fees}
yrs = full["yearly"]; pos_yrs = sum(1 for v in yrs.values() if v > 0)
print(f" {name:<26s} trd={full['trades']:>5d} win={full['win']:>4.1f}% "
f"FULL={full['ret']:>+8.0f}% OOS={oos['ret']:>+7.0f}% DD={full['dd']:>4.0f}% "
f"oDD={oos['dd']:>4.0f}% Shrp={full['sharpe']:>4.2f} exp={full['exposure']:>4.1f}% "
f"prem={full['prem_paid_pct']:>5.0f}% pay={full['pay_recv_pct']:>5.0f}% "
f"anniPos={pos_yrs}/{len(yrs)} | fee0.2%: FULL={sweep[0.002]:>+7.0f} OOS={sweep_oos[0.002]:>+6.0f}")
return {"full": full, "oos": oos, "sweep": sweep, "sweep_oos": sweep_oos,
"pos_yrs": pos_yrs, "n_yrs": len(yrs)}
# --------------------------- baseline: Donchian fade ETH ---------------------------
def donchian_fade(df, n=20, sl_atr=2.0, max_bars=24, trend_max=3.0, ema_long=200, use_sl=True):
"""Le entries della MR02/ETH (per testarci sopra gli overlay)."""
h, l, c = df["high"].values, df["low"].values, df["close"].values
a = atr(df, 14)
hh = pd.Series(h).rolling(n).max().shift(1).values
ll = pd.Series(l).rolling(n).min().shift(1).values
em = ema(c, ema_long)
ents = []
for i in range(max(n, ema_long, 14) + 1, len(c)):
if np.isnan(hh[i]) or np.isnan(ll[i]) or np.isnan(a[i]):
continue
if trend_max is not None and not np.isnan(em[i]) and a[i] > 0:
if abs(c[i] - em[i]) / a[i] > trend_max:
continue
tp = (hh[i] + ll[i]) / 2.0
if c[i] < ll[i] and c[i - 1] >= ll[i - 1]:
ents.append({"i": i, "d": 1, "tp": tp, "sl": (c[i] - sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
elif c[i] > hh[i] and c[i - 1] <= hh[i - 1]:
ents.append({"i": i, "d": -1, "tp": tp, "sl": (c[i] + sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
return ents
if __name__ == "__main__":
df = get_df("ETH", "1h")
dv = dvol_for(df, "ETH")
print(f"ETH 1h {len(df)} bar | DVOL media {np.nanmean(dv)*100:.0f}% (min {np.nanmin(dv)*100:.0f} max {np.nanmax(dv)*100:.0f})")
ents = donchian_fade(df) # MR02/ETH trend-filtrato, con SL ATR
ents_nosl = donchian_fade(df, use_sl=False) # senza SL (per testare l'opzione COME stop)
print("\n=== riferimento: MR02/ETH baseline vs overlay opzione (premio dedotto, conservativo) ===")
evaluate_hedged("baseline SL-ATR (no opt)", ents, df, dv, hedge="none")
evaluate_hedged("no-SL (no opt)", ents_nosl, df, dv, hedge="none")
print(" -- opzione COME floor al posto dello stop (no-SL + put/call OTM) --")
for otm in (0.03, 0.05, 0.08):
evaluate_hedged(f"no-SL +put {int(otm*100)}%OTM", ents_nosl, df, dv, hedge="put", otm=otm, hedge_side="both")
print(" -- put-spread / collar (piu' economici) sul no-SL --")
evaluate_hedged("no-SL +put_spread 5/15", ents_nosl, df, dv, hedge="put_spread", otm=0.05, otm2=0.15)
evaluate_hedged("no-SL +collar 5/10", ents_nosl, df, dv, hedge="collar", otm=0.05, otm2=0.10)
print(" -- hedge SELETTIVO: solo long-fade (prese-coltello) in alta DVOL --")
evaluate_hedged("SL +put long-only DVOL>0.8", ents, df, dv, hedge="put", otm=0.05, hedge_side="long", min_dvol=0.8)
+166
View File
@@ -0,0 +1,166 @@
"""Loader + lookup PREZZI REALI della catena opzioni (da cerbero-bite, data/options/).
Fonte: scripts/analysis/options_fetcher.py importa lo storico per-strike reale di
cerbero-bite (bid/ask/mid/IV/greche/OI/vol, BTC+ETH, dal 2026-05-01, cadenza ~12min)
in data/options/{eth,btc}_chain.parquet.
NB granularita': cerbero-bite snapshotta una FETTA ROTANTE della catena (~1 scadenza
per volta). Quindi:
- gli AGGREGATI (curva di skew, livelli di premio per moneyness/tenor) sono ROBUSTI
e sono l'uso principale -> skew_curve(), premium_levels();
- il lookup per-trade quote() e' BEST-EFFORT con una finestra di staleness
(default 48h) e ritorna il flag staleness_h: un backtest per-trade preciso e'
limitato dalla sparsita', usalo con cautela.
Uso:
from scripts.analysis.options_chain import OptionChain
oc = OptionChain("ETH")
oc.premium_levels() # tabella premi reali per moneyness x tenor
q = oc.quote(ts_ms, spot=1700, otm=0.10, opt_type="P", min_tenor_d=5, max_tenor_d=14)
# q = dict(ask_pct, iv, atm_iv, skew, spread_pct, oi, delta, strike, tenor_d, staleness_h)
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
OPT_DIR = PROJECT_ROOT / "data" / "options"
_NUM = ["strike", "bid", "ask", "mid", "iv", "delta", "gamma", "theta", "vega"]
def load_chain(asset: str) -> pd.DataFrame:
df = pd.read_parquet(OPT_DIR / f"{asset.lower()}_chain.parquet")
for c in _NUM:
df[c] = pd.to_numeric(df[c], errors="coerce")
for c in ("open_interest", "volume_24h"):
df[c] = pd.to_numeric(df[c], errors="coerce")
df["ts"] = pd.to_datetime(df["timestamp"], utc=True)
df["exp"] = pd.to_datetime(df["expiry"], utc=True)
df["ts_ms"] = (df["ts"].astype("int64") // 10**6)
df["tenor_d"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0
return df.sort_values("ts_ms").reset_index(drop=True)
_MKT_NUM = ["spot", "dvol", "realized_vol_30d", "iv_minus_rv", "funding_perp_annualized",
"funding_cross_annualized", "dealer_net_gamma", "gamma_flip_level",
"oi_delta_pct_4h", "macro_days_to_event"]
def load_market(asset: str | None = None) -> pd.DataFrame:
"""Pannello regime REALE pre-calcolato da cerbero-bite (market_snapshots, dal 2026-03-26,
~15min): spot, dvol, realized_vol_30d, iv_minus_rv (VRP), funding perp/cross,
dealer_net_gamma (net-GEX), gamma_flip_level, oi_delta_pct_4h, liquidation_long/short_risk.
asset=None -> tutti. Restituisce ordinato per ts con colonne ts (UTC) e ts_ms."""
df = pd.read_parquet(OPT_DIR / "market_snapshots.parquet")
if asset is not None:
df = df[df["asset"] == asset].copy()
for c in _MKT_NUM:
if c in df.columns:
df[c] = pd.to_numeric(df[c], errors="coerce")
df["ts"] = pd.to_datetime(df["timestamp"], utc=True, format="ISO8601")
df["ts_ms"] = df["ts"].astype("int64") // 10**6
return df.sort_values("ts_ms").reset_index(drop=True)
class OptionChain:
def __init__(self, asset: str):
self.asset = asset
self.df = load_chain(asset)
self._ts = self.df["ts_ms"].values
# ---------------- aggregati robusti ----------------
def _spot_proxy(self) -> pd.Series:
"""spot ~ strike del put con delta piu' vicino a -0.5, per snapshot."""
puts = self.df[self.df["option_type"] == "P"].copy()
puts["d_atm"] = (puts["delta"] + 0.5).abs()
atm = puts.sort_values("d_atm").groupby("timestamp").first()
return atm["strike"]
def skew_curve(self, opt_type: str = "P") -> pd.DataFrame:
"""IV mediana / IV-ATM per banda di moneyness (strike/spot)."""
spot = self._spot_proxy()
d = self.df[self.df["option_type"] == opt_type].copy()
d["spot"] = d["timestamp"].map(spot)
d = d.dropna(subset=["spot", "iv"])
d["m"] = d["strike"] / d["spot"]
atm_iv = d.assign(da=(d["delta"].abs() - 0.5).abs()).sort_values("da").groupby("timestamp")["iv"].first()
d["atm_iv"] = d["timestamp"].map(atm_iv)
d["skew"] = d["iv"] / d["atm_iv"]
bins = [0.7, 0.85, 0.9, 0.95, 1.0, 1.05, 1.1, 1.15, 1.3]
d["band"] = pd.cut(d["m"], bins)
g = d.groupby("band", observed=True).agg(
n=("skew", "size"), iv_med=("iv", "median"), skew_med=("skew", "median"),
spread_med=("ask", lambda s: float("nan")))
# spread% mediano
d["spread_pct"] = (d["ask"] - d["bid"]) / ((d["ask"] + d["bid"]) / 2) * 100
g["spread_med%"] = d.groupby("band", observed=True)["spread_pct"].median()
g["oi_med"] = d.groupby("band", observed=True)["open_interest"].median()
return g.drop(columns=["spread_med"])
def premium_levels(self, opt_type: str = "P") -> pd.DataFrame:
"""premio reale mediano (ask, %spot) per banda moneyness x tenor."""
spot = self._spot_proxy()
d = self.df[self.df["option_type"] == opt_type].copy()
d["spot"] = d["timestamp"].map(spot)
d = d.dropna(subset=["spot", "ask"])
d["m"] = d["strike"] / d["spot"]
d["ask_pct"] = d["ask"] * 100.0 # ask quotato in coin -> %spot = ask*100
d["tenor_b"] = pd.cut(d["tenor_d"], [0, 3.5, 14, 45, 400],
labels=["1-3d", "4-14d", "15-45d", ">45d"])
d["m_b"] = pd.cut(d["m"], [0.7, 0.85, 0.9, 0.95, 1.05, 1.15, 1.3],
labels=["<-10%", "-10%", "-7%", "ATM", "+7%", "+10%"])
g = d.groupby(["m_b", "tenor_b"], observed=True).agg(
n=("ask_pct", "size"), prem_pct=("ask_pct", "median"),
iv=("iv", "median"), oi=("open_interest", "median"))
return g
# ---------------- lookup per-trade (best effort) ----------------
def quote(self, ts_ms: int, spot: float, otm: float = 0.10, opt_type: str = "P",
min_tenor_d: float = 5.0, max_tenor_d: float = 14.0,
max_staleness_h: float = 48.0) -> dict | None:
"""Quote REALE piu' fresca <= ts_ms per la protezione a `otm` OTM e tenor nel range.
put: strike target = spot*(1-otm); call: spot*(1+otm). None se nulla nella finestra."""
lo = ts_ms - int(max_staleness_h * 3600 * 1000)
i1 = np.searchsorted(self._ts, ts_ms, "right")
i0 = np.searchsorted(self._ts, lo, "left")
if i1 <= i0:
return None
w = self.df.iloc[i0:i1]
w = w[w["option_type"] == opt_type]
w = w[(w["tenor_d"] >= min_tenor_d) & (w["tenor_d"] <= max_tenor_d)]
if w.empty:
return None
# ultima quota per strumento, poi strike piu' vicino al target
w = w.sort_values("ts_ms").groupby("instrument_name").tail(1)
target = spot * (1 - otm) if opt_type == "P" else spot * (1 + otm)
row = w.iloc[(w["strike"] - target).abs().argmin()]
atm = w.iloc[(w["delta"].abs() - 0.5).abs().argmin()]
ask, bid = float(row["ask"]), float(row["bid"])
return dict(
ask_pct=ask * 100.0, iv=float(row["iv"]), atm_iv=float(atm["iv"]),
skew=float(row["iv"]) / float(atm["iv"]) if atm["iv"] else float("nan"),
spread_pct=(ask - bid) / ((ask + bid) / 2) * 100 if (ask + bid) > 0 else float("nan"),
oi=int(row["open_interest"] or 0), delta=float(row["delta"]),
strike=float(row["strike"]), tenor_d=float(row["tenor_d"]),
staleness_h=(ts_ms - int(row["ts_ms"])) / 3600000.0,
instrument=row["instrument_name"])
if __name__ == "__main__":
for asset in ("ETH", "BTC"):
oc = OptionChain(asset)
print(f"\n===== {asset}{len(oc.df)} righe, {oc.df['ts'].min().date()} -> {oc.df['ts'].max().date()} =====")
print("\n--- curva di skew (put, IV mediana / IV-ATM) ---")
print(oc.skew_curve("P").to_string())
print("\n--- premi reali mediani (ask %spot) per moneyness x tenor ---")
print(oc.premium_levels("P").to_string())
# quote demo: ultimo ts disponibile, 10% OTM put settimanale
ts = int(oc.df["ts_ms"].iloc[-1]); sp = float(oc._spot_proxy().iloc[-1])
q = oc.quote(ts, spot=sp, otm=0.10, opt_type="P", min_tenor_d=5, max_tenor_d=14)
print(f"\n--- quote demo: put 10%OTM settimanale ~spot {sp:.0f} ---\n {q}")
+83
View File
@@ -0,0 +1,83 @@
"""Importa lo storico per-strike delle opzioni da cerbero-bite -> data/options/.
cerbero-bite (container Docker accanto) accumula in continuo gli snapshot della
catena opzioni Deribit (BTC+ETH) nella tabella `option_chain_snapshots` del suo
SQLite (`/app/data/state.sqlite`, volume `cerbero-bite_bite-data`, root-only).
Questo e' lo storico per-strike REALE (bid/ask/mid/IV/greche/OI/volume) che il
progetto credeva non-backtestabile (muro ARGO/GEX): qui esiste ed e' gratis.
Il volume non e' leggibile direttamente (root), quindi esportiamo via `docker
exec` (il container ha pandas+pyarrow) e copiamo i parquet in data/options/.
Analogo a regime_fetcher.py -> data/regime/. Rigenera con:
uv run python scripts/analysis/options_fetcher.py
NB: snapshot da 2026-05-01 in poi (cerbero-bite e' partito allora), cadenza ~12min.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OUT = PROJECT_ROOT / "data" / "options"
BITE_COMPOSE = "/opt/docker/cerbero-bite/docker-compose.yml"
BITE_SERVICE = "cerbero-bite"
_EXPORT = r'''
import sqlite3, pandas as pd
con = sqlite3.connect("file:/app/data/state.sqlite?mode=ro", uri=True)
for asset in ("ETH", "BTC"):
df = pd.read_sql_query(
"SELECT * FROM option_chain_snapshots WHERE asset=?", con, params=(asset,))
df.to_parquet(f"/tmp/{asset.lower()}_chain.parquet")
print(f"{asset} {len(df)}")
try:
dv = pd.read_sql_query("SELECT * FROM dvol_history", con)
dv.to_parquet("/tmp/dvol_history.parquet"); print(f"DVOL {len(dv)}")
except Exception as e:
print("dvol skip", e)
# market_snapshots: pannello regime allineato (spot, dvol, VRP, funding, GEX dealer,
# gamma_flip, liquidation risk, oi_delta) -- feature reali pre-calcolate.
try:
ms = pd.read_sql_query("SELECT * FROM market_snapshots", con)
ms.to_parquet("/tmp/market_snapshots.parquet"); print(f"MARKET {len(ms)}")
except Exception as e:
print("market skip", e)
'''
def _container_id() -> str:
out = subprocess.run(["docker", "compose", "-f", BITE_COMPOSE, "ps", "-q", BITE_SERVICE],
capture_output=True, text=True, check=True)
cid = out.stdout.strip().splitlines()[0]
if not cid:
raise RuntimeError("container cerbero-bite non trovato (e' su?)")
return cid
def fetch() -> None:
OUT.mkdir(parents=True, exist_ok=True)
print("export dalla SQLite di cerbero-bite (docker exec)...")
r = subprocess.run(["docker", "compose", "-f", BITE_COMPOSE, "exec", "-T", BITE_SERVICE,
"python", "-c", _EXPORT], capture_output=True, text=True)
print(r.stdout.strip())
if r.returncode != 0:
print(r.stderr[-2000:], file=sys.stderr)
raise SystemExit("export fallito")
cid = _container_id()
for fn in ("eth_chain.parquet", "btc_chain.parquet", "dvol_history.parquet",
"market_snapshots.parquet"):
dst = OUT / fn
cp = subprocess.run(["docker", "cp", f"{cid}:/tmp/{fn}", str(dst)],
capture_output=True, text=True)
if cp.returncode == 0:
print(f" -> {dst.relative_to(PROJECT_ROOT)} ({dst.stat().st_size // 1024} KB)")
else:
print(f" (skip {fn}: {cp.stderr.strip()})")
print("OK. Loader: scripts/analysis/options_chain.py")
if __name__ == "__main__":
fetch()