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:
@@ -28,3 +28,7 @@ data/portfolios/
|
||||
data/regime/
|
||||
_disp_scratch/
|
||||
data/regime/dispersion_features.parquet
|
||||
|
||||
# storico catena opzioni importato da cerbero-bite (rigenerabile: options_fetcher.py)
|
||||
data/options/
|
||||
data/_reset_backup/
|
||||
|
||||
@@ -56,13 +56,16 @@ scripts/portfolios/ → definizioni PORT01-06 (_defs.py) + report run() + hou
|
||||
scripts/waste/ → strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
|
||||
scripts/analysis/ → ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...);
|
||||
regime_fetcher.py + regime_lab.py (DVOL/funding/feature regime per la ricerca);
|
||||
exit_lab.py + exit_policies/ (harness ricerca exit: cache segnali, train/OOS)
|
||||
exit_lab.py + exit_policies/ (harness ricerca exit: cache segnali, train/OOS);
|
||||
options_fetcher.py + options_chain.py (storico opzioni REALE da cerbero-bite);
|
||||
option_overlay_lab.py (overlay opzioni prezzato; mr02eth_port06_gate.py = gate swap-sleeve)
|
||||
scripts/bump_version.py, scripts/deploy.sh → versionamento e deploy (bump+commit+rebuild)
|
||||
VERSION → versione semver (cotta nell'immagine, +1 ad ogni deploy)
|
||||
strategies.yml → config multi-strategy paper trader
|
||||
docs/diary/ → diario di ricerca giornaliero
|
||||
docs/specs/ → specifiche di design
|
||||
data/raw/ → file .parquet OHLCV (gitignored) | data/regime/ → DVOL+funding+feature (gitignored)
|
||||
data/options/ → storico catena opzioni per-strike (bid/ask/IV/greche/OI) importato da cerbero-bite (gitignored)
|
||||
data/instruments_registry.json → allowlist strumenti validati (gate del downloader)
|
||||
```
|
||||
|
||||
@@ -324,6 +327,28 @@ public API, OI reale ~368k contratti, DVOL/funding storici gratis) ma **lo stori
|
||||
non è gratuito → non backtestabile OOS** (stesso muro delle opzioni W18/19/21). Niente evidenza crypto,
|
||||
segno fragile, mercato dominato dai perp. Diario `docs/diary/2026-06-01-argo-gex-feasibility.md`.
|
||||
|
||||
**Storico opzioni REALE da cerbero-bite (2026-06-09, il muro ARGO è caduto per il periodo recente).**
|
||||
Il container **cerbero-bite** (accanto, `/opt/docker/cerbero-bite`) accumula in continuo lo **storico
|
||||
per-strike** della catena opzioni Deribit (BTC+ETH) nella tabella `option_chain_snapshots` del suo
|
||||
SQLite (`bite-data:/app/data/state.sqlite`, root-only): bid/ask/mid/**IV per-strike**/greche/OI/volume,
|
||||
**dal 2026-05-01**, cadenza ~12 min (~110k righe/asset). È esattamente il dato che ARGO/W18-21
|
||||
credevano non-gratuito. `scripts/analysis/options_fetcher.py` lo importa via `docker exec` →
|
||||
`data/options/{eth,btc}_chain.parquet`; `scripts/analysis/options_chain.py` (`OptionChain`) espone
|
||||
loader + `skew_curve()` / `premium_levels()` (aggregati ROBUSTI) + `quote()` causale best-effort.
|
||||
**Caveat granularità:** cerbero-bite snapshotta una **fetta rotante** (~1 scadenza per ciclo) → ottimo
|
||||
per skew/premi **aggregati**, limitato per il pricing **per-trade** preciso (usare la finestra di
|
||||
staleness). **Pannello regime `market_snapshots`** (anch'esso importato → `data/options/market_snapshots.parquet`,
|
||||
loader `options_chain.load_market(asset)`): feature REALI pre-calcolate dal 2026-03-26, ~15min, 99-100%
|
||||
popolate — **spot, dvol, realized_vol_30d, iv_minus_rv (VRP), funding perp/cross, `dealer_net_gamma`
|
||||
(net-GEX dealer!), `gamma_flip_level`, oi_delta_pct_4h, liquidation_long/short_risk**. È il segnale
|
||||
ARGO/GEX che il progetto dichiarò non-backtestabile: ora accumula, reale e allineato (usabile come
|
||||
pannello in regime_lab; lo `spot` elimina il proxy ATM). **Numeri reali misurati (ETH):** skew put 10% OTM = **×1.1** (liquido, spread ~7%, NON ×1.6
|
||||
come sembrava da un singolo snapshot a 1g, illiquido); premio reale put 10% OTM ≈ **1.0%/mese** (0.65%/sett),
|
||||
catastrofe 15%+ OTM ≈ 0.33%/mese. **Vincolo strutturale:** gli strike **10% OTM a 24h NON esistono**
|
||||
(Deribit lista solo near-ATM sui tenor corti) → un overlay **per-trade a 24h è infattibile**; l'unica
|
||||
struttura eseguibile è una **put settimanale/mensile standing** (catastrofe-cap di sleeve), da gateare
|
||||
coi premi reali. Vedi `docs/diary/2026-06-08-mr02eth-replace-search.md`.
|
||||
|
||||
**Frattali del segnale × Regime ARGO (ricerca 100 agenti, 2026-06-02, RECORD).** Cercata una strategia
|
||||
che combini un segnale frattale (Hurst/Higuchi/Williams/analog) con un gate di regime (DVOL/VRP/funding).
|
||||
Infrastruttura riusabile: `scripts/analysis/regime_fetcher.py` (DVOL+funding da Deribit mainnet →
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# 2026-06-08 — Ricerca sostituto/miglioria a MR02/ETH (127 strategie + opzioni)
|
||||
|
||||
## Contesto
|
||||
MR02/ETH è lo sleeve a DD più alto della famiglia fade ed è il maggior drag
|
||||
dell'esecuzione reale (testnet: −19€ su 7 trade, 0 win, faded un downtrend −14%
|
||||
in long poi shortato il rimbalzo — whipsaw classico). Obiettivo: trovare un
|
||||
sostituto o una miglioria con ≥100 agenti su strategie differenti, metodologia
|
||||
onesta (ingresso eseguibile, fee 0.10% RT, OOS, gate PORT06).
|
||||
|
||||
## Cosa è stato fatto
|
||||
- **Workflow 1 — 109 strategie ETH distinte** (`mr02eth_search.workflow.js`):
|
||||
gated-fade, mean-reversion alternative, exit, trend-following, volatilità,
|
||||
cross-asset, ML/shape, microstruttura, statistico-robusto. Harness onesto
|
||||
`explore_lab`. Poi **verifica avversariale** dei top-24 (audit look-ahead +
|
||||
re-implementazione indipendente + robustezza). → 26 survivor, **6 confermati**.
|
||||
- **Workflow 2 — 18 overlay di OPZIONI** (`mr02eth_options.workflow.js`) +
|
||||
sintesi con audit crash reali. Harness nuovo `option_overlay_lab.py`: opzioni
|
||||
prezzate Black-Scholes sulla **DVOL** reale di Deribit (storica/gratuita,
|
||||
`data/regime/eth_dvol.parquet`), premio dedotto, ipotesi **conservative**
|
||||
(skew_mult sui comprati, uscita anticipata solo a intrinseco).
|
||||
- **Gate PORT06** (`mr02eth_port06_gate.py`): swap di MR02_ETH col candidato,
|
||||
equity giornaliera su IDX 2021-2026, ri-misura FULL+OOS Sharpe/DD (cap
|
||||
weighting). Sanity check: MR02 ricostruito col mio engine == canonico (Δ 0.00).
|
||||
|
||||
## Risultato (gate PORT06; baseline canonico FULL 6.43/3.96, OOS 8.58/1.36)
|
||||
| variante (swap MR02/ETH) | FULL Sh | FULL DD | OOS Sh | OOS DD |
|
||||
|---|---|---|---|---|
|
||||
| EXIT-16 (config LIVE) | 6.73 | 3.67 | 8.80 | 1.23 |
|
||||
| no-SL nudo | 6.76 | 3.68 | 8.87 | 1.23 |
|
||||
| no-SL + put/call 10% OTM | 6.78 | 3.64 | 8.86 | 1.23 |
|
||||
| varratio_gate (PROMOSSO vs canonico) | 6.41 | 3.73 | 8.53 | 1.36 |
|
||||
| choppiness_donch | 6.37 | 3.35 | 8.47 | 1.32 |
|
||||
| vrp_neg_dvol_low | 6.36 | 3.22 | 8.37 | 1.43 |
|
||||
|
||||
## MIGLIORIA TROVATA (gate vs LIVE EXIT-16, non vs canonico)
|
||||
Riferimento onesto = config LIVE EXIT-16 (FULL 6.73/3.67 — OOS 8.80/1.23), non
|
||||
l'intrabar-SL canonico che nessuno gira.
|
||||
|
||||
| sleeve ETH | ΔFULL Sh | ΔFULL DD | ΔOOS Sh | ΔOOS DD | esito |
|
||||
|---|---|---|---|---|---|
|
||||
| **no-SL + put/call 10% OTM** | **+0.05** | **−0.03** | **+0.06** | 0.00 | ✅ MIGLIORIA deployabile |
|
||||
| no-SL nudo | +0.04 | +0.02 | +0.07 | 0.00 | ✅ migliora ma sl=0 NON deployabile |
|
||||
| EXIT16 + put10 | −0.01 | +0.03 | 0.00 | 0.00 | = pari (assicurazione gratis) |
|
||||
| varratio / vrp / chop / blend | negativi | | | | peggiorano vs EXIT-16 |
|
||||
|
||||
**La miglioria = MR02/ETH no-SL + floor a opzioni 10% OTM** (put sui long, call
|
||||
sugli short). Batte il live EXIT-16 in modo PARETO su 3/4 metriche PORT06 (4ª
|
||||
piatta). Meccanismo: **rimuovere lo stop** (intrabar/close-confirm su una fade
|
||||
mean-reverting sono falsi negativi — overshoot che rientra) e dare la funzione
|
||||
dello stop (cap della catastrofe) a un'**opzione comprata** = protezione a
|
||||
rischio-definito SENZA il costo whipsaw/falso-stop. Standalone il salto è grande
|
||||
(Sharpe 8.45→12.14, OOS DD 13%→7%); a portafoglio è modesto (+0.05/+0.06 Sharpe)
|
||||
perché è 1 sleeve su 17, ma è un miglioramento MISURATO e verificato (soglia
|
||||
gate +0.03, ottenuto +0.05/+0.06), non rumore.
|
||||
|
||||
## Conclusioni (oneste)
|
||||
1. **Sostituti/blend: tutti peggiorano vs il live EXIT-16.** I candidati gated
|
||||
(varratio/vrp/choppiness) e i blend within-sleeve battono solo lo strawman
|
||||
canonico, non il live; le loro basse esposizioni diluiscono il motore di
|
||||
ritorno (Sharpe −0.08/−0.44). Lo Sharpe ~20 nell'harness era artefatto di
|
||||
`sqrt(N_trade)` (onesto ~4.3). → **non sostituire né blendare.**
|
||||
2. **L'unica miglioria è la coppia no-SL + opzione.** Il no-SL recupera il drag
|
||||
dello stop; l'opzione cappa la coda da **gap-through** che EXIT-16 NON protegge
|
||||
(esce al close DOPO il buco → prende la perdita piena). Audit crash: FTX nov-22
|
||||
−48%→−30%, LUNA-era 2022-09 −63%→−31%, 2018/19 −65%→−30%. Premio ~break-even
|
||||
(paga 156% / incassa 149%). È ciò che rende deployabile il no-SL.
|
||||
|
||||
## Azione raccomandata
|
||||
- **Deployare MR02/ETH come no-SL + floor a opzioni 10% OTM** (put long / call
|
||||
short, per-trade o settimanale ammortizzato). Batte il live su 3/4 metriche +
|
||||
cappa esplicitamente la coda. **Prima del capitale**: validare prezzi/liquidità/
|
||||
skew per-strike REALI su Deribit testnet (qui pricing BS sintetico sulla DVOL =
|
||||
ATM-IV; stress skew ×1.5 gonfia il premio FULL ma l'OOS regge). Stesso muro
|
||||
non-backtestabile di ARGO/GEX sul per-strike, aggirato col proxy DVOL.
|
||||
- Step intermedio a costo zero: **aggiungere subito il floor opzione all'EXIT-16
|
||||
live** (= "pari" sui numeri, ma assicurazione gratis sul gap-through) mentre si
|
||||
valida la transizione a no-SL.
|
||||
|
||||
## Addendum 2026-06-09 — validazione su PREZZI REALI + integrazione cerbero-bite
|
||||
- Probe live Deribit mainnet (singolo snapshot): sembrava skew ×1.6 e liquidità
|
||||
pessima sul 10% OTM a 1g → l'overlay per-trade pareva bocciato.
|
||||
- **cerbero-bite** (container accanto) ha però **lo storico per-strike reale** in
|
||||
`option_chain_snapshots` (SQLite, dal 2026-05-01, ~110k righe/asset, bid/ask/IV/
|
||||
greche/OI, cadenza ~12min). Importato in `data/options/` via `options_fetcher.py`;
|
||||
loader `options_chain.py` (skew_curve/premium_levels/quote causale).
|
||||
- **Correzione dai dati reali (5.5 settimane):** skew put 10% OTM = **×1.1** (liquido,
|
||||
spread ~7%) — il ×1.6 del probe era un artefatto del deep-OTM a 1g illiquido. Premio
|
||||
reale 10% OTM ≈ **1.0%/mese**. → la miglioria no-SL+opzione **regge** sui prezzi veri.
|
||||
- **MA vincolo strutturale reale:** gli strike **10% OTM a 24h NON esistono** (n=0 in 5.5
|
||||
settimane; Deribit lista solo near-ATM sui tenor corti). L'overlay **per-trade a 24h è
|
||||
infattibile**; l'unica struttura eseguibile è una **put settimanale/mensile standing**
|
||||
(catastrofe-cap di sleeve), da gateare a parte coi premi reali.
|
||||
- **Infrastruttura sbloccata:** il muro ARGO/GEX ("opzioni non-backtestabili") è caduto
|
||||
per il periodo recente — d'ora in poi le tesi opzioni si validano su prezzi VERI.
|
||||
Importato anche il pannello `market_snapshots` (spot/VRP/funding/**net-GEX dealer**/
|
||||
gamma-flip/liquidation, dal 2026-03-26) → `data/options/`, loader `options_chain.load_market`.
|
||||
|
||||
## Addendum 2 — gate del COLLAR standing (catastrophe-cap auto-finanziato) — NO-GO
|
||||
Premi reali → collar = put −13% (1.0%/m) finanziata da call +10% (1.05%/m) ≈ premio netto 0.
|
||||
Gate PORT06 (`eth_collar_gate.py`, BS calibrato skew_put 1.12 / skew_call 1.0, sweep OTM/hedge_frac):
|
||||
- Il collar **PAGA nei crash** (ETH −40% dic-21 → +6.1%, −30% feb-25 → +6.6%): protezione reale.
|
||||
- MA **net-negativo a portafoglio**: taglia il FULL DD (−0.2/−0.3pp) ma **costa Sharpe** (FULL e OOS)
|
||||
e **ALZA l'OOS DD** (+0.18/+0.28pp) — nell'OOS (no crash profondo, qualche rally) la call corta
|
||||
perde e il premio erode. Peggiora il DD che doveva ridurre.
|
||||
- **Verdetto: il tail-hedge a opzioni NON conviene a PORT06** (né per-trade né collar). Strutturale:
|
||||
lo sleeve ETH pesa 6.5% → la **diversificazione è già la protezione di coda** (trade −65% =
|
||||
−4% aggregato). La cap auto-finanziata era giusta sul *costo* (~break-even) ma la protezione non
|
||||
vale il costo Sharpe/OOS-DD. Le opzioni si chiudono come **NO-GO empirico su prezzi reali**.
|
||||
|
||||
## Artefatti riusabili
|
||||
- `scripts/analysis/option_overlay_lab.py` — overlay opzioni (pricing BS sintetico su DVOL).
|
||||
- `scripts/analysis/options_fetcher.py` — import storico opzioni reale da cerbero-bite → data/options/.
|
||||
- `scripts/analysis/options_chain.py` — loader + skew/premi reali + lookup causale (`OptionChain`).
|
||||
- `scripts/analysis/mr02eth_port06_gate.py` — gate swap-sleeve PORT06.
|
||||
- `scripts/analysis/mr02eth_search.workflow.js`, `mr02eth_options.workflow.js`.
|
||||
@@ -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 })),
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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 })),
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user