From a937e3766f0c3c78807c4ff7b7f59f2623f4db7a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 26 Jul 2026 18:32:23 +0000 Subject: [PATCH] =?UTF-8?q?research:=20nuovo=20schema=20fee=20Deribit=20(1?= =?UTF-8?q?=20ago=202026)=20=E2=80=94=20misurata=20la=20curva,=20nessuna?= =?UTF-8?q?=20azione=20oggi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'annuncio (taker piu' bassi, maker rebate piu' bassi, soglie VIP abbassate, VIP7, liquidation fee 1%, spot a zero) NON contiene numeri, e la tabella nell'articolo Insights e' un'IMMAGINE: non letta da fonte primaria. I valori indicativi (base ~5bps taker / 2bps maker, VIP7 2/0) vengono da un riassunto SECONDARIO ed e' dichiarato. Quindi misurata la CURVA invece di aspettare il numero — vale per qualunque valore esca. Le repliche parametrizzate riproducono BIT-EXACT gli sleeve di produzione alla fee canonica (max|dif| = 0.0), altrimenti la curva descriverebbe un'altra strategia. bps/lato %RT | TP01 Sh | SKH01 Sh | BOOK Sh CAGR 0.0 0.00% | 1.322 | 1.567 | 1.849 21.69% 3.0 0.06% | 1.303 | 1.495 | 1.799 20.99% 5.0 0.10% | 1.290 | 1.446 | 1.766 20.53% <- oggi 10.0 0.20% | 1.258 | 1.324 | 1.682 19.39% 15.0 0.30% | 1.226 | 1.200 | 1.597 18.26% Sensibilita' del book: -0.017 Sharpe/bps, -0.23% CAGR/bps. Anche un RADDOPPIO del taker costa 0.08 di Sharpe, MENO della banda d'ancora dello stesso book (2.222 -> 1.946): la fee va messa nella sua scala di grandezza. SKH01 e' ~4x piu' sensibile di TP01 (-0.69% vs -0.09% CAGR/bps: round-trip discreti vs posizione continua vol-targeted) -> se il taker salisse, il primo parametro da rivedere e' il peso 75/25. REGOLA DECISA IN ANTICIPO (per non decidere col numero davanti): taker <=5bps/lato -> non si tocca nulla; >10bps/lato -> rivedere il peso di SKH01. Punti irrilevanti e perche': maker (il book manda ordini market; tocca solo la raccomandazione T1 gia' non implementata); liquidation fee 1% (live.json da' nozionale lordo max 1.00x l'equity con disaster-SL -30% -> servirebbe un movimento avverso ~100%); VIP (a $600 il volume 30g e' trascurabile). La conclusione sulla liquidation fee poggia sul CAP, non sulla strategia: cablata una guardia di decisione (test_leva_massima_da_config_resta_sotto_o_uguale_a_1x) che ROMPE se qualcuno alza il cap, invece di lasciarla valida per inerzia. AZIONE 1 agosto: leggere il tier reale in Account Settings e applicare la regola sopra. Book, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 35 ++++++ docs/diary/2026-07-26-fee-deribit.md | 123 +++++++++++++++++++ scripts/research/r0726_fee_sensitivity.py | 139 ++++++++++++++++++++++ tests/test_fee_sensitivity.py | 95 +++++++++++++++ 4 files changed, 392 insertions(+) create mode 100644 docs/diary/2026-07-26-fee-deribit.md create mode 100644 scripts/research/r0726_fee_sensitivity.py create mode 100644 tests/test_fee_sensitivity.py diff --git a/CLAUDE.md b/CLAUDE.md index c7b3b43..5d53375 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -919,6 +919,41 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis **REGOLE:** (a) un rischio non-diversificabile dagli sleeve va prezzato a parte; (b) P(successo) puo' nascondere P(rovina) — per una rendita la metrica e' la seconda; (c) una raccomandazione presa su un asse solo va ricontrollata sugli altri prima di considerarla stabile. +- 📅 **NUOVO SCHEMA FEE DERIBIT dal 2026-08-01 — misurata la CURVA, nessuna azione oggi + (2026-07-26).** Script `r0726_fee_sensitivity.py`, test `tests/test_fee_sensitivity.py` (6), + diario `2026-07-26-fee-deribit.md`. **Book/pesi/cron/config INVARIATI.** + L'annuncio (taker piu' bassi, **maker rebate piu' bassi**, soglie VIP abbassate, nuovo VIP7, + **liquidation fee 1%** su tutti i prodotti, spot a zero fino al collegamento con Coinbase) **non + contiene numeri** e ⚠️ **la tabella nell'articolo Insights e' un'IMMAGINE, non letta da fonte + primaria** → i valori indicativi (base ~5bps taker / 2bps maker, VIP7 2/0) vengono da un + riassunto **secondario** e vanno etichettati come tali. Percio' e' misurata la **curva**: + | bps/lato | %RT | TP01 Sh | SKH01 Sh | BOOK Sh | BOOK CAGR | + |---|---|---|---|---|---| + | 0 | 0.00% | 1.322 | 1.567 | 1.849 | 21.69% | + | 3 | 0.06% | 1.303 | 1.495 | 1.799 | 20.99% | + | **5** | **0.10%** | **1.290** | **1.446** | **1.766** | **20.53%** ← oggi | + | 10 | 0.20% | 1.258 | 1.324 | 1.682 | 19.39% | + | 15 | 0.30% | 1.226 | 1.200 | 1.597 | 18.26% | + **Sensibilita' marginale del book: −0.017 Sharpe/bps, −0.23% CAGR/bps** → anche un RADDOPPIO del + taker costa 0.08 di Sharpe, **meno della banda d'ancora dello stesso book** (2.222 → 1.946). + ⚠️ **SKH01 e' ~4× piu' sensibile di TP01** (−0.69% vs −0.09% CAGR/bps: round-trip discreti vs + posizione continua vol-targeted) → **se il taker salisse, il primo parametro da rivedere e' il + peso 75/25**, non altro. **REGOLA DECISA IN ANTICIPO (per non decidere col numero davanti): + taker ≤5bps/lato → non si tocca nulla; >10bps/lato → rivedere il peso di SKH01.** + **Punti irrilevanti e perche':** *maker* — il book manda ordini **market**; tocca solo la + raccomandazione **T1 non implementata** (TP resting per incassare il maker), che resta chiusa + per il netting su strumento unico e il cui beneficio era quasi tutto "convertire una lotteria in + certezza", non la fee. *Liquidation fee 1%* — `live.json` da' nozionale lordo max **1.00x + l'equity** (`frac 0.5` × 2 asset) con disaster-SL −30%: servirebbe un movimento avverso ~100%; + ⚠️ la conclusione poggia sul CAP, quindi e' cablata una **guardia di decisione** + (`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`): **alzare il cap rompe il test**. + *VIP/VIP7* — a $600 il volume 30g e' trascurabile, tier base a qualunque soglia. + ✅ **Cio' che il cambio non puo' rompere:** tutti i backtest sono a 0.10% RT → se la fee scende + diventano **conservativi**. **AZIONE 1 agosto:** leggere il tier reale in Account Settings e + applicare la regola sopra. **REGOLE:** (a) a un annuncio senza numeri si risponde misurando la + **curva**, non aspettando il numero; (b) dichiarare la **qualita' della fonte** (immagine non + letta = dato secondario); (c) una conclusione che poggia su un parametro di config va legata a + un **test su quel parametro**, o sopravvive al cambio che la rende falsa. - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** diff --git a/docs/diary/2026-07-26-fee-deribit.md b/docs/diary/2026-07-26-fee-deribit.md new file mode 100644 index 0000000..5c63f9f --- /dev/null +++ b/docs/diary/2026-07-26-fee-deribit.md @@ -0,0 +1,123 @@ +# 2026-07-26 — Nuovo schema fee Deribit (dal 1 agosto 2026): la curva, non il numero + +**Input:** annuncio Deribit di un nuovo schema fee **effettivo 2026-08-01** — taker piu' bassi e +maker rebate piu' bassi su futures/perpetual, soglie VIP abbassate, nuovo tier VIP7, +**liquidation fee 1% su tutti i prodotti** (opzioni: cap premio 25%), fee spot temporaneamente +azzerate fino al collegamento coi mercati spot Coinbase. + +**Script:** `r0726_fee_sensitivity.py`. **Test:** `tests/test_fee_sensitivity.py` (6). +**Book, pesi, cron, config: INVARIATI.** + +--- + +## 0. Perche' non ho aspettato la tabella + +La fee e' un **vincolo di prim'ordine** dichiarato di questo progetto ("0.10% RT baseline; molte +operazioni = morte per fee"). Ma l'annuncio non contiene un solo numero, e **la tabella numerica +nell'articolo Insights e' un'IMMAGINE**: non l'ho letta da fonte primaria. + +⚠️ I valori che cito del nuovo schema vengono da un **riassunto testuale secondario**, non dalla +tabella di Deribit: tier base ~**5 bps taker / 2 bps maker**, VIP7 **2/0 bps**. Vanno trattati come +indicativi finche' non si legge il tier reale in Account Settings. + +Quindi invece di assumere un numero ho misurato la **curva**: quando la tabella e' leggibile si +prende il valore giusto da qui, senza rifare l'analisi. + +Il progetto modella ovunque **5 bps/lato = 0.10% RT (taker)**: `trend_portfolio.CANONICAL +.fee_side = 0.0005`, `sleeves._skyhook_returns fee_rt=0.001`, paper monitor `FEE_SIDE = 0.0005`. +Le repliche parametrizzate riproducono **bit-exact** gli sleeve di produzione alla fee canonica +(`max|dif| = 0.0`, test permanenti) — altrimenti la curva descriverebbe un'altra strategia. + +--- + +## 1. La curva + +``` + bps/lato %RT | TP01 Sh CAGR | SKH01 Sh CAGR | BOOK Sh CAGR maxDD + 0.0 0.00% | 1.322 16.60% | 1.567 35.66% | 1.849 21.69% 8.92% + 1.0 0.02% | 1.316 16.51% | 1.543 34.95% | 1.833 21.46% 9.02% + 2.0 0.04% | 1.309 16.42% | 1.519 34.24% | 1.816 21.22% 9.12% + 3.0 0.06% | 1.303 16.33% | 1.495 33.53% | 1.799 20.99% 9.22% + 5.0 0.10% | 1.290 16.15% | 1.446 32.14% | 1.766 20.53% 9.42% <- oggi + 7.5 0.15% | 1.274 15.92% | 1.385 30.41% | 1.724 19.96% 9.67% + 10.0 0.20% | 1.258 15.69% | 1.324 28.70% | 1.682 19.39% 9.92% + 15.0 0.30% | 1.226 15.24% | 1.200 25.36% | 1.597 18.26% 10.59% + +drag della fee attuale vs fee zero: TP01 -0.032 Sh / -0.46% CAGR + SKH01 -0.121 Sh / -3.52% CAGR + BOOK -0.084 Sh / -1.15% CAGR +sensibilita' marginale: BOOK -0.017 Sharpe/bps -0.23% CAGR/bps +``` + +**Il book e' poco sensibile.** Anche un raddoppio del taker a 10 bps costerebbe 0.08 di Sharpe — +**meno della banda d'ancora misurata oggi stesso** (book FULL canonico 2.222 vs mediana 1.946). +Mettere la fee nella giusta scala di grandezza: e' un effetto piu' piccolo dell'ancora. + +### I due sleeve NON sono uguali + +**SKH01 e' ~4x piu' sensibile di TP01** (−0.69% vs −0.09% di CAGR per bps/lato). Il motivo e' +strutturale: SKH01 fa round-trip **discreti** (~500 trade sui 7 anni per asset), TP01 e' una +posizione **continua vol-targeted** che ribilancia poco. Conseguenza operativa: **se il taker +salisse in modo serio, il primo parametro da rivedere e' il peso 75/25, non il resto**; se scende, +SKH01 e' quello che guadagna di piu'. + +--- + +## 2. I quattro punti dell'annuncio, uno per uno + +| punto | ci tocca? | +|---|---| +| **Taker piu' basso** | bene o neutro: a 3 bps il book va 1.766 → 1.799 di Sharpe. Nessuna decisione cambia. | +| **Maker rebate piu' basso** | **non oggi**: il book manda ordini **market** (taker). | +| **Liquidation fee 1%** | **irrilevante** — vedi sotto. | +| **Soglie VIP piu' basse + VIP7** | **irrilevante a $600**: volume 30g trascurabile, restiamo al tier base a qualunque soglia. | + +### Il maker tocca una raccomandazione non implementata + +L'idea **T1 del 26/07** — TP di SKH01 come **limit resting** per incassare il maker — perderebbe +parte della sua convenienza se il maker passa da rebate a **+2 bps**. Ma: (a) **non e' +implementata** (lo blocca il fatto che TP01 e SKH01 tradano lo stesso strumento con una sola +posizione netta), e (b) il beneficio misurato era quasi tutto *"convertire una lotteria in +certezza"*, non la fee. Quindi il cambio maker **non riapre** quella decisione, la rende solo +meno appetibile se un giorno si riaprisse. + +### Liquidation fee 1%: perche' e' irrilevante + +`live.json` impone `max_notional_per_asset_frac = 0.5` su **2 asset** → nozionale lordo massimo +**1.00x l'equity**, con `disaster_sl_pct = 0.30`. A leva ≤1x la liquidazione richiederebbe un +movimento avverso ~100%, e il disaster-SL interviene a −30%. + +⚠️ Questa conclusione **poggia sul cap**, non sulla strategia. Se il cap sale, decade. Per non +lasciarla valida per inerzia c'e' una **guardia di decisione** cablata +(`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`): alzare il cap **rompe il test** e +rimanda qui. + +--- + +## 3. Azione + +**Nessuna adesso.** Il 1 agosto: leggere il tier reale in Account Settings e confrontarlo con la +curva. Regola decisa in anticipo, per non decidere col numero davanti: + +* taker **≤5 bps/lato** → non si tocca nulla (il modello e' gia' giusto o conservativo); +* taker **>10 bps/lato** → rivedere per primo il peso di SKH01 (e' li' che morde 4x). + +**Cio' che il cambio NON puo' rompere:** tutti i backtest del progetto sono a 0.10% RT, quindi se +la fee scende diventano **conservativi** — la direzione dell'errore e' quella giusta. + +--- + +## 4. Regole trasferibili + +1. **Quando arriva un annuncio senza numeri, misurare la CURVA invece di aspettare il numero.** + Il risultato vale per qualunque valore esca, e sposta la decisione da "aspettiamo" a "sappiamo + gia' cosa fare in ogni caso". +2. **Dichiarare la qualita' della fonte.** Qui la tabella e' un'immagine non letta: i numeri del + nuovo schema sono **secondari** e vanno etichettati come tali, non usati come se fossero il + listino. +3. **Una conclusione che poggia su un parametro di config va legata a un test su quel parametro.** + "Liquidation fee irrilevante" e' vera *perche'* la leva e' ≤1x; senza guardia sopravviverebbe + al cambio di cap che la rende falsa. +4. **Mettere ogni effetto nella sua scala di grandezza.** Un raddoppio della fee vale meno della + fortuna d'ancora dello stesso book: senza il confronto si rischia di dedicare a una fee un + allarme che merita altro. diff --git a/scripts/research/r0726_fee_sensitivity.py b/scripts/research/r0726_fee_sensitivity.py new file mode 100644 index 0000000..a2b1e75 --- /dev/null +++ b/scripts/research/r0726_fee_sensitivity.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +"""r0726_fee_sensitivity.py — quanto costa (o rende) un cambio di fee Deribit al book live. + +CONTESTO: Deribit ha annunciato un nuovo schema fee **dal 1 agosto 2026** ("lower taker fees and +lower maker rebates" su futures/perpetual, soglie VIP piu' basse, nuovo VIP7, **liquidation fee +1% su tutti i prodotti**, spot temporaneamente a fee zero). La tabella numerica dell'articolo +Insights e' un'IMMAGINE: i numeri esatti per tier non sono stati letti da fonte primaria. + +Percio' qui NON si assume un numero nuovo: si misura la **curva**. Quando la tabella e' leggibile +si prende il valore giusto da questa curva, senza rifare l'analisi. + +Il progetto modella ovunque **0.05%/lato = 0.10% RT (taker Deribit)**: + `trend_portfolio.CANONICAL.fee_side = 0.0005`, `sleeves._skyhook_returns fee_rt=0.001`, + e i paper monitor con `FEE_SIDE = 0.0005`. + +COSA FA: + 1. curva Sharpe/CAGR del book live (TP01+SKH01 75/25) e dei due sleeve, al variare della fee; + 2. il **drag** in %/anno: quanto la fee toglie al rendimento lordo, per sleeve; + 3. il punto di pareggio (fee a cui il book va a Sharpe zero) — la distanza dal muro; + 4. esposizione alla **liquidation fee 1%**: la leva effettiva del book coi cap di `live.json`. + + uv run python scripts/research/r0726_fee_sensitivity.py +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +from src.backtest.harness import backtest_signals # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics, to_daily # noqa: E402 +from src.strategies.skyhook import SKH01_V2_DD, build_frames, skyhook_entries # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d, simple_returns) + +ASSETS = ("BTC", "ETH") +# bps PER LATO. 5 bps/lato = 0.10% RT = quello che il progetto modella oggi. +BPS_GRID = (0.0, 1.0, 2.0, 3.0, 5.0, 7.5, 10.0, 15.0) + + +def tp01_at_fee(fee_side: float) -> pd.Series: + cfg = dict(CANONICAL) + cfg["fee_side"] = fee_side + tp = TrendPortfolio(**cfg) + series = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + r = simple_returns(df["close"].values.astype(float)) + tgt = tp.target_series(df) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + + +def skh01_at_fee(fee_rt: float) -> pd.Series: + series = {} + for a in ASSETS: + ltf, htf = build_frames(load_data(a, "5m")) + ent = skyhook_entries(ltf, htf, SKH01_V2_DD) + m = backtest_signals(ltf, ent, fee_rt=fee_rt, leverage=1.0, asset=a, tf="230m") + s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True))) + series[a] = s.resample("1D").last().ffill().pct_change().dropna() + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + + +def main() -> None: + print("=" * 100) + print(" r0726 — SENSIBILITA' DEL BOOK LIVE ALLA FEE (annuncio Deribit, dal 2026-08-01)") + print("=" * 100) + print(" Il progetto modella 5 bps/lato = 0.10% RT (taker). La tabella nuova non e' stata") + print(" letta da fonte primaria (e' un'immagine): qui si misura la CURVA, non un numero.") + + rows = [] + for bps in BPS_GRID: + fs = bps / 10_000.0 + tp = tp01_at_fee(fs) + sk = skh01_at_fee(2.0 * fs) # fee_rt = 2 x fee per lato + book = combine_outer({"TP01": tp, "SKH01": sk}, + {"TP01": 0.75, "SKH01": 0.25}) + rows.append(dict(bps=bps, tp=metrics(tp), sk=metrics(sk), bk=metrics(book))) + + print(f"\n {'bps/lato':>9}{'%RT':>8} | {'TP01 Sh':>9}{'CAGR':>8} | {'SKH01 Sh':>10}{'CAGR':>8}" + f" | {'BOOK Sh':>9}{'CAGR':>8}{'maxDD':>8}") + for r in rows: + tag = " <- oggi" if r["bps"] == 5.0 else "" + print(f" {r['bps']:>9.1f}{r['bps']*2/100:>7.2f}% | " + f"{r['tp']['sharpe']:>9.3f}{r['tp']['cagr']*100:>7.2f}% | " + f"{r['sk']['sharpe']:>10.3f}{r['sk']['cagr']*100:>7.2f}% | " + f"{r['bk']['sharpe']:>9.3f}{r['bk']['cagr']*100:>7.2f}%" + f"{r['bk']['maxdd']*100:>7.2f}%{tag}") + + base = next(r for r in rows if r["bps"] == 5.0) + zero = next(r for r in rows if r["bps"] == 0.0) + print(f"\n DRAG della fee attuale (5 bps/lato) contro fee zero:") + for k, lab in (("tp", "TP01"), ("sk", "SKH01"), ("bk", "BOOK")): + d_sh = zero[k]["sharpe"] - base[k]["sharpe"] + d_cagr = (zero[k]["cagr"] - base[k]["cagr"]) * 100 + print(f" {lab:<7} Sharpe -{d_sh:.3f} CAGR -{d_cagr:.2f}%/anno") + + print(f"\n SENSIBILITA' MARGINALE intorno al punto attuale (per bps/lato):") + lo = next(r for r in rows if r["bps"] == 3.0) + hi = next(r for r in rows if r["bps"] == 7.5) + for k, lab in (("tp", "TP01"), ("sk", "SKH01"), ("bk", "BOOK")): + d = (hi[k]["sharpe"] - lo[k]["sharpe"]) / (7.5 - 3.0) + dc = (hi[k]["cagr"] - lo[k]["cagr"]) * 100 / (7.5 - 3.0) + print(f" {lab:<7} {d:+.4f} Sharpe/bps {dc:+.3f}% CAGR/bps") + + # ---------------------------------------------------- liquidation fee + print("\n" + "=" * 100) + print(" LIQUIDATION FEE 1% — ci tocca?") + print("=" * 100) + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = cfg.get("max_notional_per_asset_frac") + print(f" cap per asset: {frac:.0%} dell'equity (config: max_notional_per_asset_frac)") + print(f" asset tradati: {len(ASSETS)} -> nozionale lordo massimo = " + f"{frac*len(ASSETS):.0%} dell'equity") + print(f" leva effettiva massima: {frac*len(ASSETS):.2f}x (e i due asset sono spesso") + print(f" correlati, quindi il rischio non e' additivo come il nozionale)") + print(f" disaster-SL on-book: -{cfg.get('disaster_sl_pct', 0)*100:.0f}% sulla posizione netta") + print("\n A leva <= 1x la liquidazione richiederebbe un movimento avverso ~100%: il") + print(" disaster-SL a -30% interviene molto prima. La liquidation fee 1% e' quindi") + print(" IRRILEVANTE per questo book — diventerebbe rilevante solo alzando la leva,") + print(" che e' una decisione separata (e il 25/07 la valutava per il fronte prop).") + + +if __name__ == "__main__": + main() diff --git a/tests/test_fee_sensitivity.py b/tests/test_fee_sensitivity.py new file mode 100644 index 0000000..576d186 --- /dev/null +++ b/tests/test_fee_sensitivity.py @@ -0,0 +1,95 @@ +"""Test per r0726_fee_sensitivity.py — sensibilita' del book live al cambio fee Deribit. + +Due categorie: + * le repliche parametrizzate sulla fee devono riprodurre BIT-EXACT gli sleeve di produzione + alla fee canonica (5 bps/lato), altrimenti la curva descrive un'altra strategia; + * il test sul CAP e' una guardia di DECISIONE: la conclusione "liquidation fee 1% irrilevante" + poggia sulla leva <= 1x che discende da `live.json`. Se qualcuno alza il cap, quella + conclusione decade e il test deve dirlo invece di lasciarla in giro valida per inerzia. +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0726_fee_sensitivity as FS # noqa: E402 + + +# =========================================================================== +# le repliche alla fee canonica == produzione +# =========================================================================== +def test_tp01_alla_fee_canonica_riproduce_lo_sleeve(): + from src.portfolio.portfolio import to_daily + from src.portfolio.sleeves import _tp01_returns + from src.strategies.trend_portfolio import CANONICAL + + mine = FS.tp01_at_fee(CANONICAL["fee_side"]) + ref = to_daily(_tp01_returns()) + assert len(mine) == len(ref) + assert float(np.max(np.abs(mine.values - ref.values))) == 0.0 + + +def test_skh01_alla_fee_canonica_riproduce_lo_sleeve(): + from src.portfolio.portfolio import to_daily + from src.portfolio.sleeves import _skyhook_returns + + mine = FS.skh01_at_fee(0.001) # fee_rt canonico dello sleeve + ref = to_daily(_skyhook_returns()) + assert len(mine) == len(ref) + assert float(np.max(np.abs(mine.values - ref.values))) == 0.0 + + +def test_la_fee_canonica_del_progetto_e_5bps_per_lato(): + """Se qualcuno cambia il default, la curva va ri-ancorata: 'oggi' non sarebbe piu' 5 bps.""" + from src.strategies.trend_portfolio import CANONICAL + + assert CANONICAL["fee_side"] == pytest.approx(0.0005) + assert 5.0 in FS.BPS_GRID + + +# =========================================================================== +# monotonicita': piu' fee, meno rendimento +# =========================================================================== +def test_piu_fee_meno_sharpe_e_meno_cagr(): + from src.portfolio.portfolio import metrics + + lo = metrics(FS.tp01_at_fee(0.0)) + hi = metrics(FS.tp01_at_fee(0.0015)) + assert lo["sharpe"] > hi["sharpe"] + assert lo["cagr"] > hi["cagr"] + + +def test_skh01_e_piu_sensibile_alla_fee_di_tp01(): + """Il fatto che guida la raccomandazione (se il taker salisse, rivedere il peso di SKH01): + SKH01 fa round-trip discreti, TP01 e' una posizione lenta vol-targeted.""" + from src.portfolio.portfolio import metrics + + d_tp = metrics(FS.tp01_at_fee(0.0))["cagr"] - metrics(FS.tp01_at_fee(0.001))["cagr"] + d_sk = metrics(FS.skh01_at_fee(0.0))["cagr"] - metrics(FS.skh01_at_fee(0.002))["cagr"] + assert d_sk > d_tp + + +# =========================================================================== +# guardia di decisione: la leva su cui poggia "liquidation fee irrilevante" +# =========================================================================== +def test_leva_massima_da_config_resta_sotto_o_uguale_a_1x(): + """La conclusione 'liquidation fee 1% irrilevante' vale perche' il cap di live.json + limita il nozionale lordo a <= 1x l'equity (2 asset x equity/2), con disaster-SL a -30%. + Se il cap sale, quella conclusione NON vale piu': questo test deve rompersi.""" + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = cfg.get("max_notional_per_asset_frac") + assert frac is not None, "cap dinamico rimosso: rivedere l'analisi liquidation fee" + leva_max = frac * len(FS.ASSETS) + assert leva_max <= 1.0, ( + f"leva massima {leva_max:.2f}x > 1x: la liquidation fee 1% torna rilevante, " + "rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md") + assert cfg.get("disaster_sl_pct", 0) > 0, "disaster-SL disattivato: idem"