13 Commits

Author SHA1 Message Date
Adriano Dal Pastro 8d559c6f33 merge(research): gamma-scalp + cash-carry + TP01×DVOL + vol-termstructure
Analisi onesta di 5 angoli (tutti su dati certificati, leak-free, netto fee,
scoring marginale vs TP01, vincoli eseguibilita' a $600):
- gamma scalping (scalp+opzioni)  -> SCARTATO (specchio del VRP, perde ogni freq)
- funding cross-sectional (FC01)  -> gia' morto (DILUTES)
- cash-and-carry (CC01)           -> premio reale ma Sharpe artefatto, STAT-MODE -> lead
- TP01 x DVOL vol-targeting       -> non migliora (de-levering, non timing)
- calendar-vol / term-structure   -> dato storico non pubblico -> forward logger (gia' su main)

Zero impatto sul codice live: solo script di ricerca + test + diari + note CLAUDE.md.
Il forward logger era gia' stato cablato in cron su main (555977d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:28:54 +00:00
Adriano Dal Pastro 555977d987 feat(research): forward logger della vol term-structure + cron giornaliero
La storia per-scadenza non e' pubblica su Deribit -> calendar-vol non backtestabile
ora (data-first gate). Unica via: costruire il dato in avanti. Aggiunto logger
idempotente (interpola ATM IV a tenor fissi {7,30,60,90,180}g -> data/raw/vol_term_*)
+ wrapper cron giornaliero. SOLO ricerca forward: non tocca il book live ne' i dati
certificati. Analisi completa (gamma scalp/cash-carry/dvol/feasibility) sul branch
research/gamma-scalp-options.

- scripts/research/log_vol_termstructure.py + probe_vol_termstructure.py
- scripts/cron_vol_term.sh
- tests/test_vol_termstructure.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:27:15 +00:00
Adriano Dal Pastro 8c3868cb31 research(vol): calendar-vol non backtestabile — data-first gate + forward logger
Angolo term-structure DVOL / calendar-vol. Scan di fattibilita' PRIMA del backtest:
la storia per-scadenza NON e' pubblica su Deribit (DVOL solo 30g; trade-history IV
solo per strumenti vivi; il front rotola/espira -> serie continua front-vs-back
irricostruibile). Per la metodologia (niente edge senza OOS su dati certi) -> STOP,
niente backtest su uno snapshot.

Unica via legittima: costruire il dato IN AVANTI. Aggiunto logger forward idempotente
che interpola l'ATM IV a tenor fissi {7,30,60,90,180}g e accumula data/raw/vol_term_*.
Seminati i primi snapshot (oggi: contango lieve su BTC/ETH). Non auto-cablato in cron.

- scripts/research/probe_vol_termstructure.py (scan fattibilita')
- scripts/research/log_vol_termstructure.py (forward dataset builder)
- tests/test_vol_termstructure.py (interpolazione offline)
- docs/diary/2026-06-26-vol-termstructure-feasibility.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:18:59 +00:00
Adriano Dal Pastro 9d586eeb58 research(tp01): DVOL vol-targeting non migliora TP01 (de-levering, non timing)
Angolo ESEGUIBILE (tocca il book live): DVOL (vol implicita forward-looking) come
denominatore del vol-target invece della realizzata. Finestra comune 2021-2026.

Le varianti DVOL abbassano DD (12.3->9.2%) ma anche Sharpe FULL (0.75->0.70) e CAGR.
Controllo decisivo: realized @ vol-tgt 15% eguaglia quel DD (9.4%) a Sharpe piu' alto
(0.75) -> il taglio di DD del DVOL e' solo DE-LEVERING, replicabile meglio con un
target_vol piu' basso. Hold-out +0.06 = single-window (storia DVOL <5y), sotto la
soglia multi-cut. Gate DVOL-spike ridondante col trend (TP01 gia' flat nei crash).

Lezione: per meno DD sul live la leva e' target_vol, non un overlay DVOL.

- scripts/research/tp01_dvol_overlay.py (realized/dvol/blend/max/derisk + controllo target_vol)
- tests/test_tp01_dvol_overlay.py
- docs/diary/2026-06-26-tp01-dvol-overlay.md
- CLAUDE.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:56:56 +00:00
Adriano Dal Pastro 69e3517f1b research(carry): cash-and-carry CC01 — premio reale, Sharpe artefatto, non deployabile
Provato l'angolo basis/cash-and-carry (l'altro proposto; il funding cross-sectional
FC01 era gia' SCARTATO 2026-06-22). Delta-neutral long-spot/short-perp -> r=funding,
zero esposizione prezzo.

Premio di funding REALE (~+8-14%/anno aggregato, positivo ogni anno in-sample,
ortogonale a TP01). MA Sharpe modellato 11-13 = ARTEFATTO: rischi di coda fuori
dal dataset (manca il 2022; procyclico +23% 2024 -> +1.7% 2026; liquidazione/slippage
non modellati). Il mark-to-market della base (premium col) sgonfia solo 13->11 ->
il basis-from-data non e' il rischio vero. NON eseguibile a $600 (spot+perp, funding
HL non Deribit) -> STAT-MODE. LEAD da rivedere a scala, non uno sleeve.

Sottoprodotto: CC01 passa ogni gate del marginal scorer -> punto cieco (manca un
gate 'Sharpe implausibile -> rischio nascosto').

- scripts/research/cash_carry_hl.py (CC-static/gated, BTC-ETH + 19-major, basis MtM, reality-check)
- tests/test_cash_carry.py (lock del fatto economico, non dello Sharpe)
- docs/diary/2026-06-26-cash-carry-hl.md
- CLAUDE.md: riga nella sintesi di ricerca

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:46:14 +00:00
Adriano Dal Pastro cf72e395d3 research(options): gamma scalping (long-vol) — SCARTATO
"Scalping BTC/ETH con copertura in opzioni" = gamma scalping, lo specchio
esatto del VRP01 (long straddle ATM + delta-hedge -> incassa RV-IV).

Esito: perde ogni anno / ogni variante / ogni frequenza (Sharpe -3 a -6).
Diagnostica strutturale: a 1d IV>=RV (paghi il VRP); a 1h RV>IV gross ma il
rehedge orario paga 24x la fee di hedge -> variante peggiore. Marginale vs TP01
= DILUTES, non e' nemmeno un hedge. Muro eseguibilita': opzione BTC min $5968 >> $600.

Schiacciato tra due muri: rehedge lento = premio, veloce = fee -> nessuna
frequenza vince. Il VRP01 (lato short, gated) resta l'unico edge opzioni.

- scripts/research/options_gamma_scalp.py (harness: naked/cheap/rich, 1d+1h, diagnostica RV-IV)
- tests/test_gamma_scalp.py (lock della conclusione)
- docs/diary/2026-06-26-gamma-scalp-options.md
- CLAUDE.md: riga nella sintesi di ricerca

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:36:48 +00:00
Adriano Dal Pastro db738bce3b feat(live): arma il BOOK DERIBIT (TP01+SKH01 nettati in software)
Estende l'esecuzione live da TP01-only al book Deribit-only completo. TP01 e SKH01
tradano lo STESSO strumento (una sola posizione netta per conto su Deribit) -> netting
in software: un solo ordine/asset verso il target netto.

- src/live/book.py: target NETTO per asset = clamp(0.5*E*(0.75*tp_frac + 0.25*skh_sign), ±cap).
  Riusa current_target (TP01, causale) + _skyhook_positions (segno L/S, book 230m) + conto reale.
- src/live/execution.py: rebalance_signed() — reconcile CON SEGNO (long/short, flip via close+open,
  reduce reduce_only). La close resta sempre permessa (si esce da qualunque posizione).
- src/live/livefeed.py: fresh_5m() — feed 5m certificato + coda recente EFFIMERA da Deribit pubblico
  (stesso simbolo inverse, in-memory, NON scrive su disco -> dati certificati intatti; fallback al
  certificato su errore). Solo SKH01 ne ha bisogno (e' a 230m); TP01 e' giornaliero.
- scripts/live/book_execute.py: executor doppio-gate (config + --execute), disaster-SL on-book sulla
  posizione netta, log in data/live/book_executions.jsonl. Feed SKH fresco (live_feed=True).
- scripts/cron_book.sh + crontab ORARIO: book idempotente ogni ora (riconcilia al netto corrente);
  rimossa la riga live_execute.py (TP01-only) dal cron daily per non far collidere i due.
- config/live.json: ARMATO (execution_enabled=true). cap/asset $300 split 75/25, disaster-SL -30%.
  Tutto flat all'arming -> nessun ordine finche' un segnale non arma.
- tests/test_book_live.py: 20 test (sizing 75/25, cap, flip/close/reduce, parita' pesi backtest,
  gate-safety, reconcile con trader fittizio, merge/dedup feed + fallback, loader onorato). 76/76 pass.

CAVEAT: exit SKH SOFTWARE (latenza fino a fine barra 230m; solo disaster-SL on-book); TP01 scende a
peso 0.75 (max $225/asset); SKH01 resta research-grade (equity daily-step, margine DD ETH sottile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:43:27 +00:00
Adriano Dal Pastro 25a22fc7c1 feat(dashboard): riordino LIVE→trades→storico + trade/anno contati
- dashboard riorganizzata in 3 sezioni: ① LIVE (mainnet sola lettura) in
  alto, ② TRADES ESEGUITI (reali) + frequenza operativa al centro,
  ③ STORICO (backtest/forward simulato) in fondo (COMBO/BOOK/FORWARD come ③·a/b/c).
- book_trade_frequency() in sleeves.py: trade/anno CONTATI sui dati certificati
  (cache di modulo, una volta per processo). SKH01 round-trip BTC ~37 / ETH ~43
  -> ~75/anno combinato; TP01 turnover ~7x/anno. Card "trade/anno" + blocco freq.
- fix: collisione var `pos` nel ramo shadow-online (-> shpos) che avrebbe rotto
  la tabella posizioni se il conto fosse leggibile dal container.
- fix: turnover TP01 nan (disallineamento groupby) -> groupby posizionale, 7.2x/anno.
- 56 test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:18:52 +00:00
Adriano Dal Pastro eeac97dde4 feat(dashboard): Deribit-only book panel (TP01+SKH01) + accumulation forecast
New section showing the executable Deribit-only book (TP01 75% + SKH01 25%): combined
FULL/HOLD Sharpe+DD, plus the reinvest-winnings accumulation projection (historical &
conservative CAGR, €5k→5y/10y, conservative €/day run-rate). Reuses the already-computed
sleeve daily series (no extra heavy compute). Honest caveats (bull sample, no leverage,
SKH01 not live, ~€177k for €50/day).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:43:47 +00:00
Adriano Dal Pastro c8a390d6b7 feat(portfolio): reproducible accumulation forecast for the Deribit book
scripts/portfolio/forecast_deribit_book.py — projects the Deribit-only book (TP01+SKH01)
forward by pure compounding (reinvest winnings), monthly-aligned, NO external contributions
(not a PAC). Deterministic @historical & @conservative CAGR + Monte-Carlo block-bootstrap
(median/p10/p90), plus €/day run-rate @conservative. Parametric (--capital/--years/--cons-frac).
Honest caveats baked in (bull-crypto sample, no leverage, SKH01 not live, conditional projection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:42:14 +00:00
Adriano Dal Pastro 384b9cb0af feat(skyhook): pos_fn introspection for SKH01 sleeve (current open trade / flat)
_skyhook_positions(): replays the non-overlap entry+exit logic (TP/SL/max_bars) to the last
closed 230m bar and reports, per asset, the current OPEN trade (dir/entry/sl/tp/bars_in) or
'flat'. Wired into skyhook_sleeve(pos_fn=...) so the Deribit book report & web dashboard show
Skyhook's live position. Causal (closed bars only). +1 test. Currently flat/flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:32:14 +00:00
Adriano Dal Pastro 160ad300be feat(portfolio): Deribit-only executable book (TP01+SKH01) + periodic rebalancing
- deribit_book_sleeves(): TP01 75% + SKH01 25% — the two directional BTC/ETH legs on
  ONE venue (Deribit), both since 2019. Excludes XS01 (Hyperliquid/stat-mode) & VRP01
  (modeled options). FULL Sharpe 1.78 / HOLD 1.17 / DD 9.4% (research).
- rebalance_sim(): realistic PERIODIC rebalancing (drift between dates, turnover cost at
  Deribit-taker ~5bps/side) vs the idealized continuous rebalance of combined_daily.
  period=1 + cost=0 reduces to continuous (tested).
- run_deribit_book.py: report — continuous vs weekly/biweekly/monthly rebal, per-year,
  accumulation €2k & $600-real, min-order $5 note. Finding: turnover is LOW (0.2-0.4x/yr),
  so monthly rebal (€7,919) ~= continuous (€7,938) — cost is negligible; daily would be
  sub-min-order fiction at $600 -> use >= weekly.
- +2 tests (rebalance_sim continuity & cost). Full suite green.

TP01 is the only live-armed leg; SKH01 is the candidate 2nd leg (validate execution code first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:26:53 +00:00
Adriano Dal Pastro 50e2adf837 merge(skyhook): SKH01-V2-DD strategy + 4-sleeve portfolio + dashboard
Brings the Skyhook line into main:
- SKH01 dual-TF regime+breakout engine (BTC/ETH, causal, honest harness)
- 2 multi-agent research waves -> SKH01-V2-DD (asymmetric %-exits, standalone
  maxDD <30%, minHold +1.26, marginal ADDS vs TP01)
- wired as 4th portfolio sleeve @25% effective: FULL Sharpe 1.68->2.13, DD 14->8%
- dashboard shows the 4-sleeve view; tests 25 pass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:42:15 +00:00
30 changed files with 2393 additions and 19 deletions
+36
View File
@@ -96,6 +96,42 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
regime-luck), calendar/seasonality (buy&hold travestito), volume/vol e momentum-reversal (negativi). regime-luck), calendar/seasonality (buy&hold travestito), volume/vol e momentum-reversal (negativi).
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia - **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia
libreria +201%/+1238% era contaminazione); trend 5m/15m (fee). libreria +201%/+1238% era contaminazione); trend 5m/15m (fee).
- **GAMMA SCALPING (long-vol) "scalping BTC/ETH con copertura in opzioni" — SCARTATO (2026-06-26)** —
`scripts/research/options_gamma_scalp.py`, test `tests/test_gamma_scalp.py`. È lo **specchio
esatto del VRP01** (long straddle ATM + delta-hedge: incassa **RVIV**, dove VRP01 incassa IVRV).
Perde **ogni anno, ogni variante, ogni frequenza** (Sharpe 3 a 6; nudo/cheap-gated/rich-skip;
rehedge 1d e 1h). Diagnostica strutturale: a 1d IV≈o>RV (BTC +4.9pp) → paghi il VRP; a 1h RV>IV
gross ma (a) gonfiata da microstruttura, (b) il rehedge orario paga **24× la fee di hedge**
variante *peggiore* (6). Marginale vs TP01 = **DILUTES**, non è nemmeno hedge (perde sia TP01-up
sia TP01-down). Muro eseguibilità: opzione BTC min $5.968 ≫ $600. Schiacciato tra due muri:
rehedge lento = premio, veloce = fee → **nessuna frequenza vince.** Regola gemella del VRP:
*niente long-vol scalp da modello in deploy*. Il VRP01 (lato short, gated) resta l'unico edge
opzioni — funziona perché sta sul lato *giusto* dello stesso premio. Diario `2026-06-26-gamma-scalp-options.md`.
- **CASH-AND-CARRY (basis trade) "CC01" — premio REALE, Sharpe ARTEFATTO, NON deployabile (2026-06-26)** —
`scripts/research/cash_carry_hl.py`, test `tests/test_cash_carry.py`. Diverso da FC01 (funding
cross-sectional, già scartato): qui delta-neutral long-spot/short-perp sullo *stesso* asset →
ritorno ≈ **+funding** (zero esposizione prezzo). Il premio di funding è **reale** (~+8-14%/anno
aggregato, positivo ogni anno in-sample, ortogonale a TP01 corr ~0.05). MA lo **Sharpe modellato
11-13 (DD 0.3%) è un ARTEFATTO**: il modello cattura solo il cashflow liscio del funding e i
rischi di coda sono **strutturalmente fuori dal dataset** — (1) storico funding dal 2023-05 →
**manca il 2022** (deleveraging, funding , basis blow-out); (2) **procyclico** (carry +23% toro
2024 → +1.7% bear 2026, si comprime quando servirebbe); (3) liquidazione short/slippage non
modellati. Il mark-to-market della base (`premium` col → r=funding−Δpremium) sgonfia lo Sharpe
solo 13→11 → il basis-from-data NON è il rischio vero. Sharpe reale di un basis-trade ~1-3 con
code brusche. **NON eseguibile a $600** (spot+perp = 4-38 gambe, funding HL non Deribit) → STAT-MODE.
**LEAD da rivedere a scala (~20k+ e venue con funding eseguibile), non uno sleeve.** Sottoprodotto:
CC01 passa OGNI gate del marginal scorer → **punto cieco** (manca un gate "Sharpe implausibile →
rischio nascosto"; prossima indurita raccomandata). Diario `2026-06-26-cash-carry-hl.md`.
- **TP01 × DVOL vol-targeting — NON migliora (2026-06-26)** — `scripts/research/tp01_dvol_overlay.py`,
test `tests/test_tp01_dvol_overlay.py`. Angolo ESEGUIBILE (tocca il book live, non STAT-MODE):
usare il DVOL (vol implicita forward-looking) come denominatore del vol-target di TP01 invece
della vol realizzata. Su finestra comune 2021-2026: le varianti DVOL abbassano il DD (12.3%→9.2%)
ma anche Sharpe FULL (0.75→0.70) e CAGR (8%→6%). **Controllo decisivo:** il realized a target_vol
RIDOTTO (15%) eguaglia quel DD (9.4%) a Sharpe **più alto** (0.75) → il taglio di DD del DVOL è
solo **de-levering**, replicabile meglio con un semplice target_vol più basso. L'unico residuo
(hold-out +0.06) è single-window (storia DVOL <5 anni) → sotto la soglia multi-cut. Il gate
DVOL-spike de-risk è **ridondante col trend** (TP01 già flat nei crash, Δ 0.00). **Lezione: per meno
DD sul live la leva è `target_vol`, non un overlay DVOL** (20% resta canonico). Diario `2026-06-26-tp01-dvol-overlay.md`.
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: - **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. 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.** - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"_nota": "Config esecuzione LIVE di TP01. execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-20.", "_nota": "Config esecuzione LIVE del BOOK DERIBIT (TP01+SKH01 nettati in software). execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-23: esecutore scripts/live/book_execute.py via cron ORARIO scripts/cron_book.sh (SKH01 e' a 230m). cap/asset $300 split 75/25, disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.",
"execution_enabled": true, "execution_enabled": true,
"max_notional_per_asset_usd": 300, "max_notional_per_asset_usd": 300,
"min_order_usd": 5, "min_order_usd": 5,
+69
View File
@@ -0,0 +1,69 @@
# 2026-06-26 — Cash-and-carry (basis trade) delta-neutral su HL (CC01): premio REALE, Sharpe ARTEFATTO, NON deployabile
**Goal utente:** dopo lo scarto del gamma scalping, "provare" gli altri due angoli proposti —
funding-carry cross-sectional e basis/cash-and-carry. Il **funding cross-sectional (FC01) è già
SCARTATO** (diario 2026-06-22: DILUTES, sign-flip su un nome). L'angolo non testato è il
**cash-and-carry delta-neutral (CC01)**. Branch `research/gamma-scalp-options`,
script `scripts/research/cash_carry_hl.py`.
## Meccanismo (diverso da FC01)
FC01 pickava cross-section (short alto-funding / long basso-funding) → scommessa relativa, fragile.
CC01 **non picka**: harvesta il premio di funding AGGREGATO restando delta-neutral sullo *stesso*
asset. Long spot + short perp (stessa size): le gambe lineari nel prezzo si cancellano → ritorno
**+funding** (lo short incassa il funding quando f>0), zero esposizione direzionale. Dati: funding
orario reale HL dal 2023-05 (`hlfund_*_1h.parquet`), gli stessi 19 major di XS01.
## Risultato grezzo — e perché NON va creduto
| config | Sharpe | CAGR | maxDD | per-anno |
|---|---|---|---|---|
| BTC/ETH CC-static | **+12.9** | +13% | 0.8% | 2023:+10 2024:+23 2025:+8 2026:+1% |
| BTC/ETH CC-gated | +13.9 | +14% | 0.3% | tutti positivi |
| 19-major CC-gated | +11.6 | +16% | 0.4% | tutti positivi |
**Uno Sharpe di 13 con DD 0.3% è un ALLARME, non un edge.** È la firma di un modello che cattura
solo un cashflow liscio (il funding) e azzera la volatilità reale. Il marginal scorer dà **ADDS /
robust_oos / has_insample_edge=True / multicut_persistent** su OGNI config — ma questo **espone un
punto cieco dello scorer**: si fida della vol riportata dal candidato e non ha un gate "Sharpe
implausibile → rischio nascosto". Un Sharpe di 13 dovrebbe squalificare-e-indagare, non passare.
## Il rischio è NASCOSTO, non assente
- **Mark-to-market della base** (`premium` col → `r = funding Δpremium`): sgonfia lo Sharpe solo
13→11. La colonna premium di HL è già lisciata/clampata → a risoluzione daily il basis-vol è
minimo. **Quindi il basis-from-data NON è il rischio vero.**
- **I rischi che giustificherebbero uno Sharpe realistico (~1-3) sono ASSENTI dal dataset:**
1. **2022 mancante.** Lo storico funding parte 2023-05 → niente deleveraging LUNA/3AC/FTX, il
regime di funding profondamente negativo + basis blow-out che farebbe il vero drawdown.
2. **Procyclicità.** Funding aggregato BTC/ETH: +23% nel toro 2024 → **+1.7% nel bear 2026**
(e 19-major CC-static 2026 = **1%**). Solo 10% di giorni a funding negativo *in-sample* — ma
in un vero bear quella frazione esplode. Il carry **non è all-weather**: è un risk-on premium.
3. **Liquidazione dello short** in uno squeeze e **slippage** su spot+perp: non modellati.
- **Sharpe reale di un crypto basis-trade ≈ 1-3 con code brusche** (Mar-2020, Mag-2021, FTX), non 13.
## Eseguibilità a $600 — muro indipendente
Serve spot **+** perp per gamba: BTC/ETH = 4 posizioni, 19-major = 38. Su Deribit (dove operiamo) lo
storico funding è **bloccato** (404) e non tradiamo spot HL. **STAT-MODE**, come XS01/VRP01 ma peggio
(doppia gamba per asset). Non deployabile a questo capitale.
## Verdetto
**CC01 = premio di funding REALE (~+8-14%/anno aggregato, positivo ogni anno in-sample) e
genuinamente ortogonale a TP01 (corr ~0.05, NON direzionale).** È la "fonte di ritorno diversa" più
concreta trovata finora. MA:
- lo **Sharpe headline è fiction** (rischi di coda strutturalmente fuori dal dataset);
- è **procyclico** (si comprime/inverte nel bear — proprio quando servirebbe);
- **non eseguibile** a $600 / sul nostro venue.
**LEAD da rivedere a scala** (capitale ~20k+ e un venue con funding eseguibile), **non uno sleeve**.
Nessun impatto sul book live (branch separato).
## Sottoprodotto metodologico (raccomandazione)
CC01 ha passato ogni gate del marginal scorer pur essendo un'illusione di rischio. **Prossima
indurita dello scorer:** un gate di *plausibilità dello Sharpe* — uno standalone Sharpe ≫ 4 deve
declassare a "SUSPECT/INVESTIGATE" invece di alimentare ADDS, perché segnala vol sottostimata
(mark-to-cashflow, dati che escludono il regime di stress, leva nascosta). Documentato qui, non
implementato in questa passata per non toccare lo scorer condiviso su `main`.
@@ -0,0 +1,74 @@
# 2026-06-26 — "Scalping BTC/ETH con copertura in opzioni" = gamma scalping → SCARTATO
**Domanda (goal utente):** analizzare lo scalping su BTC/ETH con copertura in opzioni come
possibile nuovo sleeve. **Esito: negativo, strutturale, su tutte le frequenze. Non è uno sleeve,
non è nemmeno un hedge.** Branch `research/gamma-scalp-options`, script
`scripts/research/options_gamma_scalp.py`.
## Cos'è davvero "scalping con copertura in opzioni"
L'interpretazione rigorosa è il **gamma scalping**: compri un'opzione (la *copertura* = long
gamma), delta-hedgi il sottostante a cadenza fissa (lo *scalp*), e il P&L netto è
≈ dollar-gamma × (vol realizzata² vol implicita²). È lo **specchio esatto del VRP01** (short-vol):
VRP01 incassa IVRV (positivo in media), il gamma scalping incassa **RVIV** (negativo in media).
Modello (mirror della struttura VRP per comparabilità): long straddle ATM, tenor 7g, IV = DVOL
Deribit (la stessa fonte del VRP), delta-hedge sui prezzi certificati, fee opzioni cap 12.5% del
premio + fee perp 0.05%/lato sull'hedge turnover, return-on-notional vol-targeted 20%.
## La diagnostica strutturale (il cuore)
| asset | IV (DVOL) | RV 1d | IVRV 1d | RV 1h | IVRV 1h |
|---|---|---|---|---|---|
| BTC | 60.9% | 56.0% | **+4.9pp** | 64.5% | 3.6pp |
| ETH | 74.9% | 76.0% | 1.0pp | 81.0% | 6.1pp |
- A **rehedge giornaliero** l'IV ≈ o > la RV → il long gamma **paga il VRP** (sei sul lato sbagliato
del premio). Perde gross, prima di ogni fee.
- A **rehedge orario** la RV *supera* l'IV (BTC +3.6pp, ETH +6.1pp gross) — sembrerebbe un edge…
**ma è una trappola doppia:** (a) la RV_1h è gonfiata da microstruttura (bid-ask bounce), quindi
il "gross edge" è in parte fittizio; (b) il rehedge orario paga **24× la fee di hedge**, che
spazza via qualunque edge intraday reale.
## I numeri (ogni variante, ogni anno, ogni frequenza: PERDE)
| variante | rehedge | Sharpe | CAGR | per-anno |
|---|---|---|---|---|
| NAKED | 1d | **3.99** | 56% | 2021..2026 tutti 25..68% |
| CHEAP-GATED (IVr<0.30) | 1d | 3.05 | 47% | tutti negativi |
| RICH-SKIP (IVr>0.90) | 1d | 3.92 | 55% | tutti negativi |
| NAKED | 1h | **6.06** | 71% | tutti 38..81% (peggio: le fee) |
| CHEAP-GATED (IVr<0.30) | 1h | 4.96 | 64% | tutti negativi |
Il gate "compra vol solo quando è a sconto" (lo specchio del gate vincente del VRP, *vendi vol solo
quando è cara*) **migliora ma non salva**: 3.05 invece di 3.99. Il segno resta negativo perché
l'IV-rank basso non garantisce RV > IV su 7 giorni.
## Scoring marginale vs TP01
Tutte le varianti: **DILUTES**, `is_hedge=False`, `has_insample_edge=False` (IS Sharpe 3..4.5),
blend-25 uplift hold 1.2/1.5. Non aiuta nemmeno come hedge: perde sia quando TP01 è su
(uplift 1.2..1.6) sia quando TP01 è giù (uplift 2.5..3.2). Corr a TP01 ≈ 0 ma il drift negativo
domina la matematica di diversificazione.
## Eseguibilità a ~$600 (muro indipendente)
- **BTC**: contratto 1 BTC, min 0.1 BTC → notional minimo **$5.968** ≫ $600 → **NON eseguibile**.
- **ETH**: min 0.1 ETH → **$157** → marginalmente OK, ma un solo straddle = ~25% del conto in premio.
Anche se l'edge esistesse, BTC è fuori e ETH è troppo grosso per diversificare.
## Conclusione
Lo "scalping con copertura in opzioni" è **schiacciato tra due muri**: rehedge lento → paghi il
premio (RV < IV); rehedge veloce → paghi le fee (24×/day). **Nessuna frequenza vince.** Conferma
due lezioni di prim'ordine del progetto, ora quantificate sul long-vol:
1. **Le fee uccidono lo scalping** (il rehedge orario, vera frequenza di scalping, è la variante
*peggiore*: 6 Sharpe).
2. **Vol-da-modello è fragile in entrambe le direzioni.** Regola gemella del VRP: *niente long-vol
scalp da modello in deploy*.
**Il VRP01 (lato short, gated, rischio-definito) resta l'unico edge opzioni del progetto**
e funziona proprio perché sta sul lato *giusto* dello stesso premio che il gamma scalp paga.
SCARTATO. Script preservato come riferimento. Nessun impatto sul book live (branch separato).
@@ -0,0 +1,66 @@
# 2026-06-26 — TP01 × DVOL vol-targeting: NON migliora (il taglio di DD è solo de-levering)
**Goal utente:** "continua" ad analizzare strategie. Dopo i tre angoli di diversificazione tutti
STAT-MODE (gamma scalp scartato, funding cross-sectional già morto, cash-and-carry lead non
deployabile), ho puntato sull'angolo a **più alto valore perché ESEGUIBILE**: migliorare il
risk-sizing del book **live** TP01 (BTC/ETH perp Deribit, già armato). Branch
`research/gamma-scalp-options`, script `scripts/research/tp01_dvol_overlay.py`.
## Ipotesi
TP01 vol-targeta sulla vol **realizzata** 30g (backward-looking). Il **DVOL** (vol implicita 30g
Deribit, forward-looking, che spesso anticipa i salti di vol) come denominatore → de-risking più
tempestivo prima dei crash → DD più basso / hold-out migliore senza affondare il FULL.
Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla **finestra comune 2021-04 → 2026**
(1891g, perdo il toro 2019-2020 pre-DVOL). Causale, fee 0.10% RT, config canonica TP01.
## Risultati (finestra comune, hold-out 2025+)
| variante (denominatore vol) | FULL Sh | FULL DD | CAGR | HOLD Sh |
|---|---|---|---|---|
| **realized** (baseline) | **+0.75** | 12.3% | +8% | +0.30 |
| dvol (implicita) | +0.70 | 9.3% | +6% | +0.33 |
| blend 50/50 | +0.72 | 10.4% | +7% | +0.32 |
| max(realiz, dvol) | +0.70 | 9.2% | +6% | +0.36 |
| derisk (gate DVOL>p90 ×0.5) | +0.75 | 12.3% | +8% | +0.30 |
Le varianti DVOL **abbassano il DD** (12.3% → ~9.2%) ma **anche** Sharpe FULL (0.04/0.05) e CAGR
(8% → 6%). Lo Sharpe è scale-invariant → sospetto: il taglio di DD è solo *posizioni più piccole*
(l'IV > RV per il VRP → denominatore più grande → leva minore), non vero timing.
## Il controllo decisivo
Confronto le varianti DVOL col **realized a target_vol RIDOTTO** (stesso de-levering, senza DVOL):
| | FULL Sh | DD | CAGR | HOLD Sh |
|---|---|---|---|---|
| **max-DVOL** | +0.70 | 9.2% | +6% | +0.36 |
| **realized @ vol-tgt 15%** | **+0.75** | 9.4% | +6% | +0.30 |
| **realized @ vol-tgt 13%** | +0.75 | **8.1%** | +5% | +0.30 |
**Conclusivo:** il realized a 15% eguaglia il DD del max-DVOL (9.4% ≈ 9.2%) a Sharpe **più alto**
(0.75 > 0.70) e stesso CAGR; a 13% fa DD ancora più basso (8.1%), sempre a 0.75. **Il taglio di DD
del DVOL è interamente "leva minore", replicabile MEGLIO con un semplice target_vol più basso.**
## Verdetto
**DVOL vol-targeting NON migliora TP01.** Risk-adjusted è marginalmente *peggiore* (FULL 0.70 vs
0.75 a parità di DD). L'unico residuo a favore — hold-out +0.06 (max 0.36 vs 0.30) — è **single-window**
su 5 anni scarsi di storia DVOL: sotto la soglia di **persistenza multi-cut** del progetto (la lezione
che ha ucciso 13/14 falsi positivi nello sweep alt). Il gate **DVOL-spike de-risk è RIDONDANTE col
trend**: quando il DVOL esplode (crash) TP01 è già flat (momentum negativo → posizione 0) → il gate
non tocca nulla (Δ 0.00 su tutto). Niente da deployare.
## Lezioni
1. **Per meno DD sul book live, la leva giusta è `target_vol`, non un overlay DVOL.** Abbassare TP01
da vol-tgt 20% → 15% taglia il DD 12.3% → ~9.4% allo **stesso Sharpe** (0.75). Dial pulito e noto,
se mai si volesse un profilo più difensivo (oggi 20% è la scelta canonica — invariata).
2. **La vol implicita non batte la realizzata come denominatore di sizing per un trend long-flat**:
il segnale di trend gestisce già il de-risking direzionale; il denominatore fissa solo la
magnitudine, e la realizzata lo fa marginalmente meglio per unità di rischio.
3. Conferma indiretta del soffitto: anche un raffinamento "ovvio" e *eseguibile* su TP01 non produce
alpha; al massimo sposta il punto sulla stessa frontiera rischio/rendimento.
SCARTATO come miglioria. Nessun impatto sul book live (branch separato, config canonica invariata).
@@ -0,0 +1,47 @@
# 2026-06-26 — Calendar-vol / term-structure DVOL: il DATO STORICO NON ESISTE (data-first gate)
**Goal utente:** "continua" — angolo scelto: scaricare e certificare la term-structure DVOL per
testare un calendar-vol (front IV vs back IV). **Esito: bloccato a monte dal gate "il dato prima
della strategia" (lezione v2.0.0).** Branch `research/gamma-scalp-options`.
Script: `scripts/research/probe_vol_termstructure.py` (scan) + `log_vol_termstructure.py` (forward).
## Scan di fattibilità (prima di costruire qualunque backtest)
Cosa l'API pubblica Deribit espone davvero per la vol per-scadenza:
| dato | disponibile? |
|---|---|
| Snapshot CORRENTE della term-structure ATM (mark_iv per scadenza) | **SÌ** (book_summary_by_currency, tokenless) |
| DVOL storico | solo **30g** fisso — nessun indice 7g/60g/90g pubblico |
| Trade-history IV per strumento | solo per strumenti **VIVI** (gli scaduti spariscono) |
| **Serie storica continua front-vs-back IV** | **NO** — il front-month rotola/espira → irricostruibile |
**Snapshot oggi (2026-06-26), per riferimento:**
- BTC spot $59.9k: 7g 45.6% · 30g 43.2% · 60g 42.7% · 90g 42.9% · 180g 44.5% — **contango** lieve.
- ETH spot $1.58k: 7g 57.6% · 30g 56.4% · 90g 57.5% · 180g 59.2% — contango (+1.6pp slope).
- (I valori sono interpolati a tenor fissi: il front a 1-giorno grezzo, 38%/54%, è rumore escluso.)
## Verdetto
**Un calendar-vol NON è backtestabile ora su dati certificati.** La storia per-scadenza non è
pubblica su Deribit (la nostra fonte di verità), e — coerentemente con la metodologia — **nessun
edge va creduto senza backtest OOS su dati certificati**. Costruire un calendar-vol su uno snapshot
singolo o su una storia ricostruita-a-pezzi sarebbe esattamente il tipo di scorciatoia che ha
prodotto la libreria fasulla v2.0.0. Quindi: **stop, niente backtest.**
## L'unica via legittima: costruire il dato in avanti
`scripts/research/log_vol_termstructure.py` — logger forward idempotente: ogni run prende lo snapshot
ATM, lo interpola ai tenor fissi {7,30,60,90,180}g e appende una riga/asset a
`data/raw/vol_term_<asset>.parquet`. Seminate le prime righe BTC/ETH oggi. Test offline
`tests/test_vol_termstructure.py` (interpolazione pura).
**Non auto-cablato in cron** (è ricerca forward, e non tocco il cron live senza ok). Per accumulare:
aggiungere una riga giornaliera al crontab. Dopo **~6-12 mesi** → certificare (monotonia, spike,
cross-venue) e SOLO ALLORA testare front-vs-back. Prima, nulla.
## Lezione
Il data-first gate ha funzionato: ha fermato un backtest su un dato che non esiste, invece di
produrre un numero non falsificabile. La term-structure è in **contango** oggi (regime calmo) —
osservazione da forward-monitor, non un edge. Nessun impatto sul book live (branch separato).
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# BOOK DERIBIT-ONLY (TP01+SKH01 nettati) — esecuzione LIVE a cadenza ORARIA. v2.0.0+.
# SKH01 decide su griglia 230m -> serve girare piu' spesso del daily; orario IDEMPOTENTE:
# riconcilia al target NETTO corrente (se non cambia nulla -> HOLD). Il feed 5m fresco per il
# segnale SKH e' preso IN MEMORIA dentro book_execute (livefeed.fresh_5m): NON tocca i dati
# certificati su disco. Esecuzione reale gated da config/live.json (execution_enabled) + --execute.
export PATH="/home/adriano/.local/bin:$PATH"
cd /opt/docker/PythagorasGoal || exit 1
mkdir -p logs
{
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_book ====="
uv run python scripts/live/book_execute.py --execute
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_book.log 2>&1
+3 -1
View File
@@ -10,7 +10,9 @@ mkdir -p logs
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni) uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01 uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy) uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json) # NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a
# cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py
# (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento.
# --- COMBO cross-venue (PAPER): refresh ETF IB (GTAA) + avanza paper TP01+GTAA --- # --- COMBO cross-venue (PAPER): refresh ETF IB (GTAA) + avanza paper TP01+GTAA ---
docker compose up -d ib-gateway >/dev/null 2>&1 # gateway IB paper (idempotente) docker compose up -d ib-gateway >/dev/null 2>&1 # gateway IB paper (idempotente)
for i in $(seq 1 25); do (echo > /dev/tcp/127.0.0.1/4002) >/dev/null 2>&1 && break; sleep 6; done for i in $(seq 1 25); do (echo > /dev/tcp/127.0.0.1/4002) >/dev/null 2>&1 && break; sleep 6; done
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# LOGGER FORWARD della vol term-structure Deribit — cadenza GIORNALIERA. v2.0.0+.
# Costruisce il dataset per un futuro calendar-vol (oggi NON backtestabile: storia per-scadenza non
# pubblica — vedi docs/diary/2026-06-26-vol-termstructure-feasibility.md). Append idempotente per
# giorno su data/raw/vol_term_<asset>.parquet. SOLO ricerca forward: NON tocca il book live ne' i
# dati certificati BTC/ETH; legge l'API pubblica Deribit (tokenless) e scrive un parquet dedicato.
export PATH="/home/adriano/.local/bin:$PATH"
cd /opt/docker/PythagorasGoal || exit 1
mkdir -p logs
{
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_vol_term ====="
uv run python scripts/research/log_vol_termstructure.py
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_vol_term.log 2>&1
+146
View File
@@ -0,0 +1,146 @@
"""BOOK DERIBIT-ONLY LIVE EXECUTE — TP01 + SKH01 NETTATI in software su un solo conto Deribit mainnet.
Porta il conto reale al target NETTO per asset (vedi src/live/book.py): per ogni asset combina la
frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25), e manda UN ordine con
segno (long/short/flip) per raggiungerlo. Poi assicura un disaster-SL on-book sulla posizione NETTA.
DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali):
1. config/live.json -> "execution_enabled": true (master switch, default false)
2. flag CLI --execute
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
data/live/book_executions.jsonl.
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.book import book_report
from src.live.execution import DeribitTrader
from src.live.notifier import notify
CONFIG = PROJECT_ROOT / "config" / "live.json"
LOG_DIR = PROJECT_ROOT / "data" / "live"
LOG = LOG_DIR / "book_executions.jsonl"
def load_config() -> dict:
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
cfg.setdefault("execution_enabled", False)
cfg.setdefault("max_notional_per_asset_usd", 300.0)
cfg.setdefault("min_order_usd", 5.0)
cfg.setdefault("disaster_sl_pct", 0.30)
return cfg
def log_event(rec: dict):
LOG_DIR.mkdir(parents=True, exist_ok=True)
with open(LOG, "a") as f:
f.write(json.dumps(rec) + "\n")
def _run():
cfg = load_config()
want_execute = "--execute" in sys.argv[1:]
enabled = bool(cfg["execution_enabled"])
do_execute = want_execute and enabled
min_order = float(cfg["min_order_usd"])
sl_pct = float(cfg["disaster_sl_pct"])
r = book_report(live_feed=True) # target NETTO + conto/posizioni reali (feed SKH fresco)
equity = r["equity"]
print("=" * 88)
print(" BOOK DERIBIT LIVE EXECUTE — TP01(0.75)+SKH01(0.25) NETTATI — Deribit mainnet (USDC linear)")
print("=" * 88)
mode = ("ESECUZIONE REALE" if do_execute else
("ARMATO ma manca --execute" if enabled else "DRY-RUN (execution_enabled=false)"))
print(f" modo : {mode}")
print(f" gate : execution_enabled={enabled} | --execute={want_execute}")
print(f" conto reale : ${r['real_equity']:,.2f}" if r["real_equity"] else f" conto: {r['eq_basis']}")
print(f" sizing base : ${equity:,.2f} | cap/asset ${r['cap_per_asset']:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
print(f" ultima barra : {r['last_data']}\n")
if not r["online"]:
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
if do_execute:
notify("⚠️ BOOK LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
return
trader = DeribitTrader() if do_execute else None
actions = []
for a in r["assets"]:
asset, inst = a["asset"], a["instrument"]
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
sk = a["skh_state"]
sk_txt = "flat" if sk == "flat" else f"{sk['dir']}@{sk.get('entry')}"
order = a["order"]
if order is None:
act = "HOLD (a target)"
elif order.get("is_close"):
act = f"CLOSE ${cur:,.0f}"
elif order.get("needs_flip"):
act = f"FLIP -> ${net:,.0f}"
else:
act = f"{order['side'].upper()} ${order['delta']:+,.0f}"
print(f" {asset:<3} TP {a['tp_frac']:+.3f} · SKH {a['skh_sign']:+d}({sk_txt}) -> net ${net:+,.0f} "
f"| pos ${cur:+,.0f} -> {act}")
if do_execute and order is not None:
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
newpos = trader.position_usd(inst)
for f in fills:
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos,
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"]))
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
fee=round(f.fee_usdc, 5), net=round(net, 0), pos_after=round(newpos, 0))
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
det if f.verified else {**det, "notes": f.notes})
print(f" reconcile: pos ${newpos:,.0f}")
if do_execute:
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
print(f" disaster-SL: {ds.get('state')}" + (f" @ ${ds['stop']:,.1f}" if ds.get("stop") else ""))
if ds.get("state") == "placed":
notify("🛡️ BOOK disaster-SL piazzato", {"asset": asset, "stop": round(ds.get("stop") or 0, 1),
"amount": round(ds.get("amount") or 0, 4)})
elif ds.get("state") == "place-failed":
notify("⚠️ BOOK disaster-SL FALLITO", {"asset": asset, "notes": ds.get("notes")})
actions.append(act)
print()
if not do_execute:
print(" => DRY-RUN: nessun ordine inviato." +
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
elif all(x.startswith("HOLD") for x in actions):
print(" => Nessuna azione: conto gia' al target netto del book.")
else:
print(" => Esecuzione completata (vedi data/live/book_executions.jsonl).")
def main():
try:
_run()
except Exception as e:
notify("🛑 BOOK LIVE — ERRORE", {"error": f"{type(e).__name__}: {e}"})
raise
if __name__ == "__main__":
main()
+106
View File
@@ -0,0 +1,106 @@
"""PROIEZIONE ACCUMULO del book DERIBIT-ONLY (TP01 + SKH01) — compounding puro (reinvesti le
vincite), allineamento MENSILE, NESSUN versamento esterno (non e' un PAC).
Base: rendimenti mensili del book Deribit-only (rebal mensile, netto costo turnover). Proietta in
avanti l'equity da un capitale iniziale:
- DETERMINISTICO @CAGR storico e @CAGR conservativo (= storico × cons_frac, default metà);
- MONTE CARLO block-bootstrap dei rendimenti mensili storici (mediana + banda p10/p90);
- €/giorno run-rate (cresce col capitale, perche' si rigiocano le vincite).
⚠️ ONESTA': lo storico e' un BULL crypto ~2019-26 -> il futuro sara' quasi certamente piu' magro.
Pianificare sulla colonna conservativa; il MC non contiene un vero bear pluriennale (anche il p10
e' ottimista). Nessuna leva. SKH01 e' research/forward-monitor (solo TP01 e' armato live). Non e'
una garanzia: e' una proiezione condizionata "se il futuro somigliasse al passato".
uv run python scripts/portfolio/forecast_deribit_book.py
uv run python scripts/portfolio/forecast_deribit_book.py --capital 5000 --years 1,3,5,10 --cons-frac 0.5
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.portfolio.portfolio import StrategyPortfolio, rebalance_sim
from src.portfolio.sleeves import deribit_book_sleeves
def book_monthly_returns(rebal_days: int, cost_rate: float) -> pd.Series:
"""Rendimenti MENSILI del book Deribit-only (rebal periodico, netto costo)."""
sl = deribit_book_sleeves()
w = StrategyPortfolio(sl).weights()
cols = {s.name: s.daily() for s in sl}
r = rebalance_sim(cols, w, period_days=rebal_days, cost_rate=cost_rate)["daily"]
return ((1.0 + r).resample("ME").prod() - 1.0).dropna()
def main():
ap = argparse.ArgumentParser(description="Proiezione accumulo book Deribit-only (compounding, allineamento mensile)")
ap.add_argument("--capital", type=float, default=5000.0)
ap.add_argument("--years", type=str, default="1,2,3,5,8,10")
ap.add_argument("--cons-frac", type=float, default=0.5, help="CAGR conservativo = storico × questo (default 0.5)")
ap.add_argument("--sims", type=int, default=4000)
ap.add_argument("--block-months", type=int, default=3, help="blocco del bootstrap (mesi)")
ap.add_argument("--rebal-days", type=int, default=30)
ap.add_argument("--cost-rate", type=float, default=0.0005, help="fee per-lato sul turnover (Deribit taker)")
ap.add_argument("--seed", type=int, default=7)
a = ap.parse_args()
cap = a.capital
HY = [int(x) for x in a.years.split(",") if x.strip()]
m = book_monthly_returns(a.rebal_days, a.cost_rate)
mv = m.values
nm = len(mv)
g_month = float(np.prod(1.0 + mv) ** (1.0 / nm) - 1.0)
cagr = (1.0 + g_month) ** 12 - 1.0
vol_ann = float(mv.std() * np.sqrt(12))
cons_cagr = cagr * a.cons_frac
print("=" * 90)
print(f" PROIEZIONE ACCUMULO — book Deribit-only (TP01+SKH01) | compounding, allineamento mensile, no versamenti")
print(f" storico: {nm} mesi · CAGR {cagr*100:.1f}% · vol annua {vol_ann*100:.0f}% (bull crypto, no leva) | capitale €{cap:,.0f}")
print("=" * 90)
# Monte Carlo: block-bootstrap dei rendimenti mensili
rng = np.random.default_rng(a.seed)
blk = a.block_months
maxM = max(HY) * 12
nb = maxM // blk + 1
starts = rng.integers(0, nm - blk, size=(a.sims, nb))
paths = np.concatenate([mv[starts[:, k][:, None] + np.arange(blk)[None, :]] for k in range(nb)], axis=1)[:, :maxM]
eqMC = cap * np.cumprod(1.0 + paths, axis=1)
cons_m = (1.0 + cons_cagr) ** (1.0 / 12) - 1.0
print(f"\n ACCUMULO (reinvesti le vincite):")
print(f" {'oriz.':>6} | {'det @storico':>13} | {'det @conserv.':>14} | {'MC mediana':>11} | {'MC p10':>9} | {'MC p90':>10}")
print(f" {'':>6} | {'('+format(cagr*100,'.0f')+'%)':>13} | {'('+format(cons_cagr*100,'.0f')+'%)':>14} |")
print(" " + "-" * 80)
for y in HY:
mo = y * 12
det = cap * (1.0 + g_month) ** mo
detc = cap * (1.0 + cons_m) ** mo
c = eqMC[:, mo - 1]
print(f" {y:>4}a | €{det:>11,.0f} | €{detc:>12,.0f} | €{np.median(c):>9,.0f} | €{np.percentile(c,10):>7,.0f} | €{np.percentile(c,90):>8,.0f}")
# €/giorno run-rate @conservativo (cresce col capitale)
rd = (1.0 + cons_cagr) ** (1.0 / 365.0) - 1.0
print(f"\n €/GIORNO run-rate @conservativo ({cons_cagr*100:.1f}% CAGR) — cresce col capitale (rigiochi le vincite):")
print(f" {'oriz.':>6} | {'equity':>9} | {'€/giorno':>10} | {'€/mese':>8}")
print(" " + "-" * 42)
for y in [0] + HY:
E = cap * (1.0 + cons_cagr) ** y
print(f" {('oggi' if y==0 else str(y)+'a'):>6} | €{E:>7,.0f} | €{E*rd:>7,.2f} | €{E*rd*30:>6,.0f}")
E_end = cap * (1.0 + cons_cagr) ** max(HY)
print(f" media €/giorno su {max(HY)} anni: €{(E_end-cap)/(max(HY)*365):.2f}/g (profitto €{E_end-cap:,.0f})")
need = 50 * 365 / cons_cagr if cons_cagr > 0 else float('inf')
print(f" capitale per ~€50/giorno @{cons_cagr*100:.1f}%: ≈ €{need:,.0f}")
print(f"\n ⚠️ Proiezione condizionata (storico = bull crypto); pianifica sul conservativo. Nessuna leva.")
print(f" SKH01 = research/forward-monitor; solo TP01 e' armato live. Non e' una garanzia.")
if __name__ == "__main__":
main()
+86
View File
@@ -0,0 +1,86 @@
"""REPORT del BOOK DERIBIT-ONLY realmente eseguibile = TP01 + SKH01 (75/25).
Le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), entrambe dal 2019. Esclude XS01
(Hyperliquid, stat-mode) e VRP01 (opzioni modellate). Mostra:
1. metriche oneste continuo (rebalance-continuo) vs RIBILANCIAMENTO PERIODICO realistico
(settimanale/mensile) con costo turnover Deribit-taker;
2. per-anno, accumulo da €2k (e nota €600 reale + min-order $5);
3. posizioni correnti per gamba.
uv run python scripts/portfolio/run_deribit_book.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
from src.portfolio.portfolio import StrategyPortfolio, metrics, yearly, rebalance_sim, HOLDOUT
CAP = 2000.0
REAL = 600.0 # capitale reale (vedi CLAUDE.md), min-order Deribit $5
COST_RATE = 0.0005 # Deribit taker per-lato (~0.10% RT sul turnover netto)
def line(tag, daily, extra=""):
m = metrics(daily); h = metrics(daily[daily.index >= HOLDOUT])
eqf = CAP * float(np.prod(1.0 + daily.values))
print(f" {tag:<26} FULL Sh {m['sharpe']:.2f} ret {m['ret']*100:+.0f}% DD {m['maxdd']*100:.1f}% "
f"| HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.1f}% | €2k→€{eqf:,.0f} {extra}")
return m, h
def main():
from src.portfolio.sleeves import deribit_book_sleeves
sleeves = deribit_book_sleeves()
pf = StrategyPortfolio(sleeves, capital=CAP)
w = pf.weights()
cols = {s.name: s.daily() for s in sleeves}
print("=" * 100)
print(f" BOOK DERIBIT-ONLY (eseguibile) — {' + '.join(f'{k} {v*100:.0f}%' for k, v in w.items())} "
f"| capitale €{CAP:,.0f} (reale ≈ ${REAL:,.0f}) | hold-out {HOLDOUT.date()}+")
print("=" * 100)
# standalone per-gamba
print("\n PER-GAMBA (standalone):")
for s in sleeves:
d = s.daily(); m = metrics(d); h = metrics(d[d.index >= HOLDOUT])
print(f" {s.name:<16} [{w[s.name]*100:>3.0f}%] FULL Sh {m['sharpe']:.2f} DD {m['maxdd']*100:.0f}% "
f"| HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.0f}%")
print("\n COMBINATO — rebalance-CONTINUO (idealizzato, no costi) vs PERIODICO (reale, costo turnover):")
cont = pf.combined_daily()
line("continuo (no costo)", cont)
sims = {}
for tag, period in (("settimanale (7g)", 7), ("bisettimanale (14g)", 14), ("mensile (30g)", 30)):
sim = rebalance_sim(cols, w, period_days=period, cost_rate=COST_RATE)
sims[tag] = sim
line(f"rebal {tag}", sim["daily"], extra=f"| turnover {sim['turnover_per_year']:.1f}×/anno, {sim['n_rebalances']} ribilanci")
# raccomandato = mensile
rec = sims["mensile (30g)"]["daily"]
print("\n PER ANNO (rebal mensile, netto costo):")
for y, d in yearly(rec).items():
print(f" {y}: ret {d['ret']*100:>+7.1f}% DD {d['dd']*100:>5.1f}%")
print("\n ACCUMULO (rebal mensile):")
for cap, lbl in ((CAP, "€2k nominale"), (REAL, "$600 reale")):
eq = cap * np.cumprod(1.0 + rec.values)
yrs = len(rec) / 365.25
print(f" {lbl:<14}: {cap:,.0f}{eq[-1]:,.0f} (×{eq[-1]/cap:.1f}, ~{(eq[-1]-cap)/(yrs*365.25):+,.2f}/g)")
print("\n POSIZIONI CORRENTI (ultima barra chiusa):")
for name, pos in pf.current_positions().items():
print(f" {name}: {pos if pos is not None else 'segnale dual-TF (no pos-fn) — vedi engine'}")
print("\n NOTE ONESTE:")
print(" · TP01 = unico armato live su Deribit (flat=risk-off). SKH01 = 2a gamba candidata (perp BTC/ETH).")
print(" · SKH01 equity daily-step (Sharpe lens). A $600 il min-order è $5: un ribilancio mensile")
print(" muove abbastanza nozionale da eseguirsi; il giornaliero NO (Δ sub-$5 = finzione) → usa ≥ settimanale.")
print(" · Prima del deploy 2a gamba: validare causalità sul CODICE D'ESECUZIONE reale e costi del book a 230m.")
if __name__ == "__main__":
main()
+173
View File
@@ -0,0 +1,173 @@
"""CC01 — CASH-AND-CARRY (basis trade) delta-neutral su Hyperliquid. Backtest onesto, STAT-MODE.
DIVERSO da FC01 (funding cross-sectional, gia' SCARTATO 2026-06-22). Qui NON si pickano
vincitori/perdenti cross-section: si HARVESTA il premio di funding AGGREGATO restando
delta-neutral sullo STESSO asset.
MECCANISMO. Long spot + short perp (stesso asset, stessa size):
long spot -> +price_ret
short perp -> -price_ret + funding (lo short INCASSA il funding quando f>0)
netto -> +funding (il prezzo si cancella -> zero esposizione direzionale)
Il ritorno della gamba delta-neutral = il funding realizzato, meno fee. Entrambe le gambe sono
lineari nel prezzo => restano matchate in nozionale senza ribilanciare (delta ~neutro da solo);
i costi reali sono entry/exit + spread, modellati come drag annuo fisso.
VARIANTI:
CC-static -> sempre long-spot/short-perp (basis trade classico): r = funding. Perde se f<0.
CC-gated -> harvest solo quando il funding trailing CAUSALE > 0 (esci dai regimi a funding
negativo invece di pagarli). r = funding se trail>0 else 0.
UNIVERSI: BTC/ETH (sottoinsieme rilevante per l'esecuzione) e i 19 major (basket pieno).
GIUDIZIO: standalone (Sharpe/DD/anni) + marginal_vs_tp01. CAVEAT ONESTI (pre-risultato):
- NON eseguibile a $600: serve spot+perp per gamba (BTC/ETH = 4 posizioni; 19 = 38). Su Deribit
lo storico funding e' bloccato e non operiamo spot HL -> STAT-MODE.
- Il modello "r=funding" IGNORA il rischio di base (perp != spot), la liquidazione dello short in
uno squeeze, e l'inversione brusca del funding. La vol modellata SOTTOSTIMA la coda.
- Lo storico funding parte 2023-05 -> NON contiene il deleveraging 2022 (il regime peggiore per il
carry). Edge potenzialmente sovrastimato.
uv run python scripts/research/cash_carry_hl.py
"""
from __future__ import annotations
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" / "alt"))
from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR
from altlib import marginal_vs_tp01 # type: ignore
SQ365 = np.sqrt(365.25)
ANNUAL_COST = 0.02 # drag annuo fisso: entry/exit + spread + borrow (generoso per un hold continuo)
def load_funding_panel(universe):
"""FUND, PREM [date x asset]: funding giornaliero (somma oraria) e premium/basis (ultimo del
giorno) per gli asset con dati. PREM serve a iniettare il mark-to-market della BASE (perp!=spot)."""
fund, prem = {}, {}
for sym in universe:
fp = _HL_DIR / f"hlfund_{sym.lower()}_1h.parquet"
if not fp.exists():
continue
df = pd.read_parquet(fp)
fund[sym] = df["funding"].resample("1D").sum()
prem[sym] = df["premium"].resample("1D").last()
FUND = pd.concat(fund, axis=1).sort_index()
PREM = pd.concat(prem, axis=1).sort_index().reindex(FUND.index)
if FUND.index.tz is None:
FUND.index = FUND.index.tz_localize("UTC"); PREM.index = PREM.index.tz_localize("UTC")
return FUND, PREM
def cc_returns(universe, mode="static", trail=14, cost=ANNUAL_COST, with_basis=False) -> pd.Series:
"""Ritorno giornaliero del basket cash-and-carry equal-weight, netto drag annuo.
with_basis=True inietta il mark-to-market reale della base: r = funding - Δpremium (lo short
perp marca contro l'allargamento del basis). E' il rischio che 'r=funding' nasconde."""
FUND, PREM = load_funding_panel(universe)
daily_cost = cost / 365.25
leg_raw = FUND - PREM.diff() if with_basis else FUND # per-asset daily mark
if mode == "gated":
sig = FUND.rolling(trail, min_periods=trail // 2).mean().shift(1) # funding trailing causale
active = (sig > 0).astype(float)
n_active = active.sum(axis=1).replace(0, np.nan)
gross = ((leg_raw * active).sum(axis=1) / n_active).fillna(0.0)
drag = (active.sum(axis=1) > 0).astype(float) * daily_cost
else:
gross = leg_raw.mean(axis=1)
drag = daily_cost
return (gross - drag).dropna()
def metrics(daily: pd.Series) -> dict:
r = daily.values
sh = float(np.mean(r) / np.std(r) * SQ365) if np.std(r) > 0 else 0.0
eq = np.cumprod(1.0 + np.clip(r, -0.99, None))
pk = np.maximum.accumulate(eq)
dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
yrs = (daily.index[-1] - daily.index[0]).days / 365.25 if len(daily) > 1 else 1.0
cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and len(eq) and eq[-1] > 0 else -1.0
s = pd.Series(eq, index=daily.index)
yearly = {}
for y, g in s.groupby(s.index.year):
if len(g) > 1:
v = g.values; p = np.maximum.accumulate(v)
yearly[int(y)] = (float(g.iloc[-1] / g.iloc[0] - 1), float(np.max((p - v) / p)))
return dict(sharpe=sh, dd=dd, cagr=cagr, yearly=yearly, gross_ann=float(np.mean(r) * 365.25))
def main():
print("=" * 92)
print(" CC01 — CASH-AND-CARRY (basis trade) delta-neutral su Hyperliquid")
print(" long spot + short perp -> r = funding (zero esposizione prezzo). Netto drag 2%/anno.")
print("=" * 92)
configs = [
("BTC/ETH CC-static", ["BTC", "ETH"], "static"),
("BTC/ETH CC-gated", ["BTC", "ETH"], "gated"),
("19-major CC-static", XS_UNIVERSE, "static"),
("19-major CC-gated", XS_UNIVERSE, "gated"),
]
print("\n [A] MODELLO INGENUO (r = funding) — IGNORA il rischio di base:")
series = {}
for label, uni, mode in configs:
r = cc_returns(uni, mode=mode)
series[label] = r
m = metrics(r)
ys = " ".join(f"{y}:{p*100:+.0f}%" for y, (p, d) in sorted(m['yearly'].items()))
print(f"\n --- {label} --- ({r.index[0].date()} -> {r.index[-1].date()}, {len(r)}g)")
print(f" Sharpe {m['sharpe']:+.2f} CAGR {m['cagr']*100:+.1f}% maxDD {m['dd']*100:.1f}% "
f"carry lordo {m['gross_ann']*100:+.1f}%/anno | per-anno: {ys}")
print("\n [B] CON MARK-TO-MARKET DELLA BASE (r = funding - Δpremium) — il rischio nascosto:")
series_b = {}
for label, uni, mode in configs:
r = cc_returns(uni, mode=mode, with_basis=True)
series_b[label] = r
m = metrics(r)
ys = " ".join(f"{y}:{p*100:+.0f}%" for y, (p, d) in sorted(m['yearly'].items()))
print(f"\n --- {label} (basis) ---")
print(f" Sharpe {m['sharpe']:+.2f} CAGR {m['cagr']*100:+.1f}% maxDD {m['dd']*100:.1f}% "
f"| per-anno: {ys}")
print("\n" + "=" * 92)
print(" REALITY CHECK — perche' uno Sharpe 11-13 e' un ARTEFATTO, non un edge")
print("=" * 92)
FUND, _ = load_funding_panel(["BTC", "ETH"])
agg = FUND.mean(axis=1)
neg = float((agg < 0).mean())
by_year = agg.groupby(agg.index.year).apply(lambda x: float(x.sum()))
print(f" funding aggregato BTC/ETH: giorni a funding NEGATIVO {neg*100:.0f}% | "
f"per-anno (somma): " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in by_year.items()))
print(" -> il carry e' PROCYCLICO: +23% nel toro 2024, ~+1% nel 2026 (si comprime nel bear).")
print(" -> lo storico funding parte 2023-05: ASSENTE il deleveraging 2022 (funding -, basis blow-out),")
print(" il regime che farebbe il vero drawdown. + assenti: liquidazione short in squeeze, slippage")
print(" su spot+perp a $600. Sharpe reale di un basis-trade ~1-3 con code brusche, NON 13.")
print("\n" + "=" * 92)
print(" SCORING MARGINALE vs TP01 (sul modello ONESTO con rischio di base [B])")
print(" NB: ADDS/robust_oos qui ESPONGONO un punto cieco dello scorer — si fida della vol")
print(" riportata e non ha un gate 'Sharpe implausibile -> rischio nascosto'.")
print("=" * 92)
for label, r in series_b.items():
if r.std() == 0:
print(f"\n[{label}] flat — skip"); continue
m = marginal_vs_tp01(r)
b = m.get("blends", {}).get("w25", {})
print(f"\n[{label}]")
print(f" verdict={m.get('marginal_verdict')} corr->TP01 full={m.get('corr_full')} "
f"hold={m.get('corr_hold')} is_hedge={m.get('is_hedge')} "
f"has_insample_edge={m.get('has_insample_edge')} (cand IS Sharpe {m.get('cand_insample_sharpe')})")
print(f" cand Sharpe full={m.get('cand_full_sharpe')} hold={m.get('cand_hold_sharpe')} | "
f"blend25 full {b.get('full')} (upl {b.get('uplift_full')}) "
f"hold {b.get('hold')} (upl {b.get('uplift_hold')}) DD {b.get('dd')}")
print(f" multicut persistente={m.get('multicut_persistent')} robust_oos={m.get('robust_oos')} "
f"hedge-check up/down {m.get('uplift_tp01_up')}/{m.get('uplift_tp01_down')}")
if __name__ == "__main__":
main()
+103
View File
@@ -0,0 +1,103 @@
"""LOGGER FORWARD della vol term-structure Deribit (l'UNICA via legittima per avere il dato).
Lo scan (probe_vol_termstructure.py) ha stabilito: la storia per-scadenza NON e' pubblica su Deribit
(DVOL solo 30g; trade-history IV solo per strumenti vivi; il front rotola). Quindi un calendar-vol
NON e' backtestabile oggi. Questo logger COSTRUISCE il dataset in avanti: ogni run prende lo snapshot
ATM mark_iv per scadenza, lo interpola a TENOR FISSI (7/30/60/90/180g) e appende una riga per asset a
data/raw/vol_term_<asset>.parquet. Idempotente per giorno (riscrive la riga del giorno).
Da cron giornaliero (NON auto-cablato: e' ricerca forward, va aggiunto a mano se si vuole):
uv run python scripts/research/log_vol_termstructure.py
Dopo ~6-12 mesi di accumulo -> certificare (cross-venue, monotonia, spike) e SOLO ALLORA testare un
calendar-vol (front vs back) su dati certificati. Nessun edge creduto prima.
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import datetime as _dt
import numpy as np
import pandas as pd
import requests
BASE = "https://www.deribit.com/api/v2/public/"
RAW = PROJECT_ROOT / "data" / "raw"
TENORS = [7, 30, 60, 90, 180] # giorni-a-scadenza fissi su cui interpolare l'ATM IV
def _get(method, **params):
return requests.get(BASE + method, params=params, timeout=40).json().get("result", None)
def _atm_curve(cur, now_ms):
"""[(dte_giorni, atm_iv%)] ordinato per scadenza, dallo strike piu' vicino allo spot."""
summ = _get("get_book_summary_by_currency", currency=cur, kind="option")
idx = _get("get_index_price", index_name=f"{cur.lower()}_usd")
if not summ or not idx:
return None, None
spot = float(idx["index_price"])
best = {} # exp_ms -> (dist_strike, iv)
for o in summ:
p = o["instrument_name"].split("-")
if len(p) != 4 or o.get("mark_iv") is None:
continue
try:
exp_ms = int(_dt.datetime.strptime(p[1], "%d%b%y").replace(
tzinfo=_dt.timezone.utc).timestamp() * 1000)
except ValueError:
continue
d = abs(float(p[2]) - spot)
if exp_ms not in best or d < best[exp_ms][0]:
best[exp_ms] = (d, float(o["mark_iv"]))
curve = sorted(((e - now_ms) / 86400_000.0, iv) for e, (_, iv) in best.items() if e > now_ms)
return spot, curve
def build_row(spot, curve, now_ms):
"""Pura: da (spot, curve=[(dte,iv)], now_ms) -> riga con ATM IV interpolata ai TENOR fissi."""
if not curve or len(curve) < 2:
return None
dtes = np.array([c[0] for c in curve]); ivs = np.array([c[1] for c in curve])
row = {"date": pd.Timestamp(now_ms, unit="ms", tz="UTC").normalize(), "spot": spot}
for t in TENORS:
row[f"iv_{t}d"] = float(np.interp(t, dtes, ivs)) # interp lineare sui DTE disponibili
row["slope_7_180"] = row["iv_180d"] - row["iv_7d"]
return row
def snapshot(cur, now_ms):
spot, curve = _atm_curve(cur, now_ms)
return build_row(spot, curve, now_ms) if curve else None
def append_row(cur, row):
fp = RAW / f"vol_term_{cur.lower()}.parquet"
df = pd.read_parquet(fp) if fp.exists() else pd.DataFrame()
df = df[df["date"] != row["date"]] if len(df) else df # idempotente per giorno
df = pd.concat([df, pd.DataFrame([row])], ignore_index=True).sort_values("date")
df.to_parquet(fp, index=False)
return fp, len(df)
def main():
now_ms = int(pd.Timestamp.now("UTC").timestamp() * 1000)
print("=" * 78)
print(" LOG vol term-structure (forward dataset builder)")
print("=" * 78)
for cur in ("BTC", "ETH"):
row = snapshot(cur, now_ms)
if row is None:
print(f" {cur}: snapshot fallito (chain vuota?)"); continue
fp, n = append_row(cur, row)
ivs = " ".join(f"{t}g {row[f'iv_{t}d']:.1f}%" for t in TENORS)
print(f" {cur} {row['date'].date()} spot ${row['spot']:,.0f} | {ivs} | "
f"slope7-180 {row['slope_7_180']:+.1f}pp -> {n} righe in {fp.name}")
print("\n (forward-only: serve accumulo di mesi prima di poter certificare e testare un calendar-vol)")
if __name__ == "__main__":
main()
+296
View File
@@ -0,0 +1,296 @@
"""GAMMA SCALPING (long-vol) — "scalping BTC/ETH con copertura in opzioni", valutato onestamente.
Interpretazione rigorosa di "fare scalping con copertura in opzioni" = GAMMA SCALPING:
compri un'opzione (la COPERTURA = long gamma), delta-hedgi il sottostante a cadenza fissa
(lo SCALP), e il P&L netto e' ~ dollar-gamma * (vol realizzata^2 - vol implicita^2). E' lo
SPECCHIO ESATTO del VRP01 (short-vol): VRP01 incassa IV-RV (positivo in media), il gamma
scalping incassa RV-IV (negativo in media, ma CONVESSO -> paga nei crash).
Modello (mirror della struttura VRP per comparabilita'):
- long STRADDLE ATM, tenor settimanale (7g), strike = spot all'ingresso.
- IV = DVOL Deribit (data/raw/dvol_*.parquet) — la stessa fonte IV del VRP.
- delta-hedge GIORNALIERO sui prezzi certificati 1d (BTC/ETH).
- P&L delta-hedged classico per step: DG_t * [(dS/S)^2 - sigma^2*dt],
DG_t = dollar-gamma dello straddle = phi(d1)*S/(sigma*sqrt(tau)).
- fee opzioni Deribit cap 12.5% del premio (come VRP) + fee perp sull'hedge turnover.
- return-on-notional (pnl/S0), poi VOL-TARGET 20% annuo per apples-to-apples con gli altri sleeve.
Varianti testate:
NAKED -> sempre long gamma (baseline: deve perdere il premio in media).
CHEAP-GATED -> entra solo quando IV-rank < gate (compri vol a sconto = specchio del gate VRP).
RICH-SKIP -> entra sempre tranne quando IV-rank > skip (evita di pagare vol carissima).
Output: metriche standalone + per-anno + SCORING MARGINALE vs TP01 (corr, blend uplift, is_hedge,
has_insample_edge) + il muro di ESEGUIBILITA' a $600 (min size opzioni Deribit).
uv run python scripts/research/options_gamma_scalp.py
ONESTA': premio MODELLATO su DVOL ATM (skew non esplicito) — stesso caveat del VRP. Long-vol da
modello: meno pericoloso dello short-vol (loss capped al premio), ma resta un LEAD da modello.
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
import numpy as np
import pandas as pd
from scipy.stats import norm
from src.data.downloader import load_data
from src.strategies.trend_portfolio import resample_1d
from altlib import marginal_vs_tp01 # type: ignore
RAW = PROJECT_ROOT / "data" / "raw"
SQ365 = np.sqrt(365.25)
DT = 1.0 / 365.25
# fee model (mirror VRP): opzioni Deribit cap 12.5% del premio; perp taker 0.05%/lato sull'hedge.
OPT_FEE_FRAC = 0.125
HEDGE_FEE_SIDE = 0.0005
TENOR_D = 7
def _bs_straddle(S, K, T, sig):
"""Premio BS di uno straddle ATM (call+put), r=0."""
if T <= 0 or sig <= 0:
return abs(S - K)
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
d2 = d1 - sig * np.sqrt(T)
call = S * norm.cdf(d1) - K * norm.cdf(d2)
put = K * norm.cdf(-d2) - S * norm.cdf(-d1)
return call + put
def _dvol(asset: str) -> pd.Series:
dv = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
return pd.Series(dv["close"].values.astype(float),
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)) / 100.0
def _load(asset: str):
df = resample_1d(load_data(asset, "1h"))
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
J = pd.concat({"px": s, "dvol": _dvol(asset)}, axis=1, join="inner").sort_index().dropna()
return J["px"].values, J["dvol"].values / 1.0, J.index
def _load_hourly(asset: str):
df = load_data(asset, "1h")
s = pd.Series(df["close"].values.astype(float),
index=pd.to_datetime(df["timestamp"], unit="ms", utc=True)).sort_index()
return s
def rv_iv_diagnostic(asset: str):
"""Il CUORE strutturale: vol realizzata (a vari campionamenti) vs vol implicita (DVOL).
Se RV < IV in media -> il long gamma PERDE gross, prima di ogni fee. E' il VRP, di segno opposto."""
px, dvf, idx = _load(asset)
iv = float(np.mean(dvf))
rdaily = np.diff(np.log(px))
rv_daily = float(np.std(rdaily) * SQ365)
h = _load_hourly(asset)
rhour = np.diff(np.log(h.values))
rv_hour = float(np.std(rhour) * np.sqrt(365.25 * 24))
return dict(asset=asset, iv=iv, rv_daily=rv_daily, rv_hour=rv_hour,
spread_daily=iv - rv_daily, spread_hour=iv - rv_hour)
def gamma_scalp_hourly(asset: str, mode: str = "naked",
gate_cheap: float = 0.30) -> pd.Series:
"""Gamma scalp a rehedge ORARIO (la 'vera' frequenza di scalping): cattura la RV intraday,
ma paga 24x la fee di hedge. Tenor 7g = 168 step orari. IV costante nel giorno (DVOL)."""
h = _load_hourly(asset)
dv = _dvol(asset).reindex(h.index.normalize(), method="ffill")
dv.index = h.index
J = pd.concat({"px": h, "dvol": dv}, axis=1).dropna()
px = J["px"].values; dvf = J["dvol"].values; idx = J.index
n = len(px); STEPS = TENOR_D * 24; dt = 1.0 / (365.25 * 24)
# IV-rank causale su DVOL giornaliero per il gate
daily_iv = _dvol(asset)
rets = {}
i = 24 * 60
while i + STEPS < n:
S0 = px[i]; sig = dvf[i]; K = S0
if mode == "cheap":
day = idx[i].normalize()
hist = daily_iv[daily_iv.index < day]
if len(hist) >= 60 and float((hist < sig).mean()) > gate_cheap:
rets[idx[i + STEPS]] = 0.0; i += STEPS; continue
T = TENOR_D / 365.25
premium = _bs_straddle(S0, K, T, sig)
gamma_pnl = 0.0; hedge_fee = 0.0; prev_delta = None
for t in range(1, STEPS + 1):
tau = max((STEPS - (t - 1)) * dt, dt)
Sp = px[i + t - 1]; Sn = px[i + t]
r = Sn / Sp - 1.0
d1 = (np.log(Sp / K) + 0.5 * sig ** 2 * tau) / (sig * np.sqrt(tau))
dollar_gamma = norm.pdf(d1) * Sp / (sig * np.sqrt(tau))
gamma_pnl += dollar_gamma * (r * r - sig * sig * dt)
delta = 2.0 * norm.cdf(d1) - 1.0
if prev_delta is not None:
hedge_fee += HEDGE_FEE_SIDE * abs(delta - prev_delta) * Sp
prev_delta = delta
pnl = gamma_pnl - OPT_FEE_FRAC * premium - hedge_fee
rets[idx[i + STEPS]] = pnl / S0
i += STEPS
out = pd.Series(rets)
out.index = out.index.normalize()
return out
def gamma_scalp_asset(asset: str, mode: str = "naked",
gate_cheap: float = 0.30, skip_rich: float = 0.90) -> pd.Series:
"""Rendimenti settimanali (return-on-notional) di una catena di long-straddle delta-hedged,
lumpati sul giorno di scadenza. Causale: IV/strike/gate da dati <= entry; payoff sul path."""
px, dvf, idx = _load(asset)
n = len(px)
rets = {}
i = 60
while i + TENOR_D < n:
S0 = px[i]; sig = dvf[i]; K = S0
skip = False
if i >= 60 and mode in ("cheap", "rich"):
ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente causale
if mode == "cheap" and ivr > gate_cheap: # compra gamma solo se vol a SCONTO
skip = True
if mode == "rich" and ivr > skip_rich: # evita di pagare vol carissima
skip = True
if skip:
rets[idx[i + TENOR_D]] = 0.0; i += TENOR_D; continue
T = TENOR_D / 365.25
premium = _bs_straddle(S0, K, T, sig)
gamma_pnl = 0.0; hedge_fee = 0.0; prev_delta = None
for t in range(1, TENOR_D + 1):
tau = (TENOR_D - (t - 1)) / 365.25 # tempo residuo a inizio step
Sp = px[i + t - 1]; Sn = px[i + t]
r = Sn / Sp - 1.0
d1 = (np.log(Sp / K) + 0.5 * sig ** 2 * tau) / (sig * np.sqrt(tau))
dollar_gamma = norm.pdf(d1) * Sp / (sig * np.sqrt(tau)) # DG straddle = phi(d1)*S/(sig*sqrt(tau))
gamma_pnl += dollar_gamma * (r * r - sig * sig * DT)
delta = 2.0 * norm.cdf(d1) - 1.0 # delta straddle ATM
if prev_delta is not None:
hedge_fee += HEDGE_FEE_SIDE * abs(delta - prev_delta) * Sp
prev_delta = delta
opt_fee = OPT_FEE_FRAC * premium
pnl = gamma_pnl - opt_fee - hedge_fee
rets[idx[i + TENOR_D]] = pnl / S0 # return-on-notional
i += TENOR_D
return pd.Series(rets)
def to_daily_voltgt(weekly_btc: pd.Series, weekly_eth: pd.Series, tgt_vol: float = 0.20) -> pd.Series:
"""Book 50/50 BTC+ETH su griglia giornaliera, poi scalato a tgt_vol annuo (apples-to-apples
con gli altri sleeve, tutti vol-targeted ~20%)."""
wk = pd.concat({"B": weekly_btc, "E": weekly_eth}, axis=1, join="inner").mean(axis=1).sort_index()
if wk.empty:
return wk
days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(), freq="1D", tz="UTC")
daily = pd.Series(0.0, index=days)
daily.loc[wk.index.normalize()] = wk.values
sd = daily.std()
if sd > 0:
daily = daily * (tgt_vol / (sd * SQ365))
return daily
def _metrics(daily: pd.Series) -> dict:
r = daily.values
sh = float(np.mean(r) / np.std(r) * SQ365) if np.std(r) > 0 else 0.0
eq = np.cumprod(1.0 + np.clip(r, -0.99, None))
pk = np.maximum.accumulate(eq)
dd = float(np.max((pk - eq) / pk))
yrs = (daily.index[-1] - daily.index[0]).days / 365.25
cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and eq[-1] > 0 else -1.0
s = pd.Series(eq, index=daily.index)
yearly = {}
for y, g in s.groupby(s.index.year):
if len(g) > 1:
v = g.values; p = np.maximum.accumulate(v)
yearly[int(y)] = (float(g.iloc[-1] / g.iloc[0] - 1), float(np.max((p - v) / p)))
return dict(sharpe=sh, dd=dd, cagr=cagr, yearly=yearly)
def main():
print("=" * 92)
print(" GAMMA SCALPING (long-vol) — scalping BTC/ETH con copertura in opzioni")
print(" Modello: long straddle ATM 7g, delta-hedge 1d, P&L = DG*(RV^2 - IV^2). Mirror del VRP01.")
print("=" * 92)
print("\n DIAGNOSTICA STRUTTURALE — vol implicita (DVOL) vs realizzata (il segno dell'edge):")
for a in ("BTC", "ETH"):
d = rv_iv_diagnostic(a)
print(f" {a}: IV {d['iv']*100:5.1f}% | RV_1d {d['rv_daily']*100:5.1f}% "
f"(IV-RV {d['spread_daily']*100:+.1f}pp) RV_1h {d['rv_hour']*100:5.1f}% "
f"(IV-RV {d['spread_hour']*100:+.1f}pp)")
print(" -> IV-RV > 0 = il mercato PAGA per essere short-vol (VRP). Il long gamma e' su questo lato"
", al ROVESCIO: paga il premio. RV_1h>RV_1d -> rehedge orario cattura piu' RV (test sotto).")
variants = {
"NAKED (sempre long gamma)": dict(mode="naked"),
"CHEAP-GATED (IV-rank<0.30 = vol scont)": dict(mode="cheap", gate_cheap=0.30),
"CHEAP-GATED (IV-rank<0.50)": dict(mode="cheap", gate_cheap=0.50),
"RICH-SKIP (no entry se IV-rank>0.90)": dict(mode="rich", skip_rich=0.90),
}
series = {}
for label, kw in variants.items():
wB = gamma_scalp_asset("BTC", **kw)
wE = gamma_scalp_asset("ETH", **kw)
daily = to_daily_voltgt(wB, wE)
series[label] = daily
m = _metrics(daily)
ntr = int((wB != 0).sum() + (wE != 0).sum())
print(f"\n--- {label} ---")
print(f" standalone (vol-tgt 20%): Sharpe {m['sharpe']:+.2f} CAGR {m['cagr']*100:+.1f}% "
f"maxDD {m['dd']*100:.1f}% trade {ntr}")
ys = " ".join(f"{y}:{p*100:+.0f}%" for y, (p, d) in sorted(m['yearly'].items()))
print(f" per-anno PnL: {ys}")
print("\n" + "=" * 92)
print(" REHEDGE ORARIO (la 'vera' frequenza di scalping: cattura RV intraday, paga 24x hedge fee)")
print("=" * 92)
for label, kw in (("NAKED orario", dict(mode="naked")),
("CHEAP-GATED orario (IV-rank<0.30)", dict(mode="cheap", gate_cheap=0.30))):
wB = gamma_scalp_hourly("BTC", **kw); wE = gamma_scalp_hourly("ETH", **kw)
daily = to_daily_voltgt(wB, wE)
m = _metrics(daily)
ys = " ".join(f"{y}:{p*100:+.0f}%" for y, (p, d) in sorted(m['yearly'].items()))
print(f"\n--- {label} ---")
print(f" Sharpe {m['sharpe']:+.2f} CAGR {m['cagr']*100:+.1f}% maxDD {m['dd']*100:.1f}%")
print(f" per-anno PnL: {ys}")
print("\n" + "=" * 92)
print(" SCORING MARGINALE vs TP01 (lo standard del progetto: un nuovo sleeve si giudica QUI)")
print("=" * 92)
for label, daily in series.items():
if daily.std() == 0:
print(f"\n[{label}] flat — skip"); continue
m = marginal_vs_tp01(daily)
b25 = m.get("blends", {}).get("w25", {})
print(f"\n[{label}]")
print(f" verdict={m.get('marginal_verdict')} corr->TP01 full={m.get('corr_full')} "
f"hold={m.get('corr_hold')} is_hedge={m.get('is_hedge')} "
f"has_insample_edge={m.get('has_insample_edge')} (cand IS Sharpe {m.get('cand_insample_sharpe')})")
print(f" cand Sharpe full={m.get('cand_full_sharpe')} hold={m.get('cand_hold_sharpe')} | "
f"blend25 full {b25.get('full')} (upl {b25.get('uplift_full')}) "
f"hold {b25.get('hold')} (upl {b25.get('uplift_hold')}) DD {b25.get('dd')}")
print(f" hedge-check: uplift TP01-up {m.get('uplift_tp01_up')} / TP01-down {m.get('uplift_tp01_down')} "
f"yearly-corr {m.get('hedge_yearly_corr')}")
print("\n" + "=" * 92)
print(" ESEGUIBILITA' a ~$600 (Deribit options min size)")
print("=" * 92)
pxB = _load("BTC")[0][-1]; pxE = _load("ETH")[0][-1]
for a, p, csz, minc in (("BTC", pxB, 1.0, 0.1), ("ETH", pxE, 1.0, 0.1)):
notion = p * csz * minc
print(f" {a}: spot ${p:,.0f} | contratto {csz} {a}, min {minc} {a} -> notional minimo "
f"${notion:,.0f} ({'OK' if notion < 600 else 'OLTRE i $600 -> NON eseguibile'})")
if __name__ == "__main__":
main()
+100
View File
@@ -0,0 +1,100 @@
"""SCAN DI FATTIBILITA' — la vol term-structure e' scaricabile/certificabile da Deribit pubblico?
Un calendar-vol (front vs back IV) richiede ATM IV per SCADENZA, STORICA. Il DVOL pubblico e' solo
30g. Questo script PROVA cosa l'API pubblica Deribit espone davvero, PRIMA di costruire backtest:
1. snapshot CORRENTE della term-structure (mark_iv ATM per scadenza) — book_summary_by_currency.
2. esiste STORIA per-scadenza? (DVOL e' 30g fisso; provo trade history con IV per instrument).
Verdetto: se la storia per-scadenza NON e' pubblica -> calendar-vol NON backtestabile su dati certi.
uv run python scripts/research/probe_vol_termstructure.py
"""
from __future__ import annotations
import sys, time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import requests
import numpy as np
BASE = "https://www.deribit.com/api/v2/public/"
def _get(method, **params):
r = requests.get(BASE + method, params=params, timeout=40)
return r.json().get("result", None)
def current_term_structure(cur="BTC"):
"""ATM mark_iv per scadenza ORA (snapshot pubblico, tokenless)."""
summ = _get("get_book_summary_by_currency", currency=cur, kind="option")
if not summ:
return None
idx = _get("get_index_price", index_name=f"{cur.lower()}_usd")
spot = float(idx["index_price"]) if idx else None
# raggruppa per scadenza, prendi lo strike piu' vicino allo spot (ATM)
by_exp = {}
for o in summ:
name = o["instrument_name"] # es. BTC-27JUN26-60000-C
parts = name.split("-")
if len(parts) != 4:
continue
exp, strike = parts[1], float(parts[2])
miv = o.get("mark_iv")
if miv is None or spot is None:
continue
d = abs(strike - spot)
if exp not in by_exp or d < by_exp[exp][0]:
by_exp[exp] = (d, float(miv), strike)
return spot, by_exp
def probe_history(cur="BTC"):
"""C'e' STORIA per-scadenza? Provo: (a) DVOL (sappiamo 30g), (b) trade history con IV su uno
strumento vivo, (c) se esistono indici vol a tenor diversi."""
findings = []
# (b) trade history con IV — solo strumenti NON scaduti
instr = _get("get_instruments", currency=cur, kind="option", expired="false")
live = instr[0]["instrument_name"] if instr else None
if live:
tr = _get("get_last_trades_by_instrument", instrument_name=live, count=5)
has_iv = bool(tr and tr.get("trades") and "iv" in tr["trades"][0])
findings.append(f"trade-history IV su {live}: {'SI (ma solo instrument VIVO, no scaduti)' if has_iv else 'NO'}")
# (c) altri indici vol a tenor != 30g?
findings.append("get_volatility_index_data: solo DVOL 30g (nessun indice 7g/60g/90g pubblico)")
return findings
def main():
print("=" * 88)
print(" SCAN FATTIBILITA' — vol term-structure Deribit pubblico")
print("=" * 88)
for cur in ("BTC", "ETH"):
ts = current_term_structure(cur)
if not ts:
print(f"\n{cur}: nessun dato chain"); continue
spot, by_exp = ts
print(f"\n{cur} spot ${spot:,.0f} — term-structure ATM mark_iv ORA ({len(by_exp)} scadenze):")
# ordina per scadenza (data nel nome)
def _key(e):
try:
return time.mktime(time.strptime(e, "%d%b%y"))
except Exception:
return 0
ivs = []
for exp in sorted(by_exp, key=_key):
d, miv, strike = by_exp[exp]
ivs.append(miv)
print(f" {exp:>9} ATM~{strike:>9,.0f} IV {miv:5.1f}%")
if len(ivs) >= 2:
print(f" -> front {ivs[0]:.1f}% back {ivs[-1]:.1f}% slope {ivs[-1]-ivs[0]:+.1f}pp "
f"({'contango' if ivs[-1] > ivs[0] else 'backwardation'})")
print(f" STORIA per-scadenza:")
for f in probe_history(cur):
print(f" - {f}")
print("\n" + "=" * 88)
print(" VERDETTO sulla backtestabilita' sotto.")
print("=" * 88)
if __name__ == "__main__":
main()
+159
View File
@@ -0,0 +1,159 @@
"""TP01 × DVOL — la vol IMPLICITA (forward-looking) migliora il risk-sizing di TP01? (ESEGUIBILE)
A differenza degli sleeve diversificatori (XS01/VRP01/carry = STAT-MODE, non eseguibili a $600),
questo TOCCA il book live: TP01 è BTC/ETH perp su Deribit, già armato. Oggi vol-targeta sulla vol
REALIZZATA 30g (backward-looking). Ipotesi: il DVOL (vol implicita 30g Deribit, forward-looking,
che spesso ANTICIPA i salti di vol realizzata) come denominatore del vol-target → de-risking più
tempestivo prima dei crash → hold-out migliore e/o DD più basso, SENZA peggiorare il FULL.
Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla FINESTRA COMUNE 2021-2026 (perdo
2018-2021, incluso il toro 2021 pre-DVOL). Baseline ricalcolato sulla stessa finestra. Hold-out 2025+.
Tutto causale (vol/segnale ≤ close[i]), fee 0.10% RT, long-flat, leva cap 2x — config CANONICA TP01.
VARIANTI (denominatore del vol-target):
REALIZED -> 30g realizzata (baseline canonica)
DVOL -> DVOL/100 (implicita)
BLEND -> 0.5·realizzata + 0.5·DVOL
MAX -> max(realizzata, DVOL) (sizing più difensivo: la più alta delle due)
DERISK -> realizzata, ma posizione ×0.5 quando DVOL > pctl espandente causale (gate crash)
uv run python scripts/research/tp01_dvol_overlay.py
"""
from __future__ import annotations
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))
from src.data.downloader import load_data
from src.strategies.trend_portfolio import (
resample_1d, simple_returns, realized_vol, tsmom_blend, CANONICAL,
)
RAW = ROOT / "data" / "raw"
SQ = np.sqrt(365.25)
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
TGT_VOL = CANONICAL["target_vol"]; LEV = CANONICAL["leverage"]; FEE = CANONICAL["fee_side"]
HZ = CANONICAL["horizons_days"]; VW = CANONICAL["vol_win_days"]
def _components(asset: str):
df = resample_1d(load_data(asset, "1h"))
c = df["close"].values.astype(float)
idx = pd.to_datetime(df["datetime"])
if idx.dt.tz is None:
idx = idx.dt.tz_localize("UTC")
idx = pd.DatetimeIndex(idx) # tz-aware (UTC)
r = simple_returns(c)
rv = realized_vol(r, VW, 365.25) # 30g realizzata annualizzata (bpd=1)
direction = np.clip(tsmom_blend(c, HZ), 0, None) # long-flat
dv = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
dser = pd.Series(dv["close"].values.astype(float) / 100.0,
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).sort_index()
dvol = dser.reindex(idx, method="ffill").values
return c, r, idx, rv, direction, dvol
def _net_returns(asset: str, mode: str, tvol: float = TGT_VOL) -> pd.Series:
c, r, idx, rv, direction, dvol = _components(asset)
derisk = np.ones(len(c))
if mode == "realized":
vol = rv
elif mode == "dvol":
vol = dvol
elif mode == "blend":
vol = 0.5 * rv + 0.5 * dvol
elif mode == "max":
vol = np.fmax(rv, dvol)
elif mode == "derisk":
vol = rv
# gate crash causale: DVOL sopra il suo percentile espandente (90%) -> dimezza l'esposizione
dd = pd.Series(dvol, index=idx)
rank = dd.expanding(min_periods=60).apply(lambda x: (x[:-1] < x[-1]).mean() if len(x) > 1 else 0.5, raw=True)
derisk = np.where(rank.values > 0.90, 0.5, 1.0)
else:
raise ValueError(mode)
with np.errstate(divide="ignore", invalid="ignore"):
scal = np.where((vol > 0) & np.isfinite(vol), tvol / vol, 0.0)
tgt = np.clip(direction * scal * derisk, -LEV, LEV)
tgt[~np.isfinite(tgt)] = 0.0
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] # decisa a close[t-1], tenuta in t
gross = pos * r
turn = np.abs(np.diff(pos, prepend=0.0))
net = np.clip(gross - FEE * turn, -0.99, None); net[0] = 0.0
return pd.Series(net, index=idx)
def portfolio(mode: str, tvol: float = TGT_VOL) -> pd.Series:
b = _net_returns("BTC", mode, tvol); e = _net_returns("ETH", mode, tvol)
J = pd.concat({"B": b, "E": e}, axis=1, join="inner").fillna(0.0)
return 0.5 * J["B"] + 0.5 * J["E"]
def metrics(daily: pd.Series, lo=None) -> dict:
if lo is not None:
daily = daily[daily.index >= lo]
r = daily.values
sh = float(np.mean(r) / np.std(r) * SQ) if np.std(r) > 0 else 0.0
eq = np.cumprod(1.0 + r); pk = np.maximum.accumulate(eq)
dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
yrs = (daily.index[-1] - daily.index[0]).days / 365.25 if len(daily) > 1 else 1.0
cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and len(eq) and eq[-1] > 0 else -1.0
s = pd.Series(eq, index=daily.index); yearly = {}
for y, g in s.groupby(s.index.year):
if len(g) > 1:
yearly[int(y)] = float(g.iloc[-1] / g.iloc[0] - 1)
return dict(sharpe=sh, dd=dd, cagr=cagr, tot=float(eq[-1] - 1) if len(eq) else 0.0, yearly=yearly)
def main():
modes = ["realized", "dvol", "blend", "max", "derisk"]
series = {m: portfolio(m) for m in modes}
# vero inizio DVOL (dove TUTTE le varianti hanno dati validi) — non il primo indice del prezzo
dstart = max(pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet")["timestamp"].min() for a in ("BTC", "ETH"))
dstart = pd.Timestamp(dstart, unit="ms", tz="UTC") + pd.Timedelta(days=VW) # +warmup vol-win
series = {m: s[s.index >= dstart] for m, s in series.items()}
common = series["realized"].index
base = series["realized"]
print("=" * 96)
print(f" TP01 × DVOL — vol-target con denominatore di vol diverso. Finestra COMUNE "
f"{common[0].date()} -> {common[-1].date()} ({len(base)}g). Hold-out 2025+.")
print("=" * 96)
print(f" {'variante':<10} {'FULL Sh':>8} {'FULL DD':>8} {'CAGR':>7} | "
f"{'HOLD Sh':>8} {'HOLD ret':>9} {'HOLD DD':>8} | per-anno PnL")
for m in modes:
s = series[m]; f = metrics(s); h = metrics(s, lo=HOLDOUT)
ys = " ".join(f"{y}:{p*100:+.0f}" for y, p in sorted(f['yearly'].items()))
tag = " (baseline)" if m == "realized" else ""
print(f" {m:<10} {f['sharpe']:>+8.2f} {f['dd']*100:>7.1f}% {f['cagr']*100:>+6.0f}% | "
f"{h['sharpe']:>+8.2f} {h['tot']*100:>+8.1f}% {h['dd']*100:>7.1f}% | {ys}{tag}")
print("\n DELTA vs baseline (realized) sulla stessa finestra:")
bf = metrics(base); bh = metrics(base, lo=HOLDOUT)
for m in modes:
if m == "realized":
continue
f = metrics(series[m]); h = metrics(series[m], lo=HOLDOUT)
print(f" {m:<8}: ΔFULL Sh {f['sharpe']-bf['sharpe']:+.2f} ΔFULL DD {(f['dd']-bf['dd'])*100:+.1f}pp "
f"ΔHOLD Sh {h['sharpe']-bh['sharpe']:+.2f} ΔHOLD ret {(h['tot']-bh['tot'])*100:+.1f}pp")
print("\n CONTROLLO DECISIVO — il taglio di DD del DVOL è 'posizioni più piccole' o vero timing?")
print(" Confronto le varianti DVOL con il realized a target_vol RIDOTTO (stesso de-levering, ma")
print(" senza DVOL). Se realized-ridotto eguaglia/batte il DVOL a parità di DD → DVOL non aggiunge.")
for tv in (0.15, 0.13):
s = portfolio("realized", tvol=tv); s = s[s.index >= dstart]
f = metrics(s); h = metrics(s, lo=HOLDOUT)
print(f" realized @ vol-tgt {tv*100:.0f}%: FULL Sh {f['sharpe']:+.2f} DD {f['dd']*100:.1f}% "
f"CAGR {f['cagr']*100:+.0f}% | HOLD Sh {h['sharpe']:+.2f}")
mx = metrics(series["max"]); mxh = metrics(series["max"], lo=HOLDOUT)
print(f" (vs max-DVOL: FULL Sh {mx['sharpe']:+.2f} DD {mx['dd']*100:.1f}% "
f"CAGR {mx['cagr']*100:+.0f}% | HOLD Sh {mxh['sharpe']:+.2f})")
if __name__ == "__main__":
main()
+140
View File
@@ -0,0 +1,140 @@
"""BOOK DERIBIT-ONLY (TP01 + SKH01) — target NETTO per asset via NETTING SOFTWARE su un solo conto.
TP01 e SKH01 tradano lo STESSO strumento (BTC/ETH _USDC-PERPETUAL). Su Deribit esiste UNA sola
posizione netta per strumento per conto -> non si possono tenere due gambe separate: si combinano in
software in un unico target netto, e si manda UN ordine per asset per raggiungerlo.
Formula (preserva il budget di rischio $300/asset, split 75/25; coerente col blend di ritorni
deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
net_target_usd[asset] = clamp( WEIGHT * E * (W_TP01 * tp_frac + W_SKH * skh_sign), ±CAP )
WEIGHT = 0.5 (book 50/50 BTC+ETH, come i due sleeve)
tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target
skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m)
E = equity reale del conto (fallback paper se offline)
CAP = max_notional_per_asset_usd (config/live.json, $300)
GLI EXIT DI SKH01 SONO IMPLICITI: _skyhook_positions() replica il book (ingressi + SL/TP/max_bars +
non-overlap) sulla feed certificata fresca -> quando il trade va chiuso ritorna 'flat' -> skh_sign=0
-> il target netto si aggiorna -> il reconciler chiude la quota SKH. NB: gli exit sono SOFTWARE
(no bracket on-book per SKH, sennò chiuderebbero anche la quota TP01) -> latenza fino alla chiusura
della barra 230m corrente. Solo il disaster-SL (-30%) resta on-book, sulla posizione NETTA.
Questo modulo NON invia nulla: costruisce solo il report/ordine. L'invio è in scripts/live/book_execute.py
(doppio gate). Causale: usa solo barre chiuse (eredita la causalità di TP01 e di _skyhook_positions).
"""
from __future__ import annotations
import json
from pathlib import Path
from src.live.deribit import INSTRUMENT, notional_to_amount
from src.live.shadow import ASSETS, WEIGHT, shadow_report
from src.portfolio.sleeves import _skyhook_positions
PROJECT_ROOT = Path(__file__).resolve().parents[2]
CONFIG = PROJECT_ROOT / "config" / "live.json"
# Pesi del book Deribit-only (vedi src/portfolio/sleeves.deribit_book_sleeves).
W_TP01 = 0.75
W_SKH = 0.25
FLAT_USD = 1.0
def _cap() -> float:
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
return float(cfg.get("max_notional_per_asset_usd", 300.0))
def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float,
weight: float = WEIGHT) -> float:
"""Target NETTO (USD notional, segno = direzione) di un asset del book. PURA, testabile.
Combina la frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25),
clampata al cap per-asset. Vedi formula nel docstring del modulo."""
raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign))
return max(-cap, min(cap, raw))
def _skh_sign(state) -> int:
if state == "flat" or not isinstance(state, dict):
return 0
return 1 if state.get("dir") == "LONG" else -1
def build_book_order(instrument: str, net_target_usd: float, current_pos_usd: float,
mark: float | None, min_usd: float = 5.0) -> dict | None:
"""COSTRUISCE (non invia) l'ordine per portare la posizione al target NETTO. Ritorna dict-ordine
o None se sotto-soglia. Gestisce long/short e i flip:
- |delta| < min_usd -> None (già a target);
- target ~0 -> CLOSE (reduce_only, esce sempre);
- flip di segno (long<->short) -> needs_flip=True (close + open, gestito dall'executor);
- stesso segno, |target|<|cur| -> REDUCE (reduce_only);
- altrimenti -> OPEN/INCREASE (buy se delta>0, sell se delta<0)."""
delta = net_target_usd - current_pos_usd
if abs(delta) < min_usd:
return None
side = "buy" if delta > 0 else "sell"
is_close = abs(net_target_usd) < FLAT_USD and abs(current_pos_usd) > FLAT_USD
needs_flip = (current_pos_usd > FLAT_USD and net_target_usd < -FLAT_USD) or \
(current_pos_usd < -FLAT_USD and net_target_usd > FLAT_USD)
same_sign = (net_target_usd > 0) == (current_pos_usd > 0)
is_reduce = is_close or (same_sign and abs(net_target_usd) < abs(current_pos_usd) and not needs_flip)
amount = notional_to_amount(instrument, abs(delta), price=mark)
if amount == 0.0 and not is_close:
return None
return dict(
instrument=instrument, side=side, amount=amount, type="market",
reduce_only=bool(is_reduce or is_close), needs_flip=bool(needs_flip), is_close=bool(is_close),
net_target=round(net_target_usd, 2), current=round(current_pos_usd, 2), delta=round(delta, 2),
)
def book_report(offline: bool = False, equity_override: float | None = None,
live_feed: bool = False) -> dict:
"""Stato completo del book (TP01+SKH01) NETTO, per asset. NON invia nulla. Serializzabile.
Riusa shadow_report (target TP01 + conto/posizioni/mark reali) e ci somma il segno di SKH01.
live_feed: se True usa il feed 5m fresco effimero per SKH01 (src/live/livefeed.fresh_5m) ->
segnale 230m all'ultima barra chiusa, per l'esecuzione reale. Default False (feed certificato,
deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero)."""
sh = shadow_report(offline=offline, equity_override=equity_override)
cap = _cap()
equity = sh["equity"]
load5m = None
if live_feed:
from src.live.livefeed import fresh_5m
load5m = fresh_5m
try:
skh = _skyhook_positions(load5m=load5m)
except Exception as e: # non bloccare il report se il feed SKH fallisce
skh = {a: "flat" for a in ASSETS}
sh = {**sh, "skh_error": f"{type(e).__name__}: {e}"}
assets, orders = [], []
for a_rec in sh["assets"]:
a = a_rec["asset"]
inst = INSTRUMENT[a]
tp_frac = float(a_rec["target"])
st = skh.get(a, "flat")
sign = _skh_sign(st)
net = book_net_target(tp_frac, sign, equity, cap)
cur = float(a_rec["position_usd"])
mark = a_rec["mark"]
order = build_book_order(inst, net, cur, mark, min_usd=5.0)
if order:
orders.append(order)
assets.append(dict(
asset=a, instrument=inst,
tp_frac=round(tp_frac, 4), skh_sign=sign,
skh_state=(st if st == "flat" else {k: st[k] for k in ("dir", "entry", "sl", "tp", "bars_in", "max_bars") if k in st}),
net_target=round(net, 2), position_usd=round(cur, 2), mark=mark, mark_src=a_rec.get("mark_src"),
order=order,
))
return dict(
last_data=sh["last_data"], online=sh["online"],
real_equity=sh["real_equity"], equity=equity, eq_basis=sh["eq_basis"],
cap_per_asset=cap, weights=dict(TP01=W_TP01, SKH01=W_SKH),
assets=assets, orders=orders,
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
)
+76 -15
View File
@@ -14,7 +14,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd import numpy as np, pandas as pd
from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT
from src.portfolio.sleeves import active_sleeves from src.portfolio.sleeves import active_sleeves, book_trade_frequency
from src.portfolio.gtaa import gtaa_weights from src.portfolio.gtaa import gtaa_weights
from src.live.shadow import shadow_report, tp01_trades from src.live.shadow import shadow_report, tp01_trades
from src.version import APP_VERSION from src.version import APP_VERSION
@@ -32,6 +32,21 @@ def build():
pf = StrategyPortfolio(active_sleeves(), capital=2000.0) pf = StrategyPortfolio(active_sleeves(), capital=2000.0)
bt = pf.backtest() bt = pf.backtest()
eq = bt["equity"]; idx = bt["index"] eq = bt["equity"]; idx = bt["index"]
# BOOK DERIBIT-ONLY eseguibile (TP01 75% + SKH01 25%), riusando i daily gia' calcolati
try:
sl_by = {s.name: s for s in pf.sleeves}
tp_d, sk_d = sl_by["TP01_trend_1d"].daily(), sl_by["SKH01_skyhook"].daily()
Jd = pd.concat({"t": tp_d, "s": sk_d}, axis=1, join="inner").fillna(0.0)
der = 0.75 * Jd["t"] + 0.25 * Jd["s"]
mm = ((1.0 + der).resample("ME").prod() - 1.0).dropna()
d_cagr = float(np.prod(1.0 + mm.values) ** (12.0 / len(mm)) - 1.0) if len(mm) else 0.0
cons = d_cagr * 0.5
rd = (1.0 + cons) ** (1.0 / 365.0) - 1.0 if cons > 0 else 0.0
deribit = dict(full=metrics(der), hold=metrics(der[der.index >= HOLDOUT]),
cagr=d_cagr, cons_cagr=cons, eday_5k=5000.0 * rd,
eq5_5y=5000.0 * (1.0 + cons) ** 5, eq5_10y=5000.0 * (1.0 + cons) ** 10)
except Exception as e:
deribit = {"error": f"{type(e).__name__}: {e}"}
# sparkline: subsample ~400 punti # sparkline: subsample ~400 punti
step = max(1, len(eq) // 400) step = max(1, len(eq) // 400)
spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)] spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)]
@@ -50,14 +65,18 @@ def build():
trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale
except Exception: except Exception:
trades = [] trades = []
try:
freq = book_trade_frequency() # trade/anno contati (SKH01 round-trip + TP01 turnover)
except Exception as e:
freq = {"error": f"{type(e).__name__}: {e}"}
data = dict( data = dict(
version=APP_VERSION, version=APP_VERSION,
last_data=str(idx[-1].date()), last_data=str(idx[-1].date()),
full=bt["full"], holdout=bt["holdout"], weights=bt["weights"], full=bt["full"], holdout=bt["holdout"], weights=bt["weights"],
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"], per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday, positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
combo=combo, gtaa_weights=gtaa_w, combo=combo, gtaa_weights=gtaa_w, deribit=deribit,
shadow=shadow, trades=trades, bh=None, shadow=shadow, trades=trades, freq=freq, bh=None,
) )
_CACHE.update(t=time.time(), data=data) _CACHE.update(t=time.time(), data=data)
return data return data
@@ -146,6 +165,18 @@ def html():
+ f" <span style='color:#8a93a0'>(asof {asof})</span>") + f" <span style='color:#8a93a0'>(asof {asof})</span>")
else: else:
gw_html = "n/d (cache ETF assente — gira fetch_ib_equities.py)" gw_html = "n/d (cache ETF assente — gira fetch_ib_equities.py)"
db = d.get("deribit")
if db and "error" not in db:
f2, h2 = db["full"], db["hold"]
deribit_html = (
f"FULL <b>Sh {f2['sharpe']:.2f}</b> ret {f2['ret']*100:+.0f}% DD {f2['maxdd']*100:.1f}% &nbsp;·&nbsp; "
f"HOLD-OUT <b>Sh {h2['sharpe']:.2f}</b> DD {h2['maxdd']*100:.1f}%<br>"
f"<span style='color:#8a93a0;font-size:13px'>accumulo (reinvesti le vincite, no leva) — CAGR storico "
f"<b>{db['cagr']*100:.0f}%</b>, conservativo <b>{db['cons_cagr']*100:.0f}%</b>: "
f"da €5k → ~€{db['eq5_5y']:,.0f} (5a) / ~€{db['eq5_10y']:,.0f} (10a) &nbsp;·&nbsp; "
f"run-rate oggi ~<b>€{db['eday_5k']:.2f}/g</b> @conservativo</span>")
else:
deribit_html = "n/d" + (f"{db['error']}" if db and db.get("error") else "")
sh = d.get("shadow") sh = d.get("shadow")
if sh and "error" not in sh: if sh and "error" not in sh:
bits = " &nbsp;·&nbsp; ".join( bits = " &nbsp;·&nbsp; ".join(
@@ -153,13 +184,13 @@ def html():
f"{a['target']:+.2f}x" for a in sh["assets"]) f"{a['target']:+.2f}x" for a in sh["assets"])
if sh.get("online"): if sh.get("online"):
eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?") eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?")
pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"]) shpos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"])
ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"]) ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"])
if sh.get("orders") else "nessuno (target flat / gia' a target)") if sh.get("orders") else "nessuno (target flat / gia' a target)")
dsl = sh.get("disaster_sls") or [] dsl = sh.get("disaster_sls") or []
dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl) dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl)
if dsl else "nessuno (flat)") if dsl else "nessuno (flat)")
shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {pos} · dato {sh['last_data']}<br>" shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {shpos} · dato {sh['last_data']}<br>"
f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>" f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>"
f"🛡️ disaster-SL attivi (30%): {dsl_txt}") f"🛡️ disaster-SL attivi (30%): {dsl_txt}")
else: else:
@@ -185,6 +216,24 @@ def html():
f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>") f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>")
if not live_trows: if not live_trows:
live_trows = "<tr><td colspan=6 style='color:#8a93a0'>nessun trade reale eseguito (o conto non leggibile dal container)</td></tr>" live_trows = "<tr><td colspan=6 style='color:#8a93a0'>nessun trade reale eseguito (o conto non leggibile dal container)</td></tr>"
fq = d.get("freq")
if fq and "error" not in fq:
skh = fq["skh"]
by = lambda a: " ".join(f"{y}:{c}" for y, c in skh[a]["by_year"].items())
tpy = fq["skh_combo_per_year"]
freq_html = (
f"<b>SKH01</b> (round-trip = apri+chiudi, no-overlap): BTC <b>~{fq['skh_per_year']['BTC']:.0f}/anno</b> · "
f"ETH <b>~{fq['skh_per_year']['ETH']:.0f}/anno</b> → combinato <b>~{tpy:.0f}/anno</b> "
f"(~{2*tpy:.0f} esecuzioni)<br>"
f"<b>TP01</b> (posizione continua vol-target, NON trade discreti): turnover "
f"<b>~{fq['tp01_turnover_per_year']:.0f}×/anno</b> per asset · zero quando flat/risk-off<br>"
f"<span style='color:#8a93a0;font-size:13px'>→ book eseguibile ~{tpy:.0f}-{tpy+5:.0f} operazioni/anno: "
f"poche per design (regime AND breakout). Fee ~$45-60/anno, gia' nette nei numeri storici · min-order $5 OK a $600.<br>"
f"SKH01 round-trip/anno — BTC: {by('BTC')} · ETH: {by('ETH')}</span>")
tpy_card = f"~{tpy:.0f}"
else:
freq_html = "n/d" + (f"{fq['error']}" if fq and fq.get("error") else "")
tpy_card = "~75"
return f"""<!doctype html><html><head><meta charset=utf-8> return f"""<!doctype html><html><head><meta charset=utf-8>
<meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title> <meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title>
<style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}} <style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}}
@@ -199,14 +248,26 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
.section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}} .section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}}
.section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body> .section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body>
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)</h1> <h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)</h1>
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)</div> <div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico</div>
<div class="section">PAPER — simulato (backtest + forward virtuale)</div>
<div class="section live">① LIVE — Deribit mainnet (conto reale, sola lettura)</div>
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
<p class=warn>Solo TP01 e' armato live (cap $300/asset · disaster-SL on-book 30% · capitale reale ≈ $600). SKH01/XS01/VRP01 = paper/stat-mode. Lo "Shadow" mostra il target reale ma NON invia ordini.</p>
<div class="section">② TRADES ESEGUITI (reali) + frequenza operativa</div>
<div class=box><b>Frequenza operativa</b> (contata sui dati certificati, non stimata):<br>{freq_html}</div>
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
<p class=warn>Oggi TP01 e' in target TSMOM risk-off → flat: nessun ordine reale. La tabella si popola quando il segnale arma una posizione e il cron giornaliero la esegue.</p>
<div class="section">③ STORICO — simulato (backtest + forward virtuale)</div>
<div class=cards> <div class=cards>
<div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div> <div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div>
<div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div> <div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div>
<div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div> <div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div>
<div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div> <div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div>
<div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div> <div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div>
<div class=card><div class=k>trade/anno (book eseguibile)</div><div class=v>{tpy_card}</div></div>
</div> </div>
<div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div> <div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div>
<div class=box><b>Paper forward-only:</b> {paper_html}</div> <div class=box><b>Paper forward-only:</b> {paper_html}</div>
@@ -214,21 +275,21 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
<table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table> <table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table>
<h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3> <h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3>
<table>{pos}</table> <table>{pos}</table>
<h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale, ultimi 15)</h3> <h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale SIMULATO, ultimi 15)</h3>
<table><tr><th>data</th><th>asset</th><th>azione</th><th>posizione</th><th>prezzo</th></tr>{trows}</table> <table><tr><th>data</th><th>asset</th><th>azione</th><th>posizione</th><th>prezzo</th></tr>{trows}</table>
<div style="margin-top:10px">{yrs}</div> <div style="margin-top:10px">{yrs}</div>
<div class="section">COMBO DEPLOYABLE — cross-venue (paper, forward-only)</div> <div class="section">③·a COMBO DEPLOYABLE — cross-venue (paper, forward-only)</div>
<div class=box><b>TP01 (Deribit) + GTAA (IB)</b> — le DUE gambe ESEGUIBILI a basso capitale, scorrelate (corr ~0.21) → blend Sharpe ~1.5, drawdown dimezzato. <b>Nessuna esecuzione reale</b>:<br>{combo_html}<br> <div class=box><b>TP01 (Deribit) + GTAA (IB)</b> — le DUE gambe ESEGUIBILI a basso capitale, scorrelate (corr ~0.21) → blend Sharpe ~1.5, drawdown dimezzato. <b>Nessuna esecuzione reale</b>:<br>{combo_html}<br>
<span style="color:#8a93a0;font-size:13px">posizioni azionabili IB (GTAA, peso ETF):</span> {gw_html}</div> <span style="color:#8a93a0;font-size:13px">posizioni azionabili IB (GTAA, peso ETF):</span> {gw_html}</div>
<p class=warn>⚠️ PAPER cross-venue: valida l'operativita' su due conti (Deribit + IB) a rischio zero. Lo Sharpe ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la diversificazione (corr 0.21, DD dimezzato), non il livello. XS01/VRP01 esclusi (STAT-MODE): qui solo TP01+GTAA.</p> <p class=warn>⚠️ PAPER cross-venue: valida l'operativita' su due conti (Deribit + IB) a rischio zero. Lo Sharpe ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la diversificazione (corr 0.21, DD dimezzato), non il livello. XS01/VRP01 esclusi (STAT-MODE): qui solo TP01+GTAA.</p>
<div class="section">FORWARD-MONITOR — lead paper (non deploy)</div> <div class="section">③·b BOOK DERIBIT-ONLY — eseguibile (TP01 + SKH01)</div>
<div class=box><b>TP01 75% + SKH01 25%</b> — le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), ribilancio mensile. Sottoinsieme realmente armabile (XS01/VRP01 esclusi):<br>{deribit_html}<br>
<span style="color:#8a93a0;font-size:13px">forecast: <code>scripts/portfolio/forecast_deribit_book.py</code> · report: <code>scripts/portfolio/run_deribit_book.py</code></span></div>
<p class=warn>⚠️ Accumulo = proiezione condizionata (storico bull crypto → pianifica sul conservativo); nessuna leva; SKH01 research/forward-monitor (solo TP01 armato live). A €50/g servono ~€177k @conservativo: la via è capitale+tempo, non leva.</p>
<div class="section">③·c FORWARD-MONITOR — lead paper (non deploy)</div>
<div class=box><b>PREVDAY range-breakout</b> — lead ORTOGONALE a TP01 (corr ~0.15 full / ~0 hold; marginal ADDS, non-hedge, robusto allo shift del confine-giorno). Forward-only, <b>nessuna esecuzione reale</b>:<br>{prevday_html}</div> <div class=box><b>PREVDAY range-breakout</b> — lead ORTOGONALE a TP01 (corr ~0.15 full / ~0 hold; marginal ADDS, non-hedge, robusto allo shift del confine-giorno). Forward-only, <b>nessuna esecuzione reale</b>:<br>{prevday_html}</div>
<p class=warn>⚠️ LEAD in osservazione, NON deployato. Sopravvissuto alla verifica avversariale dell'onda intraday; lo teniamo in paper per validarlo fuori-campione-vero. I due libri (modeled vs real-$600) mostrano l'haircut di fill che lo scettico aveva segnalato.</p> <p class=warn>⚠️ LEAD in osservazione, NON deployato. Sopravvissuto alla verifica avversariale dell'onda intraday; lo teniamo in paper per validarlo fuori-campione-vero. I due libri (modeled vs real-$600) mostrano l'haircut di fill che lo scettico aveva segnalato.</p>
<div class="section live">LIVE — Deribit mainnet (conto reale, sola lettura)</div> <p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). SKH01 (Skyhook dual-TF regime+breakout, BTC/ETH) = diversificatore quasi-ortogonale (corr ~0.09) aggiunto @25%: alza il FULL Sharpe del portafoglio 1.68→2.13 e dimezza il DD (14→8%) — RESEARCH/forward-monitor (book a 230m, causalita' verificata su harness ma costi reali e codice d'esecuzione da validare prima del deploy). TP01 e' l'unico deployable pieno.</p>
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
<p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). SKH01 (Skyhook dual-TF regime+breakout, BTC/ETH) = diversificatore quasi-ortogonale (corr ~0.09) aggiunto @25%: alza il FULL Sharpe del portafoglio 1.68→2.13 e dimezza il DD (14→8%) — RESEARCH/forward-monitor (book a 230m, causalita' verificata su harness ma costi reali e codice d'esecuzione da validare prima del deploy). TP01 e' l'unico deployable pieno: lo "Shadow live" mostra cosa farebbe sul mainnet, ma NON invia ordini.</p>
</body></html>""" </body></html>"""
+41
View File
@@ -140,6 +140,47 @@ class DeribitTrader(DeribitRead):
return [self.open(instrument, "buy", amount, label="tp01-buy")] return [self.open(instrument, "buy", amount, label="tp01-buy")]
return [self._submit(instrument, "sell", amount, reduce_only=True, label="tp01-reduce")] return [self._submit(instrument, "sell", amount, reduce_only=True, label="tp01-reduce")]
# --- RIBILANCIO al target CON SEGNO (book TP01+SKH01: long / short / flip) ---
def rebalance_signed(self, instrument: str, target_notional_usd: float, mark: float,
min_usd: float = 5.0) -> list[Fill]:
"""Porta la posizione su `instrument` al target NETTO con SEGNO (long-short, a differenza di
rebalance_to che e' long-only). Gestisce i flip chiudendo prima e riaprendo dall'altro lato.
- |delta| < min_usd -> niente;
- flip di segno -> close() (sempre permessa) poi apre dall'altra parte;
- target ~0 -> close();
- stesso segno, |target|<|cur| -> REDUCE reduce_only;
- apertura/aumento -> open buy/sell (capped dal guardrail apertura).
Ritorna i Fill eseguiti."""
cur = self.position_usd(instrument)
if abs(target_notional_usd - cur) < min_usd:
return []
fills: list[Fill] = []
crossing = (cur > FLAT_USD and target_notional_usd < -FLAT_USD) or \
(cur < -FLAT_USD and target_notional_usd > FLAT_USD)
if crossing: # flip: flatta, poi riparti da zero
f = self.close(instrument, label="book-flip-close")
if f:
fills.append(f)
cur = 0.0
if abs(target_notional_usd) < FLAT_USD: # target flat -> esci (sempre permessa)
if abs(cur) > FLAT_USD:
f = self.close(instrument, label="book-exit")
if f:
fills.append(f)
return fills
delta = target_notional_usd - cur
amount = notional_to_amount(instrument, abs(delta), price=mark)
if amount <= 0:
return fills
same_sign = (target_notional_usd > 0) == (cur > 0)
if cur != 0.0 and same_sign and abs(target_notional_usd) < abs(cur):
side = "sell" if cur > 0 else "buy" # riduci nello stesso verso, reduce_only
fills.append(self._submit(instrument, side, amount, reduce_only=True, label="book-reduce"))
else:
side = "buy" if target_notional_usd > 0 else "sell" # apri/aumenta verso il target
fills.append(self.open(instrument, side, amount, label="book-open"))
return fills
# --- DISASTER BRACKET (assicurazione on-book per outage; da Old) --- # --- DISASTER BRACKET (assicurazione on-book per outage; da Old) ---
def place_disaster_sl(self, instrument: str, side_held: str, amount: float, def place_disaster_sl(self, instrument: str, side_held: str, amount: float,
stop_price: float, label: str = "disaster-sl") -> Fill: stop_price: float, label: str = "disaster-sl") -> Fill:
+83
View File
@@ -0,0 +1,83 @@
"""FEED LIVE EFFIMERO per il segnale SKH01 (book a 230m) — NON tocca i dati certificati su disco.
SKH01 decide su griglia 230m: per eseguirlo fedelmente il segnale serve fresco all'ultima barra
chiusa. Il rebuild certificato (rebuild_history.py) gira 1×/giorno e fa un rebuild COMPLETO (pesante):
girarlo ogni ora sarebbe sbagliato e violerebbe la regola "aggiornare lo storico SOLO con
rebuild_history + certificare". Quindi qui NON scriviamo su disco: carichiamo il 5m CERTIFICATO e gli
appendiamo IN MEMORIA una coda recente presa da Deribit PUBBLICO (ccxt, tokenless, STESSO simbolo
inverse del feed certificato -> prezzi entro ~3 bps). I dati certificati restano la verità su disco;
questa estensione vive solo nel processo live e per il calcolo del segnale.
Robusto ai fallimenti: qualunque errore di rete/fetch -> ritorna il feed certificato invariato (il
runner degrada a "fermo all'ultimo dato certificato", mai opera a cieco). Solo SKH01 ne ha bisogno:
TP01 è giornaliero e gira bene sul feed certificato.
"""
from __future__ import annotations
import time
import pandas as pd
from src.data.downloader import load_data
# STESSO simbolo del feed certificato (vedi scripts/analysis/rebuild_history.DERIBIT_INSTR):
# inverse USD perp, storia lunga, entro ~3 bps dal lineare USDC su cui eseguiamo.
DERIBIT_SYMBOL = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"}
SCHEMA = ["timestamp", "open", "high", "low", "close", "volume"]
def _fetch_recent_5m(symbol: str, lookback_days: int) -> pd.DataFrame:
"""Coda recente di 5m da Deribit pubblico (ccxt). Paginazione in avanti. Solo letture pubbliche."""
import ccxt
ex = ccxt.deribit({"enableRateLimit": True})
tf_ms = 5 * 60 * 1000
since = int((time.time() - lookback_days * 86400) * 1000)
rows: dict[int, list] = {}
guard = 0
while guard < 200:
guard += 1
try:
r = ex.fetch_ohlcv(symbol, "5m", since=since, limit=1000)
except Exception:
break
r = [x for x in r if int(x[0]) >= since]
if not r:
break
for x in r:
t = int(x[0])
rows[t] = [t, float(x[1]), float(x[2]), float(x[3]), float(x[4]), float(x[5] or 0)]
nxt = int(r[-1][0]) + tf_ms
if nxt <= since:
break
since = nxt
if not rows:
return pd.DataFrame(columns=SCHEMA)
return pd.DataFrame(rows.values(), columns=SCHEMA).sort_values("timestamp").reset_index(drop=True)
def merge_tail(base: pd.DataFrame, tail: pd.DataFrame) -> pd.DataFrame:
"""Fonde la coda fresca sul feed certificato: concat, dedup per timestamp (la coda VINCE sui
duplicati, ma le barre certificate storiche restano), riordina. Mantiene lo schema di load_data
(inclusa 'datetime' se presente). PURA, testabile senza rete."""
if tail is None or tail.empty:
return base
cols = [c for c in SCHEMA if c in base.columns]
t = tail[[c for c in SCHEMA if c in tail.columns]].copy()
merged = pd.concat([base[cols], t], ignore_index=True)
merged = merged.drop_duplicates("timestamp", keep="last").sort_values("timestamp").reset_index(drop=True)
# ricostruisci 'datetime' coerente (build_frames non la usa, ma load_data la espone)
merged["datetime"] = pd.to_datetime(merged["timestamp"], unit="ms", utc=True)
return merged
def fresh_5m(asset: str, lookback_days: int = 12) -> pd.DataFrame:
"""Feed 5m certificato + coda recente effimera (in-memory). Fallback al certificato su errore."""
base = load_data(asset, "5m")
sym = DERIBIT_SYMBOL.get(asset)
if sym is None:
return base
try:
tail = _fetch_recent_5m(sym, lookback_days)
except Exception:
return base
return merge_tail(base, tail)
+38
View File
@@ -68,6 +68,44 @@ def yearly(daily: pd.Series) -> dict:
return out return out
def rebalance_sim(daily_cols: dict[str, pd.Series], weights: dict,
period_days: int, cost_rate: float = 0.0005) -> dict:
"""Ribilanciamento PERIODICO REALISTICO (vs il rebalance-continuo implicito di combined_daily).
Tra una data di ribilanciamento e l'altra ogni sleeve DERIVA col suo rendimento (i pesi si
scostano dal target). Ogni `period_days` si riporta al target pagando il turnover:
cost = cost_rate * sum_i |valore_i - target_i| (cost_rate = fee per-lato, Deribit taker ~0.0005)
Modella l'attrito reale che il rebalance-continuo (combined_daily) ignora. period_days=1 con
cost_rate=0 ricade sul rebalance-continuo. Ritorna serie netta + turnover annuo + n ribilanci."""
J = pd.concat(daily_cols, axis=1, join="inner").sort_index().fillna(0.0)
cols = list(J.columns)
w = np.array([weights[c] for c in cols], float); w = w / w.sum()
R = J.values
n = len(J)
E = 1.0
v = w * E
out = np.zeros(n)
n_rebal = 0
turn_tot = 0.0
for t in range(n):
Eprev = E
v = v * (1.0 + R[t])
E = float(v.sum())
if (t + 1) % period_days == 0: # giorno di ribilanciamento
target = w * E
turn = float(np.abs(v - target).sum())
cost = cost_rate * turn
E -= cost
v = w * E
n_rebal += 1
turn_tot += turn / max(Eprev, 1e-12)
out[t] = E / Eprev - 1.0 if Eprev > 0 else 0.0
years = n / DAYS_PER_YEAR
return dict(daily=pd.Series(out, index=J.index),
turnover_per_year=round(turn_tot / years, 2) if years > 0 else 0.0,
n_rebalances=n_rebal, period_days=period_days, cost_rate=cost_rate)
class StrategyPortfolio: class StrategyPortfolio:
def __init__(self, sleeves: list[Sleeve], capital: float = 2000.0): def __init__(self, sleeves: list[Sleeve], capital: float = 2000.0):
if not sleeves: if not sleeves:
+92 -1
View File
@@ -236,8 +236,86 @@ def _skyhook_returns() -> pd.Series:
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def _skyhook_positions(load5m=None) -> dict:
"""Stato corrente del book Skyhook per asset (introspezione live): se c'e' un trade APERTO ORA
-> dir/entry/sl/tp/barre-trascorse; altrimenti 'flat'. Replica la logica non-overlap di
entry+exit (TP/SL/max_bars) fino all'ultima barra 230m chiusa. Causale: usa solo barre chiuse.
load5m: callable(asset)->df5 opzionale (per il live: feed certificato + coda fresca effimera,
vedi src/live/livefeed.fresh_5m). Default = feed certificato su disco (load_data)."""
_load = load5m if load5m is not None else (lambda a: load_data(a, "5m"))
out = {}
for a in ASSETS:
ltf, htf = build_frames(_load(a))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
H = ltf["high"].values; L = ltf["low"].values; Cc = ltf["close"].values
n = len(ltf); i = 0; open_pos = "flat"
while i < n:
e = ent[i]
if e is None:
i += 1; continue
d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
exit_idx = None
for s in range(1, mb + 1):
j = i + s
if j >= n: # non ancora uscito: posizione APERTA ora
break
hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp)
if hit or s == mb:
exit_idx = j; break
if exit_idx is None:
open_pos = dict(dir="LONG" if d == 1 else "SHORT", entry=round(float(Cc[i]), 2),
sl=round(float(sl), 2), tp=round(float(tp), 2),
bars_in=int((n - 1) - i), max_bars=int(mb))
break
i = exit_idx + 1
out[a] = open_pos
return out
def skyhook_sleeve(weight: float = 0.25) -> Sleeve: def skyhook_sleeve(weight: float = 0.25) -> Sleeve:
return Sleeve("SKH01_skyhook", weight, _skyhook_returns) return Sleeve("SKH01_skyhook", weight, _skyhook_returns, pos_fn=_skyhook_positions)
_TRADE_FREQ_CACHE = None
def book_trade_frequency() -> dict:
"""Frequenza operativa del book eseguibile (TP01+SKH01), conteggiata sui dati certificati.
Strategia + storico fissi -> cache a livello di MODULO: calcolata una sola volta per processo
(le nuove barre aggiungono pochissimo). Niente magic-number: e' contata, non stimata.
- SKH01: round-trip DISCRETI (apri+chiudi) no-overlap dell'harness, per asset e per anno.
- TP01: posizione CONTINUA vol-target -> 'turnover' annuo (somma |Δpeso|), non trade discreti."""
global _TRADE_FREQ_CACHE
if _TRADE_FREQ_CACHE is not None:
return _TRADE_FREQ_CACHE
skh = {}
for a in ASSETS:
ltf, htf = build_frames(load_data(a, "5m"))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
yr = pd.to_datetime(pd.Series(ltf["timestamp"]).astype("int64"), unit="ms", utc=True).dt.year.values
yc: dict[int, int] = {}
for i, e in enumerate(ent):
if e is not None:
y = int(yr[i]); yc[y] = yc.get(y, 0) + 1
skh[a] = dict(by_year={int(k): int(v) for k, v in sorted(yc.items())},
total=int(sum(yc.values())), first=int(min(yc)), last=int(max(yc)))
sf = min(v["first"] for v in skh.values()); sl = max(v["last"] for v in skh.values())
skh_combo = sum(v["total"] for v in skh.values()) / (sl - sf + 1)
tp = TrendPortfolio(**CANONICAL); turns = []
for a in ASSETS:
df = resample_1d(load_data(a, "1h"))
tgt = np.asarray(tp.target_series(df), dtype=float)
years = pd.DatetimeIndex(pd.to_datetime(df["datetime"])).year # ndarray-aligned key
d = np.abs(np.diff(np.nan_to_num(tgt, nan=0.0), prepend=0.0)) # turnover = somma |Δpeso|
ty = pd.Series(d).groupby(years).sum() # groupby posizionale (RangeIndex)
turns.append(float(ty.mean()))
_TRADE_FREQ_CACHE = dict(
skh=skh,
skh_per_year={a: skh[a]["total"] / (skh[a]["last"] - skh[a]["first"] + 1) for a in ASSETS},
skh_combo_per_year=skh_combo,
tp01_turnover_per_year=sum(turns) / len(turns))
return _TRADE_FREQ_CACHE
# ----------------------------- REGISTRY ----------------------------- # ----------------------------- REGISTRY -----------------------------
@@ -253,3 +331,16 @@ def active_sleeves() -> list[Sleeve]:
vrp_sleeve(weight=0.15), # options short-vol (put credit spread + gate IV-rank), dal 2021 (lead modellato, scorrelato) vrp_sleeve(weight=0.15), # options short-vol (put credit spread + gate IV-rank), dal 2021 (lead modellato, scorrelato)
skyhook_sleeve(weight=0.25), # dual-TF regime+breakout BTC/ETH, dal 2019 (quasi-ortogonale, exit %-asimmetrici, research) skyhook_sleeve(weight=0.25), # dual-TF regime+breakout BTC/ETH, dal 2019 (quasi-ortogonale, exit %-asimmetrici, research)
] ]
def deribit_book_sleeves() -> list[Sleeve]:
"""BOOK DERIBIT-ONLY realmente eseguibile (TP01 + SKH01, 75/25): le DUE gambe direzionali
BTC/ETH sullo stesso venue (Deribit), entrambe dal 2019. Esclude XS01 (Hyperliquid, stat-mode)
e VRP01 (opzioni modellate). FULL Sharpe ~1.78 / HOLD ~1.17 / DD ~9% (research; SKH01 daily-step).
Pensato per il deploy reale a basso capitale: stesso conto, stesso feed, ribilanciamento
periodico (vedi src.portfolio.portfolio.rebalance_sim + scripts/portfolio/run_deribit_book.py).
TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione)."""
return [
tp01_sleeve(weight=0.75),
skyhook_sleeve(weight=0.25),
]
+230
View File
@@ -0,0 +1,230 @@
"""Test del BOOK DERIBIT-ONLY live (TP01+SKH01 nettati in software, un solo conto).
Coprono: la formula di netting (sizing 75/25 + cap, long/short/flat/flip), la PARITA' coi pesi del
backtest (deribit_book_sleeves), la sicurezza del gate (disarmato -> nessun ordine), e il reconcile
CON SEGNO (close+open sui flip, reduce reduce_only) — senza toccare la rete (trader fittizio).
"""
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.book import W_SKH, W_TP01, book_net_target, build_book_order
from src.live.execution import Fill
# ---------------------------------------------------------------------------
# Formula di netting: 75/25, cap, combinazioni long/short/flat.
# ---------------------------------------------------------------------------
def test_net_target_sizing():
E, cap = 600.0, 300.0
assert book_net_target(0.0, 0, E, cap) == 0.0 # tutto flat
assert book_net_target(0.0, 1, E, cap) == 75.0 # solo SKH long = 0.25*0.5*600
assert book_net_target(0.0, -1, E, cap) == -75.0 # solo SKH short
assert book_net_target(1.0, 0, E, cap) == 225.0 # solo TP01 pieno = 0.75*0.5*600
assert book_net_target(1.0, -1, E, cap) == 150.0 # TP long + SKH short (hedge parziale)
assert book_net_target(1.0, 1, E, cap) == 300.0 # capped
assert book_net_target(2.0, 1, E, cap) == 300.0 # cap superiore
assert book_net_target(2.0, -1, E, cap) == 300.0 # 0.5*600*(1.5-0.25)=375 -> cap 300
def test_net_target_clamps_negative():
# TP flat e SKH short forte non sfora il cap negativo
assert book_net_target(0.0, -1, 4000.0, 300.0) == -300.0
# tp_frac negativo trattato come 0 (TP01 e' long-flat)
assert book_net_target(-5.0, 1, 600.0, 300.0) == 75.0
def test_weights_match_backtest_sleeves():
"""I pesi del book live DEVONO coincidere con quelli del backtest (deribit_book_sleeves)."""
from src.portfolio.sleeves import deribit_book_sleeves
w = {s.name.split("_")[0]: s.weight for s in deribit_book_sleeves()}
assert abs(w["TP01"] - W_TP01) < 1e-12 and abs(w["SKH01"] - W_SKH) < 1e-12
assert abs((W_TP01 + W_SKH) - 1.0) < 1e-12
# ---------------------------------------------------------------------------
# Costruzione ordine: side, reduce_only, flip, close, soglia minima.
# ---------------------------------------------------------------------------
def test_build_order_open_long():
o = build_book_order("BTC_USDC-PERPETUAL", 75.0, 0.0, 60000.0, min_usd=5.0)
assert o["side"] == "buy" and not o["reduce_only"] and not o["needs_flip"] and not o["is_close"]
def test_build_order_reduce_same_sign():
o = build_book_order("BTC_USDC-PERPETUAL", 75.0, 150.0, 60000.0)
assert o["side"] == "sell" and o["reduce_only"] and not o["needs_flip"]
def test_build_order_flip():
o = build_book_order("BTC_USDC-PERPETUAL", -75.0, 75.0, 60000.0)
assert o["needs_flip"] and o["side"] == "sell"
def test_build_order_close_to_flat():
o = build_book_order("BTC_USDC-PERPETUAL", 0.0, 75.0, 60000.0)
assert o["is_close"] and o["reduce_only"] and o["side"] == "sell"
def test_build_order_below_min_is_none():
assert build_book_order("BTC_USDC-PERPETUAL", 75.0, 73.0, 60000.0, min_usd=5.0) is None
assert build_book_order("BTC_USDC-PERPETUAL", 0.0, 0.0, 60000.0) is None
# ---------------------------------------------------------------------------
# Sicurezza del GATE: disarmato (execution_enabled=false) -> do_execute False.
# ---------------------------------------------------------------------------
def test_gate_requires_both_switches():
def do_execute(enabled, want_execute):
return bool(want_execute) and bool(enabled)
assert not do_execute(False, False)
assert not do_execute(True, False) # armato ma senza --execute
assert not do_execute(False, True) # --execute ma disarmato
assert do_execute(True, True) # solo con entrambi
def test_config_default_disarmed(tmp_path, monkeypatch):
"""load_config di book_execute mette execution_enabled=False di default (fail-safe)."""
import importlib
be = importlib.import_module("scripts.live.book_execute") if False else None
# carica il modulo via path (scripts/ non e' un package importabile per nome)
import importlib.util
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
monkeypatch.setattr(mod, "CONFIG", tmp_path / "nope.json") # config assente
assert mod.load_config()["execution_enabled"] is False
# ---------------------------------------------------------------------------
# Reconcile CON SEGNO (long/short/flip) senza rete: trader fittizio.
# ---------------------------------------------------------------------------
class FakeTrader:
"""Replica la logica di DeribitTrader.rebalance_signed registrando le chiamate, senza rete."""
def __init__(self, pos):
self.pos = float(pos)
self.calls = []
def position_usd(self, instrument):
return self.pos
def _mk(self, side, amount, reduce_only, label):
self.calls.append((label, side, round(amount, 6), reduce_only))
return Fill(instrument="X", side=side, amount=amount, filled=amount, price=60000.0,
fee_usdc=0.0, order_id="1", state="filled", verified=True)
def close(self, instrument, label="x"):
if abs(self.pos) < 1.0:
return None
f = self._mk("sell" if self.pos > 0 else "buy", abs(self.pos) / 60000.0, True, label)
self.pos = 0.0
return f
def open(self, instrument, side, amount, label="x"):
f = self._mk(side, amount, False, label)
self.pos += (amount * 60000.0) * (1 if side == "buy" else -1)
return f
def _submit(self, instrument, side, amount, *, reduce_only, label, **k):
f = self._mk(side, amount, reduce_only, label)
self.pos += (amount * 60000.0) * (1 if side == "buy" else -1)
return f
# importa il metodo reale da DeribitTrader (testiamo proprio quella logica)
from src.live.execution import DeribitTrader as _DT
rebalance_signed = _DT.rebalance_signed
def test_reconcile_open_from_flat():
t = FakeTrader(0.0)
t.rebalance_signed("BTC_USDC-PERPETUAL", 75.0, 60000.0, min_usd=5.0)
labels = [c[0] for c in t.calls]
assert labels == ["book-open"] and t.calls[0][1] == "buy"
def test_reconcile_flip_closes_then_opens():
t = FakeTrader(75.0) # long, target short -> flip
t.rebalance_signed("BTC_USDC-PERPETUAL", -75.0, 60000.0, min_usd=5.0)
labels = [c[0] for c in t.calls]
assert labels == ["book-flip-close", "book-open"]
assert t.calls[1][1] == "sell" # apre short dopo il close
def test_reconcile_reduce_same_sign_is_reduce_only():
t = FakeTrader(150.0)
t.rebalance_signed("BTC_USDC-PERPETUAL", 75.0, 60000.0, min_usd=5.0)
assert [c[0] for c in t.calls] == ["book-reduce"]
assert t.calls[0][3] is True # reduce_only
def test_reconcile_target_flat_closes():
t = FakeTrader(75.0)
t.rebalance_signed("BTC_USDC-PERPETUAL", 0.0, 60000.0, min_usd=5.0)
assert [c[0] for c in t.calls] == ["book-exit"]
def test_reconcile_below_min_noop():
t = FakeTrader(73.0)
t.rebalance_signed("BTC_USDC-PERPETUAL", 75.0, 60000.0, min_usd=5.0)
assert t.calls == []
# ---------------------------------------------------------------------------
# PARITA' d'integrazione: il report reale (offline, niente rete) applica ESATTAMENTE
# la formula pura per ogni asset -> wiring corretto e riproducibile.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Feed live effimero (src/live/livefeed): merge/dedup, fallback, loader onorato.
# Nessuna rete (la coda fresca è iniettata / il fetch è stubbato a errore).
# ---------------------------------------------------------------------------
def test_merge_tail_dedups_and_tail_wins():
import pandas as pd
from src.live.livefeed import merge_tail
base = pd.DataFrame({"timestamp": [0, 300000, 600000], "open": [1, 2, 3], "high": [1, 2, 3],
"low": [1, 2, 3], "close": [1, 2, 3], "volume": [1, 1, 1],
"datetime": pd.to_datetime([0, 300000, 600000], unit="ms", utc=True)})
tail = pd.DataFrame({"timestamp": [600000, 900000], "open": [9, 4], "high": [9, 4],
"low": [9, 4], "close": [9, 4], "volume": [2, 2]})
m = merge_tail(base, tail)
assert list(m["timestamp"]) == [0, 300000, 600000, 900000] # esteso + ordinato
assert m.loc[m["timestamp"] == 600000, "close"].iloc[0] == 9 # la coda VINCE sul duplicato
assert "datetime" in m.columns and m["datetime"].is_monotonic_increasing
def test_merge_tail_empty_returns_base():
import pandas as pd
from src.live.livefeed import merge_tail
base = pd.DataFrame({"timestamp": [0], "open": [1], "high": [1], "low": [1], "close": [1], "volume": [1]})
assert merge_tail(base, pd.DataFrame()).equals(base)
def test_fresh_5m_falls_back_to_certified_on_error(monkeypatch):
import src.live.livefeed as lf
from src.data.downloader import load_data
monkeypatch.setattr(lf, "_fetch_recent_5m", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("net")))
got = lf.fresh_5m("BTC")
base = load_data("BTC", "5m")
assert len(got) == len(base) and got["timestamp"].iloc[-1] == base["timestamp"].iloc[-1]
def test_skyhook_positions_honors_custom_loader():
from src.portfolio import sleeves
calls = []
def spy(a):
calls.append(a)
return sleeves.load_data(a, "5m")
pos = sleeves._skyhook_positions(load5m=spy)
assert set(pos.keys()) == {"BTC", "ETH"} and calls == ["BTC", "ETH"]
def test_book_report_uses_pure_formula_offline():
from src.live.book import book_report
r = book_report(offline=True, equity_override=600.0)
assert r["equity"] == 600.0 and r["cap_per_asset"] > 0
for a in r["assets"]:
expect = book_net_target(a["tp_frac"], a["skh_sign"], 600.0, r["cap_per_asset"])
assert abs(a["net_target"] - expect) < 1e-6, f"{a['asset']}: net incoerente con la formula"
assert a["skh_sign"] in (-1, 0, 1)
# offline -> conto assunto flat -> nessuna posizione reale, report deterministico
assert all(a["position_usd"] == 0.0 for a in r["assets"])
+36
View File
@@ -0,0 +1,36 @@
"""Lock della conclusione 2026-06-26 (CC01 cash-and-carry): il premio di funding e' REALE e
positivo, ma PROCYCLICO (si comprime nel bear) — NON un edge all-weather. Lo Sharpe headline ~13 e'
un artefatto (rischi di coda assenti dal dataset) -> non testiamo lo Sharpe, testiamo il FATTO
economico e la procyclicita'. Diario docs/diary/2026-06-26-cash-carry-hl.md."""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from cash_carry_hl import cc_returns, load_funding_panel, metrics # type: ignore
def test_funding_premium_is_real_and_positive():
"""Il carry esiste: funding aggregato BTC/ETH positivo, CC-static con CAGR > 0."""
r = cc_returns(["BTC", "ETH"], mode="static")
m = metrics(r)
assert m["gross_ann"] > 0.0 # premio di funding netto positivo
assert m["cagr"] > 0.0
def test_basis_markup_does_not_explain_high_sharpe():
"""Aggiungere il mark-to-market della base (Δpremium) NON sgonfia lo Sharpe in modo materiale:
prova che il rischio vero e' FUORI dal dataset (2022/liquidazione), non nel basis dei dati."""
naive = metrics(cc_returns(["BTC", "ETH"], mode="static"))["sharpe"]
basis = metrics(cc_returns(["BTC", "ETH"], mode="static", with_basis=True))["sharpe"]
assert basis > 0.7 * naive # cala poco -> il basis-from-data non e' il rischio nascosto
def test_carry_is_procyclical_fades_in_bear():
"""Il carry e' procyclico: il funding aggregato del toro 2024 supera quello del bear 2026."""
FUND, _ = load_funding_panel(["BTC", "ETH"])
agg = FUND.mean(axis=1)
by_year = agg.groupby(agg.index.year).sum()
assert by_year.get(2024, 0.0) > by_year.get(2026, 0.0)
+50
View File
@@ -0,0 +1,50 @@
"""Lock della conclusione 2026-06-26: il gamma scalping (long-vol) su BTC/ETH PERDE
strutturalmente — è lo specchio del VRP01, dal lato sbagliato del premio. Guardia contro
ri-litigare l'idea "scalping con copertura in opzioni" come edge.
Diario docs/diary/2026-06-26-gamma-scalp-options.md."""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from options_gamma_scalp import ( # type: ignore # noqa: E402
_bs_straddle, gamma_scalp_asset, rv_iv_diagnostic, to_daily_voltgt, _metrics,
)
def test_bs_straddle_positive_and_increasing_in_vol():
"""Premio straddle ATM > 0 e cresce con la vol implicita (sanity del pricer)."""
p_lo = _bs_straddle(100.0, 100.0, 7 / 365.25, 0.30)
p_hi = _bs_straddle(100.0, 100.0, 7 / 365.25, 0.90)
assert p_lo > 0
assert p_hi > p_lo
def test_long_gamma_loses_when_rv_below_iv_synthetic():
"""Sintetico: con prezzo PIATTO (RV=0) e IV>0, il long gamma deve perdere (solo theta)."""
# P&L per step = DG*(r^2 - sigma^2 dt); con r=0 ogni step e' -DG*sigma^2*dt < 0.
from options_gamma_scalp import OPT_FEE_FRAC # noqa: F401
sig = 0.60
# se RV=0 il gamma_pnl e' strettamente negativo -> ritorno negativo. Verifico via il segno
# del contributo per-step a r=0.
r = 0.0
contrib = (r * r - sig * sig * (1.0 / 365.25))
assert contrib < 0
def test_iv_exceeds_or_near_daily_rv_btc():
"""BTC: a campionamento giornaliero l'IV (DVOL) >= RV in media -> il long gamma paga il VRP."""
d = rv_iv_diagnostic("BTC")
assert d["iv"] > 0 and d["rv_daily"] > 0
assert d["spread_daily"] > 0.0 # IV - RV_1d > 0 su BTC (lato short paga)
def test_naked_gamma_scalp_standalone_sharpe_negative():
"""La conclusione: il book gamma-scalp nudo (BTC+ETH) ha Sharpe standalone NEGATIVO."""
wB = gamma_scalp_asset("BTC", mode="naked")
wE = gamma_scalp_asset("ETH", mode="naked")
daily = to_daily_voltgt(wB, wE)
assert daily.std() > 0
assert _metrics(daily)["sharpe"] < 0.0
+32 -1
View File
@@ -7,7 +7,7 @@ import numpy as np
import pandas as pd import pandas as pd
import pytest import pytest
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, rebalance_sim
def _const_sleeve(name, weight, val, n=400): def _const_sleeve(name, weight, val, n=400):
@@ -15,6 +15,37 @@ def _const_sleeve(name, weight, val, n=400):
return Sleeve(name, weight, lambda: pd.Series(val, index=idx)) return Sleeve(name, weight, lambda: pd.Series(val, index=idx))
def _ret_series(vals):
idx = pd.date_range("2020-01-01", periods=len(vals), freq="1D", tz="UTC")
return pd.Series(vals, index=idx)
def test_rebalance_sim_no_cost_period1_matches_continuous():
"""period=1 + cost=0 deve coincidere col rebalance-continuo (weighted-return giornaliero)."""
rng = np.random.default_rng(0)
A = _ret_series(rng.normal(0.001, 0.02, 300))
B = _ret_series(rng.normal(0.000, 0.03, 300))
w = {"A": 0.6, "B": 0.4}
sim = rebalance_sim({"A": A, "B": B}, w, period_days=1, cost_rate=0.0)
cont = 0.6 * A + 0.4 * B
assert np.allclose(sim["daily"].values, cont.values, atol=1e-12)
assert sim["n_rebalances"] == 300
def test_rebalance_sim_cost_reduces_return_and_counts():
"""Il costo del turnover abbassa il rendimento; ribilanci meno frequenti = meno costo."""
rng = np.random.default_rng(1)
A = _ret_series(rng.normal(0.001, 0.02, 360))
B = _ret_series(rng.normal(0.001, 0.04, 360))
w = {"A": 0.5, "B": 0.5}
free = rebalance_sim({"A": A, "B": B}, w, period_days=7, cost_rate=0.0)["daily"]
weekly = rebalance_sim({"A": A, "B": B}, w, period_days=7, cost_rate=0.001)
monthly = rebalance_sim({"A": A, "B": B}, w, period_days=30, cost_rate=0.001)
assert weekly["daily"].sum() < free.sum() # il costo morde
assert monthly["n_rebalances"] < weekly["n_rebalances"] # mensile ribilancia meno
assert weekly["turnover_per_year"] > 0
def test_single_sleeve_equals_itself(): def test_single_sleeve_equals_itself():
s = _const_sleeve("A", 1.0, 0.001) s = _const_sleeve("A", 1.0, 0.001)
pf = StrategyPortfolio([s]) pf = StrategyPortfolio([s])
+10
View File
@@ -129,6 +129,16 @@ def test_short_override_changes_only_shorts():
assert longs_same > 0 and shorts_diff > 0 assert longs_same > 0 and shorts_diff > 0
def test_skyhook_pos_fn_structure():
"""pos_fn introspettiva: dict BTC/ETH, ciascuno 'flat' o dict con dir/entry/sl/tp coerenti."""
from src.portfolio.sleeves import _skyhook_positions
pos = _skyhook_positions()
assert set(pos.keys()) == {"BTC", "ETH"}
for a, p in pos.items():
assert p == "flat" or (isinstance(p, dict) and p["dir"] in ("LONG", "SHORT")
and p["sl"] > 0 and p["tp"] > 0 and 0 <= p["bars_in"] <= p["max_bars"])
def test_v2dd_robust_both_assets(): def test_v2dd_robust_both_assets():
"""SKH01-V2-DD: PASS netto fee su BTC&ETH, hold-out forte, e maxDD standalone <30%.""" """SKH01-V2-DD: PASS netto fee su BTC&ETH, hold-out forte, e maxDD standalone <30%."""
import skyhooklib as sk import skyhooklib as sk
+46
View File
@@ -0,0 +1,46 @@
"""Lock della conclusione 2026-06-26: il DVOL come denominatore del vol-target NON migliora TP01
risk-adjusted. Il taglio di DD delle varianti DVOL è solo de-levering, replicabile MEGLIO con un
target_vol più basso sul realizzato. Diario docs/diary/2026-06-26-tp01-dvol-overlay.md."""
import sys
from pathlib import Path
import pandas as pd
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from tp01_dvol_overlay import portfolio, metrics, RAW, VW # type: ignore
def _dstart():
s = max(pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet")["timestamp"].min() for a in ("BTC", "ETH"))
return pd.Timestamp(s, unit="ms", tz="UTC") + pd.Timedelta(days=VW)
def test_dvol_does_not_beat_realized_risk_adjusted():
"""Sulla finestra comune, il realized batte (o eguaglia) le varianti DVOL sullo Sharpe FULL."""
lo = _dstart()
base = metrics(portfolio("realized")[portfolio("realized").index >= lo])
mx = metrics(portfolio("max")[portfolio("max").index >= lo])
dv = metrics(portfolio("dvol")[portfolio("dvol").index >= lo])
assert base["sharpe"] >= mx["sharpe"] - 1e-9
assert base["sharpe"] >= dv["sharpe"] - 1e-9
def test_lower_target_vol_replicates_dd_cut_at_better_sharpe():
"""Il taglio di DD del DVOL è solo de-levering: realized @ vol-tgt 15% eguaglia il DD del
max-DVOL a Sharpe FULL non inferiore (anzi superiore)."""
lo = _dstart()
mx = metrics(portfolio("max")[portfolio("max").index >= lo])
lo_tv = metrics(portfolio("realized", tvol=0.15)[portfolio("realized", tvol=0.15).index >= lo])
assert lo_tv["dd"] <= mx["dd"] + 0.01 # stesso DD (entro 1pp)
assert lo_tv["sharpe"] >= mx["sharpe"] # a Sharpe non inferiore
def test_dvol_spike_gate_is_redundant_with_trend():
"""Il gate DVOL-spike de-risk non cambia nulla: TP01 è già flat nei crash (momentum<0)."""
lo = _dstart()
base = portfolio("realized"); dr = portfolio("derisk")
base = base[base.index >= lo]; dr = dr[dr.index >= lo]
assert abs(metrics(base)["sharpe"] - metrics(dr)["sharpe"]) < 0.02
+26
View File
@@ -0,0 +1,26 @@
"""Test offline del logger forward della vol term-structure (2026-06-26). La STORIA per-scadenza
non e' pubblica su Deribit -> calendar-vol non backtestabile ora; questo logger costruisce il dataset
in avanti. Si testa la pura interpolazione ai tenor fissi. Diario 2026-06-26-vol-termstructure-feasibility.md."""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from log_vol_termstructure import build_row, TENORS # type: ignore
def test_build_row_interpolates_all_tenors():
"""Curva sintetica in contango -> riga con tutti i tenor + slope positivo."""
curve = [(7, 40.0), (30, 45.0), (90, 50.0), (180, 55.0)]
row = build_row(100000.0, curve, now_ms=1_700_000_000_000)
assert all(f"iv_{t}d" in row for t in TENORS)
assert abs(row["iv_30d"] - 45.0) < 1e-6 # nodo esatto
assert 45.0 < row["iv_60d"] < 50.0 # interpolato tra 30 e 90
assert row["slope_7_180"] > 0 # contango -> slope positivo
def test_build_row_needs_two_points():
assert build_row(100.0, [(30, 50.0)], now_ms=1_700_000_000_000) is None
assert build_row(100.0, [], now_ms=1_700_000_000_000) is None